diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5dcc0ce3c..b5469bf9f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,6 +45,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 if: steps.skip.outputs.skip != 'true' with: + bun-version: 1.3.14 no-cache: true - name: Install bun deps (for SPA verify) @@ -54,6 +55,8 @@ jobs: - name: Set up Rust if: steps.skip.outputs.skip != 'true' uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: taiki-e/install-action@773334c0e05d7e699e4d78234494308223f3a2cf # nextest if: steps.skip.outputs.skip != 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75b216f91..6900c4dea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,7 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: + bun-version: 1.3.14 no-cache: true - name: Install bun deps @@ -67,6 +68,7 @@ jobs: - name: Set up Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: + toolchain: 1.97.1 targets: ${{ matrix.target }} - name: Set up zig @@ -136,7 +138,7 @@ jobs: release: name: Release needs: compile - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write steps: @@ -164,7 +166,7 @@ jobs: docker: name: Docker image needs: compile - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write @@ -257,7 +259,7 @@ jobs: name: Update Homebrew Formula needs: release if: ${{ !contains(github.ref_name, '-') }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release permissions: contents: read @@ -306,7 +308,7 @@ jobs: name: Update Homebrew Nightly Formula needs: release if: ${{ contains(github.ref_name, '-') }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: release permissions: contents: read diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 12ba1b9cc..b3686f872 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -101,6 +101,8 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true @@ -116,6 +118,8 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true @@ -140,6 +144,8 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: cache-on-failure: true diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 064c38c3d..f2086669e 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -44,6 +44,8 @@ jobs: with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 - run: bun install --frozen-lockfile - run: cd apps/fabro-web && bun run typecheck - run: cd lib/packages/fabro-api-client && bun run typecheck @@ -58,6 +60,8 @@ jobs: with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 - run: bun install --frozen-lockfile - run: cd apps/fabro-web && bun run test @@ -71,7 +75,11 @@ jobs: with: persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 - run: bun install --frozen-lockfile - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + with: + toolchain: 1.97.1 - run: cargo --locked dev build -- --locked -p fabro-cli --release - run: wc -c < target/release/fabro diff --git a/Cargo.lock b/Cargo.lock index d2f53dfa9..800846e58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2255,7 +2255,7 @@ dependencies = [ [[package]] name = "fabro-acp" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "fabro-agent" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2320,7 +2320,7 @@ dependencies = [ [[package]] name = "fabro-api" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "fabro-automation", @@ -2343,7 +2343,7 @@ dependencies = [ [[package]] name = "fabro-auth" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2368,7 +2368,7 @@ dependencies = [ [[package]] name = "fabro-automation" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2388,11 +2388,11 @@ dependencies = [ [[package]] name = "fabro-build-support" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" [[package]] name = "fabro-checkpoint" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "fabro-config", @@ -2408,7 +2408,7 @@ dependencies = [ [[package]] name = "fabro-cli" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "fabro-client" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "bytes", @@ -2539,7 +2539,7 @@ dependencies = [ [[package]] name = "fabro-config" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "fabro-core" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "fabro-types", @@ -2584,7 +2584,7 @@ dependencies = [ [[package]] name = "fabro-db" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2596,7 +2596,7 @@ dependencies = [ [[package]] name = "fabro-dev" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "fabro-dump" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "bytes", @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "fabro-environment" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2651,9 +2651,10 @@ dependencies = [ [[package]] name = "fabro-github" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", + "async-trait", "base64", "chrono", "fabro-http", @@ -2665,6 +2666,7 @@ dependencies = [ "jsonwebtoken", "serde", "serde_json", + "strum 0.28.0", "thiserror 2.0.18", "tokio", "tracing", @@ -2673,7 +2675,7 @@ dependencies = [ [[package]] name = "fabro-graphviz" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "fabro-types", @@ -2688,7 +2690,7 @@ dependencies = [ [[package]] name = "fabro-hooks" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "fabro-agent", @@ -2711,7 +2713,7 @@ dependencies = [ [[package]] name = "fabro-http" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-static", "http 1.4.0", @@ -2721,7 +2723,7 @@ dependencies = [ [[package]] name = "fabro-install" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "base64", @@ -2740,7 +2742,7 @@ dependencies = [ [[package]] name = "fabro-interview" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "dialoguer", @@ -2755,7 +2757,7 @@ dependencies = [ [[package]] name = "fabro-llm" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -2797,7 +2799,7 @@ dependencies = [ [[package]] name = "fabro-macros" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "clap", "fabro-options-metadata", @@ -2808,7 +2810,7 @@ dependencies = [ [[package]] name = "fabro-manifest" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "fabro-api", @@ -2829,7 +2831,7 @@ dependencies = [ [[package]] name = "fabro-mcp" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "axum", @@ -2849,7 +2851,7 @@ dependencies = [ [[package]] name = "fabro-mcp-server" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -2877,7 +2879,7 @@ dependencies = [ [[package]] name = "fabro-mcp-store" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "fabro-db", @@ -2895,7 +2897,7 @@ dependencies = [ [[package]] name = "fabro-model" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-static", "http 1.4.0", @@ -2911,7 +2913,7 @@ dependencies = [ [[package]] name = "fabro-oauth" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "axum", @@ -2933,7 +2935,7 @@ dependencies = [ [[package]] name = "fabro-options-metadata" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "serde", "serde_json", @@ -2941,7 +2943,7 @@ dependencies = [ [[package]] name = "fabro-proc" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "cc", "libc", @@ -2950,7 +2952,7 @@ dependencies = [ [[package]] name = "fabro-redact" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "aho-corasick", "ref-cast", @@ -2966,7 +2968,7 @@ dependencies = [ [[package]] name = "fabro-sandbox" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3010,7 +3012,7 @@ dependencies = [ [[package]] name = "fabro-server" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3105,7 +3107,7 @@ dependencies = [ [[package]] name = "fabro-slack" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-http", "fabro-interview", @@ -3127,18 +3129,18 @@ dependencies = [ [[package]] name = "fabro-spa" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "rust-embed", ] [[package]] name = "fabro-static" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" [[package]] name = "fabro-store" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "async-trait", "bytes", @@ -3168,7 +3170,7 @@ dependencies = [ [[package]] name = "fabro-telemetry" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "base64", @@ -3194,7 +3196,7 @@ dependencies = [ [[package]] name = "fabro-template" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "fabro-types", @@ -3208,7 +3210,7 @@ dependencies = [ [[package]] name = "fabro-test" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -3233,7 +3235,7 @@ dependencies = [ [[package]] name = "fabro-tool" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3254,7 +3256,7 @@ dependencies = [ [[package]] name = "fabro-tracker" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-trait", @@ -3268,7 +3270,7 @@ dependencies = [ [[package]] name = "fabro-types" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "chrono", "clap", @@ -3291,7 +3293,7 @@ dependencies = [ [[package]] name = "fabro-util" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "console 0.15.11", @@ -3314,7 +3316,7 @@ dependencies = [ [[package]] name = "fabro-validate" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-acp", "fabro-graphviz", @@ -3327,7 +3329,7 @@ dependencies = [ [[package]] name = "fabro-variable" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -3344,7 +3346,7 @@ dependencies = [ [[package]] name = "fabro-vault" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "chrono", @@ -3363,7 +3365,7 @@ dependencies = [ [[package]] name = "fabro-workflow" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "assert_cmd", @@ -3433,7 +3435,7 @@ dependencies = [ [[package]] name = "fabro-workflow-version" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "fabro-config", "fabro-graphviz", @@ -8544,7 +8546,7 @@ dependencies = [ [[package]] name = "twin-github" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "axum", "base64", @@ -8563,7 +8565,7 @@ dependencies = [ [[package]] name = "twin-openai" -version = "0.325.0-nightly.0" +version = "0.332.0-nightly.1" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index 0923bda8c..4a6437546 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.332.0-nightly.1" license = "MIT" [workspace.dependencies] diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx index 52196878b..71f30dd43 100644 --- a/apps/fabro-web/app/routes/runs.test.tsx +++ b/apps/fabro-web/app/routes/runs.test.tsx @@ -3,6 +3,7 @@ import type { BoardColumn, Run } from "@qltysh/fabro-api-client"; import { buildBoardColumns, + buildFilterOptions, loadStoredRunsWorkspaceSearchParams, placeArchivedColumnLast, persistRunsWorkspacePreferences, @@ -11,6 +12,7 @@ import { shouldRefreshBoardForEvent, } from "./runs"; import { summarizeBatchLifecycleAction } from "../components/runs-list/batch-lifecycle"; +import { mapRunListItem } from "../data/runs"; import { TEST_PRINCIPAL } from "../lib/test-fixtures"; function boardRun(id: string, column: BoardColumn, questionText?: string): Run { @@ -217,6 +219,38 @@ describe("runs route board mapping", () => { }); }); +describe("runs route filter options", () => { + function runWith(id: string, repoName: string, workflowName: string): Run { + const run = boardRun(id, "running"); + return { + ...run, + repository: { ...run.repository, name: repoName }, + workflow: { ...run.workflow, name: workflowName }, + }; + } + + test("derives sorted unique options from run items", () => { + const items = [ + runWith("a", "qlty/beta", "release"), + runWith("b", "qlty/alpha", "hello"), + runWith("c", "qlty/beta", "release"), + ].map(mapRunListItem); + + expect(buildFilterOptions(items, (item) => item.repo, "all")).toEqual(["alpha", "beta"]); + expect(buildFilterOptions(items, (item) => item.workflow, "all")).toEqual([ + "hello", + "release", + ]); + }); + + test("keeps the active selection when no loaded run matches it", () => { + const items = [runWith("a", "qlty/beta", "release")].map(mapRunListItem); + + expect(buildFilterOptions(items, (item) => item.repo, "gamma")).toEqual(["beta", "gamma"]); + expect(buildFilterOptions([], (item) => item.workflow, "release")).toEqual(["release"]); + }); +}); + describe("runs route workspace preferences", () => { class MemoryStorage { values = new Map(); diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index b1e09bf67..9aeceb78c 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -140,6 +140,18 @@ export function buildBoardColumns( }); } +export function buildFilterOptions( + items: RunItem[], + pick: (item: RunItem) => string, + selected: string, +): string[] { + const values = new Set(items.map(pick)); + // Keep the active selection visible even when no loaded run matches it, + // e.g. a stored repo filter while paginating the list view. + if (selected !== "all") values.add(selected); + return Array.from(values).sort(); +} + export function placeArchivedColumnLast(columns: Column[], includeArchived: boolean): Column[] { if (!includeArchived) return columns; const archived = columns.find((column) => column.id === "archived"); @@ -771,18 +783,18 @@ export default function Runs() { ); const hasGitHubAuth = authConfig.data?.methods.includes("github") === true; const serverUrl = systemInfo.data?.server_url; - const allRepos = Array.from( - new Set( - initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => String(item.repo))), - ), + // Filter options come from the loaded runs: all runs in columns view, the + // current page in list view (until a facets endpoint provides the full set). + const filterSourceItems: RunItem[] = + view === "list" + ? (listRunsPage.data?.data ?? []).map(mapRunListItem) + : initialColumns.flatMap((col: Column) => col.items); + const allRepos = buildFilterOptions(filterSourceItems, (item) => item.repo, repoFilter); + const allWorkflows = buildFilterOptions( + filterSourceItems, + (item) => item.workflow, + workflowFilter, ); - allRepos.sort(); - const allWorkflows = Array.from( - new Set( - initialColumns.flatMap((col: Column) => col.items.map((item: RunItem) => String(item.workflow))), - ), - ); - allWorkflows.sort(); const [columnsState, setColumnsState] = useState(() => ({ base: initialColumns, columns: initialColumns, 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/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/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..546b9d106 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: @@ -596,6 +596,15 @@ paths: application/json: schema: $ref: "#/components/schemas/DiagnosticsReport" + "504": + description: Diagnostics operation timed out + headers: + x-request-id: + $ref: "#/components/headers/XRequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" /api/v1/openapi.json: get: @@ -3092,7 +3101,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 +3146,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,14 +5983,13 @@ 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}$' + $ref: "#/components/schemas/BlobHash" example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ArtifactFilename: @@ -9152,9 +9160,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,16 +10293,22 @@ components: description: Assigned event sequence number. example: 42 + BlobHash: + 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-Fa-f]{64}$" + example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 + WriteBlobResponse: - description: Content-addressed identifier for a stored blob. + description: Content-addressed hash of a stored blob. type: object required: - - id + - hash properties: - id: - type: string - description: Blob identifier. - example: 550e8400-e29b-41d4-a716-446655440000 + hash: + $ref: "#/components/schemas/BlobHash" CommandTermination: description: Terminal state for a command execution. @@ -10401,7 +10417,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"] @@ -11434,6 +11450,8 @@ components: type: ["string", "null"] definition_blob: type: ["string", "null"] + spec_blob: + type: ["string", "null"] git: oneOf: - $ref: "#/components/schemas/GitContext" diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx index df78987d7..d471e18fd 100644 --- a/docs/public/core-concepts/models.mdx +++ b/docs/public/core-concepts/models.mdx @@ -59,13 +59,18 @@ Fabro performs this selection once when creating a run and persists the chosen p | `gemini-3.1-flash-lite` | gemini | `gemini-flash-lite`, `gemini-3.1-flash-lite-preview` | 1M | $0.25 / $1.50 | 200 tok/s | | `kimi-k2.5` | moonshot | | 262K | $0.60 / $3.00 | 50 tok/s | | `kimi-k3` | moonshot | `kimi` | 1M | $3.00 / $15.00 | n/a | +| `kimi-k3-fast` | venice | `kimi-fast` | 1M | $4.50 / $22.50 | n/a | | `deepseek-v4-flash` | deepseek | `deepseek`, `deepseek-v4`, `deepseek-flash` | 1,048,576 | $0.14 / $0.28 | n/a | | `deepseek-v4-pro` | deepseek | | 1,048,576 | $0.435 / $0.87 | n/a | +| `grok-4.6` | venice | `grok`, `grok46`, `grok-46` | 500K | $2.27 / $6.80 | n/a | | `laguna-s-2.1` | poolside | `laguna`, `laguna-s` | 1M | $0.10 / $0.20 | n/a | | `laguna-xs-2.1` | poolside | `laguna-xs` | 262K | $0.10 / $0.20 | n/a | | `glm-5.2` | zai | `glm`, `glm5`, `glm52`, `glm5.2` | 1M | $1.40 / $4.40 | n/a | +| `glm-5.3` | venice | `glm`, `glm5`, `glm53`, `glm5.3`, `glm-5-3` | 1M | $1.75 / $5.50 | n/a | | `minimax-m2.5` | minimax | `minimax` | 197K | $0.30 / $1.20 | 45 tok/s | | `mercury-2` | inception | `mercury` | 131K | $0.25 / $0.75 | 1000 tok/s | +| `qwen3.8-max` | venice | `qwen`, `qwen-max`, `qwen3.8`, `qwen-3.8`, `qwen38`, `qwen-3.8-max`, `qwen38-max` | 1M | $2.50 / $7.50 | n/a | +| `qwen3.8-27b` | venice | `qwen-27b`, `qwen-3.8-27b`, `qwen38-27b` | 262K | $0.45 / $3.20 | n/a | Each provider requires its own API key. Server-backed workflows read provider credentials from the server vault (for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `DEEPSEEK_API_KEY`, or `POOLSIDE_API_KEY` set with `fabro secret set` or `fabro provider login`). Standalone SDK/CLI flows can opt into env-backed credential sources explicitly. See the [Quick Start](/getting-started/quick-start) for setup. @@ -169,6 +174,10 @@ Provider `billing_policy` defaults from `adapter` and controls usage-cost estima Provider fields in configuration, APIs, and model routing are provider ID strings. Built-in names like `anthropic`, `openai`, and `gemini` still work, but custom IDs like `proxy` work anywhere a provider ID is accepted. +### Venice + +Fabro ships a built-in [Venice](/integrations/venice) provider with a curated catalog of Venice-hosted Kimi, Grok, GLM, DeepSeek, and Qwen models. Store its API key with `fabro provider login --provider venice`. Pin `provider = "venice"` when a shared model slug must use Venice instead of a higher-priority direct provider. + ### Poolside Fabro ships a built-in [Poolside](/integrations/poolside) provider for Laguna S 2.1 and Laguna XS 2.1 over Poolside's OpenAI-compatible API. Store a direct API key with `fabro provider login --provider poolside`. The same model slugs are also available through the opt-in OpenRouter provider; its vendor-namespaced strings remain provider-only `api_id` values. @@ -232,6 +241,7 @@ When no model or provider is specified, Fabro chooses the default offering on th | `moonshot` | `kimi-k3` | | `poolside` | `laguna-s-2.1` | | `zai` | `glm-5.2` | +| `venice` | `deepseek-v4-flash` | | `minimax` | `minimax-m2.5` | | `inception` | `mercury-2` | diff --git a/docs/public/docs.json b/docs/public/docs.json index faf0d0f84..284e5e4cb 100644 --- a/docs/public/docs.json +++ b/docs/public/docs.json @@ -97,6 +97,7 @@ "integrations/litellm", "integrations/bedrock", "integrations/deepseek", + "integrations/venice", "integrations/poolside", "integrations/openrouter", "integrations/modal", diff --git a/docs/public/examples/nlspec-conformance.mdx b/docs/public/examples/nlspec-conformance.mdx index cfeb30fec..6a1161769 100644 --- a/docs/public/examples/nlspec-conformance.mdx +++ b/docs/public/examples/nlspec-conformance.mdx @@ -123,7 +123,7 @@ Do not rewrite working code. Make targeted fixes to the specific failures. ### Max visits as a safety valve -`max_visits=5` on the `fix` node prevents infinite loops. If the agent can't pass in 5 iterations, the workflow moves on with the best result so far. Tune this based on spec complexity: a 30-line spec might need 2 iterations, a 2,000-line spec might need 10. +`max_visits=5` on the `fix` node prevents infinite loops. The node can execute up to 5 times; a sixth visit fails the run rather than looping forever. Tune this based on spec complexity: a 30-line spec might need 2 iterations, a 2,000-line spec might need 10. ### Goal gate on full conformance diff --git a/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/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/docs/public/integrations/venice.mdx b/docs/public/integrations/venice.mdx new file mode 100644 index 000000000..f172ccb34 --- /dev/null +++ b/docs/public/integrations/venice.mdx @@ -0,0 +1,125 @@ +--- +title: "Venice" +description: "Run Kimi, Grok, GLM, DeepSeek, and Qwen models through Venice" +--- + +[Venice](https://venice.ai/) provides an OpenAI-compatible API for hosted text models. Fabro enables the `venice` provider in its built-in catalog and maps stable Fabro model slugs to Venice's API model IDs. + +## Prerequisites + +- A Venice account +- An inference API key from [venice.ai/settings/api](https://venice.ai/settings/api) +- A running Fabro server + +## Configure credentials + +Store the API key in the target Fabro server vault: + +```bash +fabro provider login --provider venice + +# For a non-default remote server: +fabro provider login --server https://your-fabro.example --provider venice + +# Or set the vault token directly: +fabro secret set VENICE_API_KEY +fabro secret --server https://your-fabro.example set VENICE_API_KEY +``` + +Standalone SDK usage outside a Fabro server can use an env-backed credential source explicitly: + +```bash +export VENICE_API_KEY= +``` + +Fabro sends bearer-authenticated Chat Completions requests to `https://api.venice.ai/api/v1`. + +## Included models + +| Fabro model slug | Venice API ID | Context | Max output | Role and aliases | +|---|---|---:|---:|---| +| `kimi-k3` | `kimi-k3` | 1,000,000 | 131,072 | Alias `kimi` | +| `kimi-k3-fast` | `kimi-k3-fast-api` | 1,000,000 | 131,072 | Alias `kimi-fast` | +| `grok-4.6` | `grok-4-6` | 500,000 | 32,000 | Aliases `grok`, `grok46`, `grok-46` | +| `glm-5.3` | `z-ai-glm-5-3` | 1,000,000 | 131,072 | Aliases `glm`, `glm5`, `glm53`, `glm5.3`, `glm-5-3` | +| `deepseek-v4-flash` | `deepseek-v4-flash-0731` | 1,000,000 | 32,768 | Provider default; aliases `deepseek`, `deepseek-v4`, `deepseek-flash` | +| `deepseek-v4-pro` | `deepseek-v4-pro-0813` | 1,000,000 | 32,768 | Alias `deepseek-pro` | +| `qwen3.8-max` | `qwen-3-8-max` | 1,000,000 | 131,072 | Aliases `qwen`, `qwen-max`, `qwen3.8`, `qwen-3.8`, `qwen38`, `qwen-3.8-max`, `qwen38-max` | +| `qwen3.8-27b` | `qwen-3-8-27b` | 262,144 | 131,072 | Aliases `qwen-27b`, `qwen-3.8-27b`, `qwen38-27b` | + +Venice API IDs are also valid provider-scoped selectors. Fabro persists the stable Fabro slug and the selected provider when it creates a run. + +## Select Venice explicitly + +Some Venice models use the same stable slugs as direct providers. An unqualified selector chooses the highest-priority ready provider. For example, `deepseek` can select the direct DeepSeek provider when both API keys are configured. + +Pin Venice when the run must use Venice: + +```bash +fabro model list --provider venice +fabro model test --provider venice --model deepseek-v4-flash --deep +fabro run workflow.fabro --provider venice --model deepseek-v4-flash +``` + +In a workflow stylesheet: + +```dot title="workflow.fabro" +digraph Example { + graph [ + model_stylesheet=" + * { provider: venice; model: deepseek-v4-flash; } + .complex { provider: venice; model: qwen; } + .fast { provider: venice; model: kimi-fast; } + " + ] + + start [shape=Mdiamond, label="Start"] + work [label="Implement", class="complex"] + check [label="Check", class="fast"] + exit [shape=Msquare, label="Exit"] + + start -> work -> check -> exit +} +``` + +The generic Qwen aliases `qwen` and `qwen3.8` select Qwen 3.8 Max. Use a size-specific alias such as `qwen-27b` to select Qwen 3.8 27B. + +## Capabilities and reasoning + +All included models support tool calling and reasoning. Kimi K3, Kimi K3 Fast, Grok 4.6, Qwen 3.8 Max, and Qwen 3.8 27B also accept image input. + +Fabro exposes native reasoning-effort controls only when Venice supports them: + +| Model | Reasoning effort values | +|---|---| +| `grok-4.6` | `low`, `medium`, `high`, `xhigh` | +| `glm-5.3` | `low`, `high`, `max` | +| `deepseek-v4-flash` | `low`, `high`, `max` | +| `qwen3.8-27b` | `low`, `medium`, `xhigh` | + +The other models reason by default but do not expose a Venice reasoning-effort control. Fabro omits sampling parameters for Kimi and DeepSeek because those routes do not use them with their configured reasoning behavior. + +## Pricing and prompt caching + +The built-in catalog uses Venice's published prices per million tokens: + +| Model | Uncached input | Cache hit | Output | +|---|---:|---:|---:| +| `kimi-k3` | $3.75 | $0.375 | $18.75 | +| `kimi-k3-fast` | $4.50 | $0.45 | $22.50 | +| `grok-4.6` | $2.27 | $0.57 | $6.80 | +| `glm-5.3` | $1.75 | $0.325 | $5.50 | +| `deepseek-v4-flash` | $0.175 | $0.035 | $0.35 | +| `deepseek-v4-pro` | $1.65 | $0.165 | $4.95 | +| `qwen3.8-max` | $2.50 | $0.3125 | $7.50 | +| `qwen3.8-27b` | $0.45 | n/a | $3.20 | + +Fabro reports cached input separately when Venice returns cache usage for the selected model. Prices and model availability can change upstream; use `fabro model list --provider venice` to inspect the catalog shipped with your Fabro version and the [Venice model catalog](https://docs.venice.ai/models/overview) for the current upstream service. + +## Troubleshooting + +**"No credential was found for provider 'venice'"** — Store `VENICE_API_KEY` in the server vault with `fabro provider login --provider venice`. Pass `--server` when configuring a remote Fabro server. + +**A shared model used another provider** — Pin Venice with `--provider venice` or `provider: venice` in the workflow stylesheet. Unqualified selectors use provider priority. + +**A Venice API model ID is rejected without a provider** — Use the stable Fabro slug for portable selection, or qualify the API ID with the provider, such as `venice:qwen-3-8-max`. 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/attach.rs b/lib/apps/fabro-cli/src/commands/run/attach.rs index cd356ffa0..7c1cf93b7 100644 --- a/lib/apps/fabro-cli/src/commands/run/attach.rs +++ b/lib/apps/fabro-cli/src/commands/run/attach.rs @@ -849,6 +849,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/apps/fabro-cli/src/commands/run/output.rs b/lib/apps/fabro-cli/src/commands/run/output.rs index 0e5ba2d42..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,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) = parse_blob_ref(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,10 +341,6 @@ async fn resolve_response_string( })) } -fn blob_id_from_response(response: &str) -> Option { - parse_blob_ref(response) -} - async fn list_artifact_display_entries_with_client( client: &server_client::Client, run_id: &RunId, diff --git a/lib/apps/fabro-cli/src/commands/run/runner.rs b/lib/apps/fabro-cli/src/commands/run/runner.rs index 446888e7e..713838555 100644 --- a/lib/apps/fabro-cli/src/commands/run/runner.rs +++ b/lib/apps/fabro-cli/src/commands/run/runner.rs @@ -1018,12 +1018,12 @@ 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_id = *id; - async move { client.read_run_blob(&run_id, &blob_id).await } + let blob_hash = *blob_hash; + 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..9f388c14b 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_ID]".to_string()), - ); - } - if properties.contains_key("definition_blob") { - properties.insert( - "definition_blob".to_string(), - Value::String("[BLOB_ID]".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 @@ -896,7 +884,7 @@ fn attach_json_errors_without_prompting_for_human_input() { } } }, - "manifest_blob": "[BLOB_ID]", + "manifest_blob": "[BLOB_HASH]", "provenance": { "client": { "name": "fabro-cli", @@ -1024,6 +1012,7 @@ fn attach_json_errors_without_prompting_for_human_input() { } }, "source_directory": "[TEMP_DIR]", + "spec_blob": "[BLOB_HASH]", "title": "Wait for approval", "web_url": "http://localhost:3000/runs/[ULID]", "workflow_slug": "human-gate", @@ -1036,7 +1025,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-cli/tests/it/support/mod.rs b/lib/apps/fabro-cli/tests/it/support/mod.rs index 2f3557044..57e4a98e5 100644 --- a/lib/apps/fabro-cli/tests/it/support/mod.rs +++ b/lib/apps/fabro-cli/tests/it/support/mod.rs @@ -53,6 +53,7 @@ pub(crate) fn run_projection_json(run_id: &str, status: &serde_json::Value) -> s provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index 988f84404..9373cc81e 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -6,7 +6,7 @@ use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use fabro_auth::auth_issue_message; use fabro_llm::client::Client as LlmClient; -use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe}; +use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe_with_timeout}; use fabro_model::{Catalog, ProviderId}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; @@ -23,6 +23,9 @@ use tokio::time::timeout; use crate::server::AppState; +const EXTERNAL_SERVICE_PROBE_TIMEOUT: Duration = Duration::from_secs(15); +const DOCKER_PROBE_TIMEOUT: Duration = Duration::from_secs(5); + fn http_client_or_check( name: &str, status: CheckStatus, @@ -252,13 +255,20 @@ async fn probe_single_provider( None, ); }; - let model_id = model.id.clone(); + let model_id = model.id.to_string(); + + let outcome = run_basic_model_probe_with_timeout( + &model_id, + &provider, + client, + EXTERNAL_SERVICE_PROBE_TIMEOUT, + ) + .await; - let outcome = run_basic_model_probe(model_id.as_str(), &provider, client).await; match outcome.status { ModelTestStatus::Ok => ProviderProbeResult { provider, - model_id: Some(model_id.to_string()), + model_id: Some(model_id), status: ProviderProbeStatus::Ok, error_message: None, diagnostic_detail: None, @@ -267,12 +277,7 @@ async fn probe_single_provider( let raw = outcome .error_message .unwrap_or_else(|| "provider probe failed".to_string()); - provider_probe_error( - provider, - Some(model_id.to_string()), - redact_string(&raw), - None, - ) + provider_probe_error(provider, Some(model_id), redact_string(&raw), None) } } } @@ -388,7 +393,7 @@ async fn check_github_app(state: &AppState) -> CheckResult { Err(result) => return result, }; let probe = timeout( - Duration::from_secs(15), + EXTERNAL_SERVICE_PROBE_TIMEOUT, http.get(format!("{}/user", fabro_github::github_api_base_url())) .header("Authorization", format!("Bearer {token}")) .header("Accept", "application/vnd.github+json") @@ -538,7 +543,7 @@ async fn check_github_app(state: &AppState) -> CheckResult { Err(result) => return result, }; let auth_result = timeout( - Duration::from_secs(15), + EXTERNAL_SERVICE_PROBE_TIMEOUT, fabro_github::get_authenticated_app(&http, &jwt, &fabro_github::github_api_base_url()), ) .await; @@ -581,7 +586,7 @@ async fn check_docker_sandbox(state: &AppState) -> CheckResult { .await .map_err(|err| err.display_with_causes()) }, - Duration::from_secs(5), + DOCKER_PROBE_TIMEOUT, ) .await } @@ -656,7 +661,14 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { }; }; - match state.check_daytona_api_key(api_key).await { + let probe = state + .check_daytona_api_key_with_timeout(api_key, EXTERNAL_SERVICE_PROBE_TIMEOUT) + .await; + cloud_sandbox_probe_check(probe) +} + +fn cloud_sandbox_probe_check(probe: anyhow::Result) -> CheckResult { + match probe { Ok(check) if check.ok() => CheckResult { name: "Cloud Sandbox".to_string(), status: CheckStatus::Pass, @@ -678,13 +690,29 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { daytona::required_perms_display() )), }, - Err(err) => CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: "Daytona credential rejected".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], - remediation: Some("Verify DAYTONA_API_KEY value and Daytona reachability".to_string()), - }, + Err(err) => { + if let Some(timeout) = err.downcast_ref::() { + return CheckResult { + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: format!("timeout ({:?})", timeout.timeout()), + details: vec![CheckDetail::new("Daytona probe timed out".to_string())], + remediation: Some( + "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), + ), + }; + } + + CheckResult { + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: "Daytona credential rejected".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], + remediation: Some( + "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), + ), + } + } } } @@ -750,7 +778,7 @@ async fn check_brave_search(state: &AppState) -> CheckResult { Err(result) => return result, }; - let probe = timeout(Duration::from_secs(15), async move { + let probe = timeout(EXTERNAL_SERVICE_PROBE_TIMEOUT, async move { http.get("https://api.search.brave.com/res/v1/web/search?q=test&count=1") .header("X-Subscription-Token", api_key) .send() @@ -1154,6 +1182,18 @@ enabled = false ); } + #[test] + fn check_cloud_sandbox_reports_timeout() { + let result = cloud_sandbox_probe_check(Err(anyhow::Error::new( + daytona::DaytonaCredentialProbeTimeout::new(Duration::from_millis(1)), + ))); + + assert_eq!(result.name, "Cloud Sandbox"); + assert_eq!(result.status, CheckStatus::Error); + assert_eq!(result.summary, "timeout (1ms)"); + assert_eq!(result.details[0].text, "Daytona probe timed out"); + } + #[tokio::test] async fn check_brave_search_ignores_env_backed_api_key() { let state = TestAppStateBuilder::new() diff --git a/lib/apps/fabro-server/src/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/run_files.rs b/lib/apps/fabro-server/src/run_files.rs index 920460f79..2343933b6 100644 --- a/lib/apps/fabro-server/src/run_files.rs +++ b/lib/apps/fabro-server/src/run_files.rs @@ -2387,6 +2387,7 @@ index 1111111..2222222 160000 provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index fa8af18c8..8c7d5514d 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -1455,6 +1455,15 @@ impl AppState { pub(crate) async fn check_daytona_api_key( &self, api_key: String, + ) -> anyhow::Result { + self.check_daytona_api_key_with_timeout(api_key, daytona::DAYTONA_CREDENTIAL_PROBE_TIMEOUT) + .await + } + + pub(crate) async fn check_daytona_api_key_with_timeout( + &self, + api_key: String, + probe_timeout: Duration, ) -> anyhow::Result { let base_url = self .config_env_lookup(EnvVars::DAYTONA_API_URL) @@ -1463,8 +1472,14 @@ impl AppState { let org_id = self.config_env_lookup(EnvVars::DAYTONA_ORGANIZATION_ID); let http_client = fabro_http::http_client().context("failed to build HTTP client")?; - daytona::check_daytona_api_key_with(&base_url, org_id.as_deref(), api_key, http_client) - .await + daytona::check_daytona_api_key_with_timeout( + &base_url, + org_id.as_deref(), + api_key, + http_client, + probe_timeout, + ) + .await } /// Borrow the persistent store so sibling modules can open run readers @@ -2889,11 +2904,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..f0f2b6064 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,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_id) => Json(WriteBlobResponse { - id: blob_id.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() } @@ -118,11 +115,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/events.rs b/lib/apps/fabro-server/src/server/handler/events.rs index 1bfb7f889..5fd655f1d 100644 --- a/lib/apps/fabro-server/src/server/handler/events.rs +++ b/lib/apps/fabro-server/src/server/handler/events.rs @@ -627,6 +627,7 @@ mod stage_events_tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/apps/fabro-server/src/server/handler/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/handler/pair.rs b/lib/apps/fabro-server/src/server/handler/pair.rs index ec31d2e54..6e244bcac 100644 --- a/lib/apps/fabro-server/src/server/handler/pair.rs +++ b/lib/apps/fabro-server/src/server/handler/pair.rs @@ -1027,6 +1027,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index c2ee94b32..e9bacdfff 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -1923,6 +1923,7 @@ reasoning = false provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/apps/fabro-server/src/server/handler/system.rs b/lib/apps/fabro-server/src/server/handler/system.rs index d26e445a6..d74f65944 100644 --- a/lib/apps/fabro-server/src/server/handler/system.rs +++ b/lib/apps/fabro-server/src/server/handler/system.rs @@ -1,5 +1,7 @@ use std::collections::BTreeMap; +use std::future::Future; use std::sync::Arc; +use std::time::Duration; use chrono::Utc; use fabro_slack::config::{ @@ -9,6 +11,7 @@ use fabro_slack::config::{ use fabro_static::EnvVars; use fabro_types::settings::server::GithubIntegrationSettings; use fabro_vault::Vault; +use tokio::time::timeout; use super::super::{ AggregateBilling, AggregateBillingTotals, ApiError, AppState, BilledTokenCounts, @@ -21,6 +24,8 @@ use super::super::{ resource_sampler, spawn_blocking, system_sandbox_provider, to_i64, }; +const SERVER_DIAGNOSTICS_TIMEOUT: Duration = Duration::from_secs(25); + pub(super) fn routes() -> Router> { Router::new() .route("/repos/github/{owner}/{name}", get(get_github_repo)) @@ -683,11 +688,34 @@ async fn get_github_repo( } async fn run_diagnostics(_auth: RequiredUser, State(state): State>) -> Response { - ( - StatusCode::OK, - Json(diagnostics::run_all(state.as_ref()).await), + diagnostics_response_with_timeout( + Box::pin(diagnostics::run_all(state.as_ref())), + SERVER_DIAGNOSTICS_TIMEOUT, ) - .into_response() + .await +} + +async fn diagnostics_response_with_timeout( + diagnostics: F, + operation_timeout: Duration, +) -> Response +where + F: Future, +{ + let Ok(report) = timeout(operation_timeout, diagnostics).await else { + tracing::warn!( + timeout_secs = operation_timeout.as_secs(), + "server diagnostics timed out" + ); + return ApiError::with_code( + StatusCode::GATEWAY_TIMEOUT, + "Server diagnostics timed out.", + "diagnostics_timeout", + ) + .into_response(); + }; + + (StatusCode::OK, Json(report)).into_response() } pub(in crate::server) async fn openapi_spec() -> Response { @@ -737,3 +765,26 @@ async fn get_aggregate_billing( }; (StatusCode::OK, Json(response)).into_response() } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn diagnostics_response_returns_gateway_timeout_before_client_deadline() { + let response = diagnostics_response_with_timeout( + std::future::pending::(), + Duration::from_millis(1), + ) + .await; + + let body = fabro_test::expect_axum_json( + response, + StatusCode::GATEWAY_TIMEOUT, + "GET /api/v1/system/diagnostics timeout", + ) + .await; + assert_eq!(body["errors"][0]["code"], "diagnostics_timeout"); + assert_eq!(body["errors"][0]["detail"], "Server diagnostics timed out."); + } +} diff --git a/lib/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/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 2ffec61de..dc656c32e 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -10,7 +10,7 @@ use std::sync::{Arc as StdArc, Mutex as StdMutex}; use async_zip::base::read::mem::ZipFileReader; use axum::body::Body; use axum::http::{Method, Request, header}; -use chrono::{Duration as ChronoDuration, Utc}; +use chrono::{Duration as ChronoDuration, SubsecRound as _, Utc}; use fabro_automation::{AutomationId, AutomationTarget}; use fabro_config::bind::Bind; use fabro_config::{ @@ -4069,7 +4069,9 @@ async fn create_run_from_manifest_resolves_generated_id_after_variable_snapshot( let body = response_json!(response, StatusCode::CREATED).await; let run_id = body["id"].as_str().unwrap().parse::().unwrap(); - assert!(run_id.created_at() >= variable.updated_at); + // RunId is a ULID whose timestamp only has millisecond precision, so + // truncate the variable timestamp to milliseconds before comparing. + assert!(run_id.created_at() >= variable.updated_at.trunc_subsecs(3)); } #[tokio::test] @@ -4650,6 +4652,7 @@ async fn append_default_run_created(run_store: &fabro_store::RunDatabase, run_id automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -4701,6 +4704,7 @@ async fn create_slack_notification_run( automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -5774,6 +5778,7 @@ async fn list_run_stages_distinguishes_visits() { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -5910,6 +5915,7 @@ async fn list_run_stages_exposes_execution_identity_for_resumed_stage() { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -7097,6 +7103,7 @@ async fn create_completed_run_ready_for_pull_request( provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; @@ -7113,6 +7120,7 @@ async fn create_completed_run_ready_for_pull_request( automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git, fork_source_ref: None, retried_from: None, @@ -11034,7 +11042,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)); @@ -11057,11 +11065,14 @@ 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.to_uppercase() + ))) .body(Body::empty()) .unwrap(); let response = app.oneshot(req).await.unwrap(); @@ -11459,7 +11470,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 +11564,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 +12069,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 +12132,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(), )) @@ -14082,6 +14093,7 @@ async fn create_preserved_local_sandbox_run(state: &Arc, run_id: RunId automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -14831,6 +14843,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/apps/fabro-server/tests/it/api/run_files.rs b/lib/apps/fabro-server/tests/it/api/run_files.rs index 6d286cbab..307c4a574 100644 --- a/lib/apps/fabro-server/tests/it/api/run_files.rs +++ b/lib/apps/fabro-server/tests/it/api/run_files.rs @@ -68,6 +68,7 @@ async fn append_completed_run_with_final_patch( automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-agent/src/lib.rs b/lib/components/fabro-agent/src/lib.rs index c9d7f2e3d..e9549a716 100644 --- a/lib/components/fabro-agent/src/lib.rs +++ b/lib/components/fabro-agent/src/lib.rs @@ -59,8 +59,9 @@ pub use question_tools::{ }; pub use sandbox::{ CommandOutputCallback, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, - GrepOptions, RefreshOutcome, Sandbox, SandboxEvent, SandboxEventCallback, StderrCollector, - StdioProcess, StdioProcessHandle, format_lines_numbered, shell_quote, + GrepOptions, RefreshOutcome, RemoteCredentialAction, Sandbox, SandboxEvent, + SandboxEventCallback, StderrCollector, StdioProcess, StdioProcessHandle, TokenProvenance, + TokenSnapshot, format_lines_numbered, shell_quote, }; pub use session::{ CompletionCoordinator, Session, SessionControlHandle, SessionInputTiming, diff --git a/lib/components/fabro-agent/src/sandbox.rs b/lib/components/fabro-agent/src/sandbox.rs index 47fbf894c..2f1ade3a4 100644 --- a/lib/components/fabro-agent/src/sandbox.rs +++ b/lib/components/fabro-agent/src/sandbox.rs @@ -3,7 +3,8 @@ // `crate::delegate_sandbox!` invocations continue to work. pub use fabro_sandbox::{ CommandOutputCallback, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, - GrepOptions, RefreshOutcome, Sandbox, SandboxEvent, SandboxEventCallback, SandboxFile, - StderrCollector, StdioProcess, StdioProcessHandle, StdioProcessTermination, WalkOptions, - delegate_sandbox, format_lines_numbered, shell_quote, + GrepOptions, RefreshOutcome, RemoteCredentialAction, Sandbox, SandboxEvent, + SandboxEventCallback, SandboxFile, StderrCollector, StdioProcess, StdioProcessHandle, + StdioProcessTermination, TokenProvenance, TokenSnapshot, WalkOptions, delegate_sandbox, + format_lines_numbered, shell_quote, }; diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index f892c971c..aad8e4e3d 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -226,7 +226,7 @@ pub fn make_edit_file_tool() -> RegisteredTool { }; ctx.env - .write_file(file_path, &new_content) + .write_existing_file(file_path, &new_content) .await .map_err(|e| e.display_with_causes())?; Ok(format!("Successfully edited {file_path}")) @@ -1002,6 +1002,7 @@ mod tests { ) .await; assert_eq!(result.unwrap(), "Successfully wrote to /out.txt"); + assert_eq!(env.existing_file_write_count(), 0); let written = env.written_files.lock().unwrap(); assert_eq!(written.len(), 1); assert_eq!(written[0].0, "/out.txt"); @@ -1036,6 +1037,7 @@ mod tests { ) .await; assert_eq!(result.unwrap(), "Successfully edited /f.txt"); + assert_eq!(env.existing_file_write_count(), 1); let written = env.written_files.lock().unwrap(); assert_eq!(written.len(), 1); assert_eq!(written[0].1, "goodbye world"); diff --git a/lib/components/fabro-dump/src/lib.rs b/lib/components/fabro-dump/src/lib.rs index 1028408e0..43cf278eb 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" @@ -214,31 +215,42 @@ 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) - .await? - .with_context(|| format!("blob {blob_id:?} is missing from the store"))?; - *text = serde_json::from_slice::(&blob).with_context(|| { - format!("blob {blob_id:?} 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(_) => {} } @@ -386,21 +398,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 +425,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) => { @@ -475,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; @@ -484,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() } } @@ -724,8 +728,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 +740,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))) }) }) @@ -752,4 +756,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"); + } } diff --git a/lib/components/fabro-github/Cargo.toml b/lib/components/fabro-github/Cargo.toml index 9db73130e..8f755dd79 100644 --- a/lib/components/fabro-github/Cargo.toml +++ b/lib/components/fabro-github/Cargo.toml @@ -9,12 +9,17 @@ description = "GitHub App authentication and API helpers for Fabro" [lib] doctest = false +[features] +test-support = [] + [lints] workspace = true [dependencies] anyhow.workspace = true +async-trait.workspace = true serde.workspace = true +strum.workspace = true serde_json.workspace = true fabro-http.workspace = true fabro-redact.workspace = true diff --git a/lib/components/fabro-github/src/lib.rs b/lib/components/fabro-github/src/lib.rs index fd4b98936..a38c6c8ce 100644 --- a/lib/components/fabro-github/src/lib.rs +++ b/lib/components/fabro-github/src/lib.rs @@ -9,6 +9,11 @@ use fabro_types::settings::run::MergeStrategy; use serde::Deserialize; use tokio::process::Command; +pub mod token_source; + +#[cfg(any(test, feature = "test-support"))] +pub mod test_support; + pub const GITHUB_API_BASE_URL: &str = "https://api.github.com"; /// Returns the GitHub API base URL, allowing override via `GITHUB_BASE_URL` env diff --git a/lib/components/fabro-github/src/test_support.rs b/lib/components/fabro-github/src/test_support.rs new file mode 100644 index 000000000..09cca93ce --- /dev/null +++ b/lib/components/fabro-github/src/test_support.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use crate::InstallationToken; +use crate::token_source::{InstallationTokenMinter as InnerMinter, InstallationTokenSource}; + +#[async_trait::async_trait] +pub trait InstallationTokenMinter: Send + Sync { + async fn mint(&self) -> anyhow::Result; +} + +struct TestMinterAdapter(Arc); + +#[async_trait::async_trait] +impl InnerMinter for TestMinterAdapter { + async fn mint(&self) -> anyhow::Result { + self.0.mint().await + } +} + +#[must_use] +pub fn installation_token_source( + repo: impl Into, + minter: Arc, +) -> Arc { + InstallationTokenSource::with_minter(repo.into(), Box::new(TestMinterAdapter(minter))) +} diff --git a/lib/components/fabro-github/src/token_source.rs b/lib/components/fabro-github/src/token_source.rs new file mode 100644 index 000000000..276c5f9b8 --- /dev/null +++ b/lib/components/fabro-github/src/token_source.rs @@ -0,0 +1,685 @@ +//! Cached GitHub installation-token source. +//! +//! One [`InstallationTokenSource`] can serve GitHub-token consumers that share +//! a repository and permission scope. Reusing mature tokens keeps consumers +//! out of GitHub's token-replication lag window, where a token minted +//! milliseconds earlier is rejected with 404 "Repository not found" or an +//! authentication failure. +//! +//! The source also reports *provenance*: when it minted the token it returned, +//! and which mint generation it belongs to. Retry classification, logging, and +//! failure reports all read that one fact instead of threading booleans +//! through call stacks. + +use std::fmt; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context as _; +use chrono::{DateTime, Utc}; +use tokio::sync::Mutex; + +use crate::{GitHubAppCredentials, GitHubCredentials, InstallationToken}; + +/// How long before expiry a cached installation token stops being reused. +/// +/// Must comfortably exceed the longest git operation that pins a resolved +/// token, so a token handed out just above the margin still outlives the +/// operation. GitHub App installation tokens live 60 minutes. +pub const REFRESH_MARGIN: Duration = Duration::from_mins(10); + +/// Where the token a resolve returned came from. +/// +/// Time metadata exists only for tokens this source minted. Static +/// credentials (a PAT, or a pre-minted installation token) carry no +/// `minted_at`, so token age is undefined for them and they are never +/// treated as freshly minted. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, strum::Display, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum TokenProvenance { + /// This resolve minted the token. + Minted { + minted_at: DateTime, + expires_at: DateTime, + }, + /// This resolve returned a token minted by an earlier resolve. + Reused { + minted_at: DateTime, + expires_at: DateTime, + }, + /// A fixed credential the source cannot re-mint. + Static, +} + +/// Non-secret description of the token a resolve returned. Shared by the +/// source, refresh outcomes, logs, and events. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct TokenSnapshot { + /// Increments per mint; 0 for `Static`. + pub generation: u64, + pub provenance: TokenProvenance, +} + +impl TokenSnapshot { + #[must_use] + pub fn minted_at(&self) -> Option> { + match self.provenance { + TokenProvenance::Minted { minted_at, .. } + | TokenProvenance::Reused { minted_at, .. } => Some(minted_at), + TokenProvenance::Static => None, + } + } + + #[must_use] + pub fn expires_at(&self) -> Option> { + match self.provenance { + TokenProvenance::Minted { expires_at, .. } + | TokenProvenance::Reused { expires_at, .. } => Some(expires_at), + TokenProvenance::Static => None, + } + } + + /// Age of the token at `now`. `None` for static credentials, whose age is + /// undefined. + #[must_use] + pub fn age_at(&self, now: DateTime) -> Option { + let minted_at = self.minted_at()?; + Some((now - minted_at).to_std().unwrap_or(Duration::ZERO)) + } + + /// Age of the token in milliseconds, measured now. + #[must_use] + pub fn age_ms(&self) -> Option { + self.age_at(Utc::now()) + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)) + } + + #[must_use] + pub fn is_static(&self) -> bool { + matches!(self.provenance, TokenProvenance::Static) + } +} + +/// A token secret that never appears in `Debug` output. Call +/// [`SecretString::expose`] at the point of use (URL embedding, git +/// credentials) — never in a log line. +#[derive(Clone)] +pub struct SecretString(String); + +impl SecretString { + #[must_use] + pub fn new(secret: String) -> Self { + Self(secret) + } + + #[must_use] + pub fn expose(&self) -> &str { + &self.0 + } +} + +impl fmt::Debug for SecretString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SecretString()") + } +} + +/// A token handed out by [`InstallationTokenSource::resolve`]: the secret plus +/// its non-secret snapshot. Only the snapshot may cross logging or event +/// boundaries. +#[derive(Debug, Clone)] +pub struct ResolvedToken { + pub token: SecretString, + pub snapshot: TokenSnapshot, + /// The source tried to refresh an expiring token, but returned the still- + /// valid cached token after the mint failed. + pub refresh_failed: bool, +} + +/// Mints installation tokens for [`InstallationTokenSource`]. Abstracted so +/// tests can script mint results without HTTP. +#[async_trait::async_trait] +pub(crate) trait InstallationTokenMinter: Send + Sync { + async fn mint(&self) -> anyhow::Result; +} + +/// Real minter backed by GitHub App credentials. +struct AppTokenMinter { + creds: GitHubAppCredentials, + http: fabro_http::HttpClient, + owner: String, + repo: String, + base_url: String, + permissions: serde_json::Value, +} + +#[async_trait::async_trait] +impl InstallationTokenMinter for AppTokenMinter { + async fn mint(&self) -> anyhow::Result { + self.creds + .mint_installation_token( + &self.http, + &self.owner, + &self.repo, + &self.base_url, + self.permissions.clone(), + None, + ) + .await + } +} + +/// A minted token plus the metadata the cache tracks for it. +struct CachedToken { + token: InstallationToken, + minted_at: DateTime, + generation: u64, +} + +impl CachedToken { + fn resolved(&self, provenance: TokenProvenance) -> ResolvedToken { + ResolvedToken { + token: SecretString::new(self.token.token.clone()), + snapshot: TokenSnapshot { + generation: self.generation, + provenance, + }, + refresh_failed: false, + } + } +} + +enum SourceState { + /// A fixed personal access token — no expiry metadata. + Pat(SecretString), + /// A pre-minted installation token — fixed, rejected client-side once + /// expired. + Installation(InstallationToken), + /// GitHub App credentials that mint installation tokens on demand. + /// + /// The async lock is held across the mint, making `resolve()` + /// single-flight: concurrent near-expiry callers wait and receive the + /// same generation instead of racing to mint. + App { + minter: Box, + cache: Mutex>, + }, +} + +/// Cached installation-token source for one origin repository. +/// +/// Static credentials pass through unchanged. App credentials mint through +/// the shared cache: a resolve reuses the cached token until it is within +/// [`REFRESH_MARGIN`] of expiry, then mints a new generation. +pub struct InstallationTokenSource { + /// `owner/repo`, for logs only. + repo: String, + state: SourceState, +} + +impl InstallationTokenSource { + /// Build a source for `creds` against the repository in `origin_url`. + /// + /// `permissions` scopes minted installation tokens; static credentials + /// pass through and ignore it. + pub fn for_origin( + creds: &GitHubCredentials, + origin_url: &str, + permissions: serde_json::Value, + ) -> anyhow::Result> { + let normalized = crate::normalize_repo_origin_url(origin_url); + let (owner, repo) = crate::parse_github_owner_repo(&normalized) + .context("parsing GitHub origin for token source")?; + Self::for_repository(creds, owner, repo, permissions) + } + + /// Build a source for an already parsed GitHub repository. + pub fn for_repository( + creds: &GitHubCredentials, + owner: String, + repo: String, + permissions: serde_json::Value, + ) -> anyhow::Result> { + let repo_display = format!("{owner}/{repo}"); + let state = match creds { + GitHubCredentials::Pat(token) => SourceState::Pat(SecretString::new(token.clone())), + GitHubCredentials::Installation(token) => SourceState::Installation(token.clone()), + GitHubCredentials::App(app) => { + let http = fabro_http::http_client() + .map_err(anyhow::Error::new) + .context("building HTTP client for token source")?; + SourceState::App { + minter: Box::new(AppTokenMinter { + creds: app.clone(), + http, + owner, + repo, + base_url: crate::github_api_base_url(), + permissions, + }), + cache: Mutex::new(None), + } + } + }; + Ok(Arc::new(Self { + repo: repo_display, + state, + })) + } + + /// Build a source for a personal access token. + #[must_use] + pub fn pat(token: String) -> Arc { + Arc::new(Self { + repo: String::new(), + state: SourceState::Pat(SecretString::new(token)), + }) + } + + /// Build a source for a pre-minted installation token. + #[must_use] + pub fn installation(token: InstallationToken) -> Arc { + Arc::new(Self { + repo: String::new(), + state: SourceState::Installation(token), + }) + } + + /// Build a minting source over a custom minter. + #[cfg(any(test, feature = "test-support"))] + #[must_use] + pub(crate) fn with_minter(repo: String, minter: Box) -> Arc { + Arc::new(Self { + repo, + state: SourceState::App { + minter, + cache: Mutex::new(None), + }, + }) + } + + /// Whether this source can mint new tokens (GitHub App credentials). + #[must_use] + pub fn mints_installation_tokens(&self) -> bool { + matches!(self.state, SourceState::App { .. }) + } + + /// Resolve a token, reusing the cached one until it nears expiry. + pub async fn resolve(&self) -> anyhow::Result { + match &self.state { + SourceState::Pat(_) | SourceState::Installation(_) => self.resolve_static(), + SourceState::App { minter, cache } => { + let mut cache = cache.lock().await; + // Re-check under the lock: a waiter queued behind a minter + // finds the fresh token here instead of minting again. + if let Some(cached) = cache.as_ref() { + if !cached.token.near_expiry(REFRESH_MARGIN) { + let resolved = cached.resolved(TokenProvenance::Reused { + minted_at: cached.minted_at, + expires_at: cached.token.expires_at, + }); + tracing::debug!( + repo = %self.repo, + generation = cached.generation, + expires_at = %cached.token.expires_at, + "Reusing cached GitHub installation token" + ); + return Ok(resolved); + } + } + match self.mint_locked(minter.as_ref(), &mut cache).await { + Ok(resolved) => Ok(resolved), + Err(err) => { + if let Some(cached) = cache.as_ref() { + if cached.token.valid_token().is_ok() { + tracing::warn!( + error = %format!("{err:#}"), + repo = %self.repo, + generation = cached.generation, + expires_at = %cached.token.expires_at, + "GitHub installation token refresh failed; using cached token" + ); + let mut resolved = cached.resolved(TokenProvenance::Reused { + minted_at: cached.minted_at, + expires_at: cached.token.expires_at, + }); + resolved.refresh_failed = true; + return Ok(resolved); + } + } + Err(err) + } + } + } + } + } + + /// Mint a fresh token for the first repository clone and seed the cache + /// with it. + /// + /// The clone deliberately never reuses a warm cache: retrying a clone with + /// the token minted for it is the established replication-lag recovery, + /// and reuse of older tokens for clones is a separate follow-up. Seeding + /// makes the clone token generation 1, so later refreshes reuse it until + /// it nears expiry. + pub async fn mint_for_clone(&self) -> anyhow::Result { + match &self.state { + SourceState::Pat(_) | SourceState::Installation(_) => self.resolve_static(), + SourceState::App { minter, cache } => { + let mut cache = cache.lock().await; + self.mint_locked(minter.as_ref(), &mut cache).await + } + } + } + + fn resolve_static(&self) -> anyhow::Result { + let secret = match &self.state { + SourceState::Pat(token) => token.clone(), + SourceState::Installation(token) => SecretString::new(token.valid_token()?.to_owned()), + SourceState::App { .. } => unreachable!("resolve_static called for App credentials"), + }; + Ok(ResolvedToken { + token: secret, + snapshot: TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }, + refresh_failed: false, + }) + } + + async fn mint_locked( + &self, + minter: &dyn InstallationTokenMinter, + cache: &mut Option, + ) -> anyhow::Result { + let token = minter + .mint() + .await + .context("minting GitHub installation access token")?; + let generation = cache.as_ref().map_or(0, |cached| cached.generation) + 1; + let minted_at = Utc::now(); + tracing::info!( + repo = %self.repo, + generation, + expires_at = %token.expires_at, + "Minted GitHub installation token" + ); + let cached = CachedToken { + token, + minted_at, + generation, + }; + let resolved = cached.resolved(TokenProvenance::Minted { + minted_at, + expires_at: cached.token.expires_at, + }); + *cache = Some(cached); + Ok(resolved) + } +} + +#[cfg(test)] +mod tests { + use std::collections::VecDeque; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use anyhow::anyhow; + + use super::*; + + enum MintAction { + Token(&'static str, DateTime), + Error(&'static str), + } + + struct MockMinter { + calls: AtomicUsize, + script: Mutex>, + } + + impl MockMinter { + fn new(script: Vec) -> Self { + Self { + calls: AtomicUsize::new(0), + script: Mutex::new(script.into()), + } + } + + fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } + } + + #[async_trait::async_trait] + impl InstallationTokenMinter for MockMinter { + async fn mint(&self) -> anyhow::Result { + self.calls.fetch_add(1, Ordering::SeqCst); + match self.script.lock().await.pop_front().expect("mint script") { + MintAction::Token(token, expires_at) => Ok(InstallationToken { + token: token.to_string(), + expires_at, + }), + MintAction::Error(message) => Err(anyhow!(message)), + } + } + } + + struct SharedMinter(Arc); + + #[async_trait::async_trait] + impl InstallationTokenMinter for SharedMinter { + async fn mint(&self) -> anyhow::Result { + self.0.mint().await + } + } + + fn mintable(script: Vec) -> (Arc, Arc) { + let minter = Arc::new(MockMinter::new(script)); + let source = InstallationTokenSource::with_minter( + "owner/repo".to_string(), + Box::new(SharedMinter(Arc::clone(&minter))), + ); + (source, minter) + } + + #[tokio::test] + async fn pat_resolves_as_static_generation_zero() { + let source = InstallationTokenSource::for_origin( + &GitHubCredentials::Pat("ghp_pat".to_string()), + "https://github.com/owner/repo.git", + serde_json::json!({ "contents": "write" }), + ) + .unwrap(); + + let resolved = source.resolve().await.unwrap(); + assert_eq!(resolved.token.expose(), "ghp_pat"); + assert_eq!(resolved.snapshot.generation, 0); + assert!(resolved.snapshot.is_static()); + assert!(!source.mints_installation_tokens()); + } + + #[tokio::test] + async fn static_installation_token_resolves_until_expiry() { + let valid = InstallationTokenSource::for_origin( + &GitHubCredentials::Installation(InstallationToken { + token: "ghs_static".to_string(), + expires_at: Utc::now() + chrono::Duration::minutes(30), + }), + "https://github.com/owner/repo.git", + serde_json::json!({}), + ) + .unwrap(); + let resolved = valid.resolve().await.unwrap(); + assert_eq!(resolved.token.expose(), "ghs_static"); + assert!(resolved.snapshot.is_static()); + + let expired = InstallationTokenSource::for_origin( + &GitHubCredentials::Installation(InstallationToken { + token: "ghs_expired".to_string(), + expires_at: Utc::now() - chrono::Duration::seconds(1), + }), + "https://github.com/owner/repo.git", + serde_json::json!({}), + ) + .unwrap(); + assert!(expired.resolve().await.is_err()); + } + + #[tokio::test] + async fn resolve_reuses_cached_token_before_the_margin() { + let (source, minter) = mintable(vec![MintAction::Token( + "ghs_gen1", + Utc::now() + chrono::Duration::minutes(30), + )]); + + let first = source.resolve().await.unwrap(); + let second = source.resolve().await.unwrap(); + + assert_eq!(minter.calls(), 1); + assert_eq!(first.snapshot.generation, 1); + assert_eq!(second.snapshot.generation, 1); + assert!(matches!( + first.snapshot.provenance, + TokenProvenance::Minted { .. } + )); + assert!(matches!( + second.snapshot.provenance, + TokenProvenance::Reused { .. } + )); + assert_eq!(second.token.expose(), "ghs_gen1"); + } + + #[tokio::test] + async fn resolve_mints_a_new_generation_inside_the_margin() { + let (source, minter) = mintable(vec![ + // Expires inside REFRESH_MARGIN, so the second resolve re-mints. + MintAction::Token("ghs_gen1", Utc::now() + chrono::Duration::minutes(5)), + MintAction::Token("ghs_gen2", Utc::now() + chrono::Duration::minutes(60)), + ]); + + let first = source.resolve().await.unwrap(); + let second = source.resolve().await.unwrap(); + + assert_eq!(minter.calls(), 2); + assert_eq!(first.snapshot.generation, 1); + assert_eq!(second.snapshot.generation, 2); + assert!(matches!( + second.snapshot.provenance, + TokenProvenance::Minted { .. } + )); + assert_eq!(second.token.expose(), "ghs_gen2"); + } + + #[tokio::test] + async fn resolve_uses_a_valid_cached_token_when_refresh_fails() { + let (source, minter) = mintable(vec![ + MintAction::Token("ghs_gen1", Utc::now() + chrono::Duration::minutes(5)), + MintAction::Error("mint failed"), + ]); + + let first = source.resolve().await.unwrap(); + let second = source.resolve().await.unwrap(); + + assert_eq!(minter.calls(), 2); + assert_eq!(first.snapshot.generation, 1); + assert_eq!(second.snapshot.generation, 1); + assert!(second.refresh_failed); + assert!(matches!( + second.snapshot.provenance, + TokenProvenance::Reused { .. } + )); + assert_eq!(second.token.expose(), "ghs_gen1"); + } + + #[tokio::test] + async fn concurrent_resolves_share_one_generation() { + // Single mint in the script: a second mint would panic on an empty + // script, so success proves single-flight. + let (source, minter) = mintable(vec![MintAction::Token( + "ghs_gen1", + Utc::now() + chrono::Duration::minutes(60), + )]); + + let handles: Vec<_> = (0..8) + .map(|_| { + let source = Arc::clone(&source); + tokio::spawn(async move { source.resolve().await }) + }) + .collect(); + + for handle in handles { + let resolved = handle.await.unwrap().unwrap(); + assert_eq!(resolved.snapshot.generation, 1); + assert_eq!(resolved.token.expose(), "ghs_gen1"); + } + assert_eq!(minter.calls(), 1); + } + + #[tokio::test] + async fn mint_for_clone_always_mints_and_seeds_the_cache() { + let (source, minter) = mintable(vec![MintAction::Token( + "ghs_clone", + Utc::now() + chrono::Duration::minutes(60), + )]); + + let clone_token = source.mint_for_clone().await.unwrap(); + assert_eq!(clone_token.snapshot.generation, 1); + assert!(matches!( + clone_token.snapshot.provenance, + TokenProvenance::Minted { .. } + )); + + // A later resolve reuses the clone token instead of minting again. + let refreshed = source.resolve().await.unwrap(); + assert_eq!(refreshed.snapshot.generation, 1); + assert_eq!(refreshed.token.expose(), "ghs_clone"); + assert!(matches!( + refreshed.snapshot.provenance, + TokenProvenance::Reused { .. } + )); + assert_eq!(minter.calls(), 1); + } + + #[tokio::test] + async fn mint_failure_surfaces_with_context() { + let (source, _minter) = mintable(vec![MintAction::Error("mint failed")]); + + let err = format!("{:#}", source.resolve().await.unwrap_err()); + assert!(err.contains("mint failed"), "got: {err}"); + assert!( + err.contains("minting GitHub installation access token"), + "got: {err}" + ); + } + + #[test] + fn secret_string_debug_never_prints_the_secret() { + let secret = SecretString::new("ghs_super_secret".to_string()); + let rendered = format!("{secret:?}"); + assert!(!rendered.contains("ghs_super_secret"), "{rendered}"); + } + + #[test] + fn snapshot_age_is_defined_only_for_minted_tokens() { + let now = Utc::now(); + let minted = TokenSnapshot { + generation: 3, + provenance: TokenProvenance::Minted { + minted_at: now - chrono::Duration::seconds(42), + expires_at: now + chrono::Duration::minutes(60), + }, + }; + assert_eq!(minted.age_at(now), Some(Duration::from_secs(42))); + + let fixed = TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }; + assert_eq!(fixed.age_at(now), None); + assert_eq!(fixed.expires_at(), None); + } +} diff --git a/lib/components/fabro-llm/src/adapter_registry.rs b/lib/components/fabro-llm/src/adapter_registry.rs index a306c846b..60ad8ec42 100644 --- a/lib/components/fabro-llm/src/adapter_registry.rs +++ b/lib/components/fabro-llm/src/adapter_registry.rs @@ -269,91 +269,19 @@ mod tests { .unwrap_or_else(|error| panic!("built-in model '{selector}' should resolve: {error}")) } - /// One row of the route-equivalence table: model id plus the - /// `(deployment_id, transport, codec, billing_policy, agent_profile)` - /// tuple it must resolve to. - type RouteRow = ( - &'static str, - &'static str, - AdapterKind, - CodecKind, - BillingPolicy, - AgentProfileKind, - ); - - /// The compat mapping as an executable table: every built-in catalog - /// model resolves to exactly this tuple. Adding or rerouting a built-in - /// model means updating this table deliberately. #[test] - fn builtin_catalog_route_equivalence_table() { - use AdapterKind as T; - use AgentProfileKind as P; - use BillingPolicy as B; - use CodecKind as C; - - #[rustfmt::skip] - let expected: &[RouteRow] = &[ - // model id deployment_id transport codec billing profile - ("claude-fable-5", "claude-fable-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Claude5), - ("claude-haiku-4-5", "claude-haiku-4-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-4-6", "claude-opus-4-6", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-4-7", "claude-opus-4-7", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-4-8", "claude-opus-4-8", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-opus-5", "claude-opus-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Claude5), - ("claude-sonnet-4-5", "claude-sonnet-4-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-sonnet-4-6", "claude-sonnet-4-6", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Anthropic), - ("claude-sonnet-5", "claude-sonnet-5", T::Anthropic, C::AnthropicMessages, B::Anthropic, P::Claude5), - ("deepseek-v4-flash", "deepseek-v4-flash", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("deepseek-v4-pro", "deepseek-v4-pro", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("gemini-3-flash-preview", "gemini-3-flash-preview", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.1-flash-lite", "gemini-3.1-flash-lite", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.1-pro-preview", "gemini-3.1-pro-preview", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.1-pro-preview-customtools", "gemini-3.1-pro-preview-customtools", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("gemini-3.5-flash", "gemini-3.5-flash", T::Gemini, C::GeminiGenerate, B::Gemini, P::Gemini), - ("glm-4.7", "glm-4.7", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("glm-5.2", "glm-5.2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("gpt-5.4", "gpt-5.4", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.4-mini", "gpt-5.4-mini", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.4-pro", "gpt-5.4-pro", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.5", "gpt-5.5", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.5-pro", "gpt-5.5-pro", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::OpenAi), - ("gpt-5.6-luna", "gpt-5.6-luna", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::Gpt56), - ("gpt-5.6-sol", "gpt-5.6-sol", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::Gpt56), - ("gpt-5.6-terra", "gpt-5.6-terra", T::OpenAi, C::OpenAiResponses, B::OpenAi, P::Gpt56), - ("kimi-k2.5", "kimi-k2.5", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::Kimi), - ("kimi-k3", "kimi-k3", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::Kimi), - ("laguna-s-2.1", "poolside/laguna-s-2.1", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("laguna-xs-2.1", "poolside/laguna-xs-2.1", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("mercury-2", "mercury-2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("minimax-m2.5", "minimax-m2.5", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("venice-uncensored-1-2", "venice-uncensored-1-2", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ("venice-uncensored-role-play", "venice-uncensored-role-play", T::OpenAiCompatible, C::OpenAiCompatible, B::OpenAi, P::OpenAi), - ]; - + fn every_builtin_catalog_offering_resolves() { let catalog = Catalog::builtin(); - let mut model_ids: Vec<&str> = catalog - .list(None) - .iter() - .map(|model| model.id.as_str()) - .collect(); - model_ids.sort_unstable(); - let mut expected_ids: Vec<&str> = expected.iter().map(|row| row.0).collect(); - expected_ids.sort_unstable(); - assert_eq!( - model_ids, expected_ids, - "route-equivalence table must cover every built-in model row" - ); - - for (model_id, deployment_id, transport, codec, billing_policy, agent_profile) in expected { - let model = select_from_all(catalog, model_id); - let route = resolve_route(catalog, model) - .unwrap_or_else(|| panic!("built-in model '{model_id}' should resolve")); - assert_eq!(route.deployment_id, *deployment_id, "{model_id}"); - assert_eq!(route.transport, *transport, "{model_id}"); - assert_eq!(route.codec, *codec, "{model_id}"); - assert_eq!(route.billing_policy, *billing_policy, "{model_id}"); - assert_eq!(route.agent_profile, *agent_profile, "{model_id}"); + for model in catalog.list(None) { + let route = resolve_route(catalog, model).unwrap_or_else(|| { + panic!( + "built-in offering '{}/{}' should resolve", + model.provider, model.id + ) + }); + assert_eq!(route.provider, model.provider); + assert!(!route.deployment_id.is_empty()); } } diff --git a/lib/components/fabro-llm/src/client.rs b/lib/components/fabro-llm/src/client.rs index fc2802be1..59cfa421d 100644 --- a/lib/components/fabro-llm/src/client.rs +++ b/lib/components/fabro-llm/src/client.rs @@ -2037,17 +2037,20 @@ reasoning = false client } - /// Live-dispatch counterpart of the adapter_registry route-equivalence - /// table: for every built-in model, `resolve_provider` lands on the same - /// provider the resolved route names. + /// For every built-in model selector, live dispatch and catalog selection + /// choose the same provider from the same ready-provider set. #[tokio::test] async fn dispatch_agrees_with_resolve_route_for_every_builtin_model() { let catalog = catalog_with(""); let client = client_with_all_catalog_providers(&catalog).await; + let ready_providers = catalog.all_provider_ids(); for model in catalog.list(None) { - let route = adapter_registry::resolve_route(&catalog, model) - .expect("built-in model should resolve to a route"); + let selected = catalog + .select(model.id.as_str(), None, &ready_providers) + .expect("built-in model should be selectable"); + let route = adapter_registry::resolve_route(&catalog, selected) + .expect("selected built-in model should resolve to a route"); let mut request = test_request(); request.model = model.id.to_string(); diff --git a/lib/components/fabro-llm/src/error.rs b/lib/components/fabro-llm/src/error.rs index 6ec6d7886..dd9a98347 100644 --- a/lib/components/fabro-llm/src/error.rs +++ b/lib/components/fabro-llm/src/error.rs @@ -355,7 +355,14 @@ 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 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 => { return Error::RequestTimeout { @@ -728,6 +735,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 [ diff --git a/lib/components/fabro-llm/src/model_test.rs b/lib/components/fabro-llm/src/model_test.rs index 6a985fb1b..de09f6fde 100644 --- a/lib/components/fabro-llm/src/model_test.rs +++ b/lib/components/fabro-llm/src/model_test.rs @@ -1,3 +1,4 @@ +use std::future::Future; use std::sync::Arc; use std::time::Duration; @@ -63,22 +64,38 @@ pub async fn run_basic_model_probe( model_id: &str, provider: impl ToString, client: Arc, +) -> ModelTestOutcome { + run_basic_model_probe_with_timeout( + model_id, + provider, + client, + Duration::from_secs(ModelTestMode::Basic.timeout_secs()), + ) + .await +} + +pub async fn run_basic_model_probe_with_timeout( + model_id: &str, + provider: impl ToString, + client: Arc, + probe_timeout: Duration, ) -> ModelTestOutcome { let params = GenerateParams::new(model_id, client) .provider(provider.to_string()) .prompt("Say OK") .max_tokens(16); - let result = time::timeout( - Duration::from_secs(ModelTestMode::Basic.timeout_secs()), - generate::generate(params), - ) - .await; + basic_model_probe_outcome(generate::generate(params), probe_timeout).await +} - match result { +async fn basic_model_probe_outcome(probe: F, probe_timeout: Duration) -> ModelTestOutcome +where + F: Future>, +{ + match time::timeout(probe_timeout, probe).await { Ok(Ok(_)) => ModelTestOutcome::ok(), Ok(Err(err)) => ModelTestOutcome::error(err.to_string()), - Err(_) => ModelTestOutcome::error("timeout (30s)"), + Err(_) => ModelTestOutcome::error(format!("timeout ({probe_timeout:?})")), } } @@ -244,6 +261,18 @@ mod tests { ); } + #[tokio::test] + async fn basic_model_probe_reports_configured_timeout() { + let outcome = basic_model_probe_outcome( + std::future::pending::>(), + Duration::from_millis(1), + ) + .await; + + assert_eq!(outcome.status, ModelTestStatus::Error); + assert_eq!(outcome.error_message.as_deref(), Some("timeout (1ms)")); + } + #[test] fn deep_test_omits_effort_for_reasoning_without_effort_controls() { let info = test_model_with(ModelFeatures { diff --git a/lib/components/fabro-sandbox/Cargo.toml b/lib/components/fabro-sandbox/Cargo.toml index 908d010e6..251e6bd77 100644 --- a/lib/components/fabro-sandbox/Cargo.toml +++ b/lib/components/fabro-sandbox/Cargo.toml @@ -9,8 +9,8 @@ description = "Sandbox trait and implementations for Fabro agent execution envir [features] default = ["local"] local = [] -docker = ["dep:bollard", "dep:tar", "dep:fabro-github"] -daytona = ["dep:daytona-sdk", "dep:daytona-api-client", "dep:git2", "dep:fabro-github", "dep:fabro-config", "dep:fabro-http", "dep:reqwest-middleware", "dep:rand", "dep:tokio-tungstenite", "dep:futures-util", "dep:rustls"] +docker = ["dep:bollard", "dep:tar"] +daytona = ["dep:daytona-sdk", "dep:daytona-api-client", "dep:git2", "dep:fabro-config", "dep:fabro-http", "dep:reqwest-middleware", "dep:rand", "dep:tokio-tungstenite", "dep:futures-util", "dep:rustls"] test-support = [] [lib] @@ -47,7 +47,7 @@ tar = { workspace = true, optional = true } # daytona fabro-config = { path = "../../foundation/fabro-config", optional = true } -fabro-github = { path = "../fabro-github", optional = true } +fabro-github = { path = "../fabro-github" } fabro-types = { path = "../../foundation/fabro-types" } chrono = { workspace = true } @@ -64,6 +64,7 @@ futures-util = { workspace = true, optional = true } rustls = { version = "0.23", default-features = false, features = ["std", "ring"], optional = true } [dev-dependencies] +fabro-github = { path = "../fabro-github", features = ["test-support"] } tokio = { workspace = true, features = ["test-util", "macros"] } tempfile = "3" serde_json.workspace = true diff --git a/lib/components/fabro-sandbox/src/clone_retry.rs b/lib/components/fabro-sandbox/src/clone_retry.rs deleted file mode 100644 index 75dbb7fec..000000000 --- a/lib/components/fabro-sandbox/src/clone_retry.rs +++ /dev/null @@ -1,407 +0,0 @@ -//! Retry for the first repository clone in a clone-based sandbox. -//! -//! Clone-based providers can mint a GitHub App installation token and clone -//! with it immediately. GitHub can reject that first clone before the token is -//! available to the git endpoint. On a private repository, the rejection can -//! arrive as `Repository not found.` or an authentication failure. -//! -//! Only a token minted during the current clone operation makes those messages -//! safe to retry. Static PATs and pre-minted installation tokens fail fast. -//! -//! Retries reuse the same token on purpose. Replication of a given token only -//! makes progress, so each attempt strictly improves the odds, while re-minting -//! would restart the replication clock. - -use std::future::Future; -use std::time::Duration; - -use fabro_types::SandboxProviderKind; -use fabro_util::backoff::BackoffPolicy; -use tokio::time; - -/// Total clone attempts, including the first. -const MAX_ATTEMPTS: u32 = 3; - -/// Why a failed clone attempt is worth repeating. -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)] -#[strum(serialize_all = "snake_case")] -pub(crate) enum CloneRetryReason { - /// A freshly minted installation token has not reached the GitHub edge - /// cache site serving this clone yet. - TokenReplication, - /// The clone failed on infrastructure, unrelated to credentials. - TransientInfra, -} - -/// What a clone failure message tells us about retry safety. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum CloneMessageClass { - Retry(CloneRetryReason), - Permanent, - Unknown, -} - -impl CloneMessageClass { - pub(crate) fn retry_reason(self) -> Option { - match self { - Self::Retry(reason) => Some(reason), - Self::Permanent | Self::Unknown => None, - } - } -} - -/// A failed clone or fetch attempt: the terminal error plus whether the -/// failure is worth retrying. -pub(crate) struct CloneAttemptFailure { - pub(crate) error: crate::Error, - pub(crate) retry_reason: Option, -} - -/// Message fragments that mean the clone failed on infrastructure. -/// -/// These are safe to retry whether or not the clone was authenticated. -const TRANSIENT_HINTS: &[&str] = &[ - "could not resolve host", - "temporary failure in name resolution", - "connection refused", - "connection reset", - "connection timed out", - "timed out", - "network is unreachable", - "no route to host", - "tls handshake", - "early eof", - "rpc failed", - "unexpected disconnect", - "the remote end hung up unexpectedly", - "index-pack failed", - "service unavailable", - "gateway timeout", - "too many requests", - "rate limit", -]; - -/// Message fragments GitHub uses when a token is not yet visible. -/// -/// Only meaningful when the clone carried credentials. The same lag surfaces as -/// 404 or as an auth failure depending on which endpoint answers first. -const TOKEN_REPLICATION_HINTS: &[&str] = &[ - "repository not found", - "authentication failed", - "invalid username or password", - "bad credentials", -]; - -/// Classify a failed clone by its rendered message. -/// -/// `token_was_freshly_minted` gates the token-replication reading. A static -/// credential cannot become valid during backoff, so auth failures for it are -/// permanent. -pub(crate) fn classify_message(message: &str, token_was_freshly_minted: bool) -> CloneMessageClass { - let lower = message.to_ascii_lowercase(); - - if TRANSIENT_HINTS.iter().any(|hint| lower.contains(hint)) { - return CloneMessageClass::Retry(CloneRetryReason::TransientInfra); - } - if TOKEN_REPLICATION_HINTS - .iter() - .any(|hint| lower.contains(hint)) - { - return if token_was_freshly_minted { - CloneMessageClass::Retry(CloneRetryReason::TokenReplication) - } else { - CloneMessageClass::Permanent - }; - } - let permanent = lower.contains("could not read username") - || lower.contains("terminal prompts disabled") - || lower.contains("permission denied") - || (lower.contains("permission to") && lower.contains("denied")) - || (lower.contains("destination path") && lower.contains("already exists")) - || (lower.contains("remote branch") && lower.contains("not found")); - if permanent { - return CloneMessageClass::Permanent; - } - CloneMessageClass::Unknown -} - -/// Backoff between clone attempts: 3s, then 9s. -/// -/// GitHub's guidance for token replication is to wait a few seconds and retry -/// with the same token. Sub-second delays land inside the same replication -/// window and spend an attempt for nothing. -fn backoff() -> BackoffPolicy { - BackoffPolicy { - initial_delay: Duration::from_secs(3), - factor: 3.0, - max_delay: Duration::from_secs(10), - jitter: false, - } -} - -/// Run a clone, repeating it while the failure looks transient. -/// -/// `attempt` receives the 1-based attempt number. `classify` decides whether an -/// error is worth repeating; `None` returns it to the caller untouched. When a -/// deadline is present, a retry starts only when its backoff fits before that -/// deadline. The final error is returned as-is. -pub(crate) async fn retry_clone( - provider: SandboxProviderKind, - deadline: Option, - mut attempt: Attempt, - classify: Classify, -) -> Result -where - Attempt: FnMut(u32) -> Fut, - Fut: Future>, - Classify: Fn(&E) -> Option, -{ - let backoff = backoff(); - - for attempt_number in 1..MAX_ATTEMPTS { - match attempt(attempt_number).await { - Ok(value) => return Ok(value), - Err(err) => { - let Some(reason) = classify(&err) else { - return Err(err); - }; - let delay = backoff.delay_for_attempt(attempt_number); - if deadline.is_some_and(|deadline| { - delay >= deadline.saturating_duration_since(time::Instant::now()) - }) { - return Err(err); - } - // The failure text can carry git stderr, so log the category - // rather than the message. The caller still reports the full - // error if the attempts run out. - tracing::warn!( - provider = %provider, - attempt = attempt_number, - max_attempts = MAX_ATTEMPTS, - reason = %reason, - delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), - "Git clone failed, retrying" - ); - time::sleep(delay).await; - } - } - } - - attempt(MAX_ATTEMPTS).await -} - -#[cfg(test)] -mod tests { - use std::sync::Mutex; - - use super::*; - - /// Records the attempt numbers a closure was called with. - #[derive(Default)] - struct Attempts(Mutex>); - - impl Attempts { - fn record(&self, attempt: u32) { - self.0.lock().expect("attempt log mutex").push(attempt); - } - - fn recorded(&self) -> Vec { - self.0.lock().expect("attempt log mutex").clone() - } - } - - /// A classifier that treats every failure as worth repeating. - const ALWAYS_RETRY: fn(&String) -> Option = - |_| Some(CloneRetryReason::TokenReplication); - - #[test] - fn private_repo_not_found_after_a_successful_mint_is_a_replication_lag() { - assert_eq!( - classify_message("repository not found: Repository not found.", true), - CloneMessageClass::Retry(CloneRetryReason::TokenReplication) - ); - } - - #[test] - fn not_found_without_a_fresh_token_is_permanent() { - assert_eq!( - classify_message("repository not found: Repository not found.", false), - CloneMessageClass::Permanent - ); - } - - #[test] - fn auth_failure_with_a_fresh_token_is_a_replication_lag() { - assert_eq!( - classify_message( - "fatal: Authentication failed for 'https://github.com/owner/repo'", - true - ), - CloneMessageClass::Retry(CloneRetryReason::TokenReplication) - ); - assert_eq!( - classify_message( - "fatal: Authentication failed for 'https://github.com/owner/repo'", - false - ), - CloneMessageClass::Permanent - ); - } - - #[test] - fn infra_failures_retry_without_credentials() { - for message in [ - "fatal: unable to access: Could not resolve host: github.com", - "error: RPC failed; curl 56 recv failure", - "fatal: early EOF", - "Operation timed out", - ] { - assert_eq!( - classify_message(message, false), - CloneMessageClass::Retry(CloneRetryReason::TransientInfra), - "expected {message:?} to be transient" - ); - } - } - - #[test] - fn genuine_failures_are_not_retried() { - for message in [ - "fatal: could not read Username for 'https://github.com'", - "remote: Permission to owner/repo.git denied", - "fatal: destination path 'repo' already exists", - ] { - assert_eq!( - classify_message(message, true), - CloneMessageClass::Permanent, - "expected {message:?} to fail fast" - ); - } - } - - #[test] - fn unrecognized_failures_remain_unknown() { - assert_eq!( - classify_message("git clone stopped for an unexpected reason", true), - CloneMessageClass::Unknown - ); - } - - #[test] - fn backoff_waits_seconds_not_milliseconds() { - let backoff = backoff(); - assert_eq!(backoff.delay_for_attempt(1), Duration::from_secs(3)); - assert_eq!(backoff.delay_for_attempt(2), Duration::from_secs(9)); - } - - #[tokio::test(start_paused = true)] - async fn first_success_runs_one_attempt() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { Ok::<_, String>(attempt) } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!(result, Ok(1)); - assert_eq!(attempts.recorded(), vec![1]); - } - - #[tokio::test(start_paused = true)] - async fn retries_until_a_later_attempt_succeeds() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { - if attempt < 3 { - Err("Repository not found.".to_string()) - } else { - Ok(attempt) - } - } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!(result, Ok(3)); - assert_eq!(attempts.recorded(), vec![1, 2, 3]); - } - - #[tokio::test(start_paused = true)] - async fn exhausted_attempts_return_the_final_error() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { Err::<(), _>(format!("Repository not found. (attempt {attempt})")) } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!( - result, - Err("Repository not found. (attempt 3)".to_string()), - "the caller should see the last failure, not the first" - ); - assert_eq!(attempts.recorded(), vec![1, 2, 3]); - } - - #[tokio::test(start_paused = true)] - async fn unretryable_failure_stops_immediately() { - let attempts = Attempts::default(); - - let result = retry_clone( - SandboxProviderKind::Docker, - None, - |attempt| { - attempts.record(attempt); - async move { Err::<(), _>("permission denied".to_string()) } - }, - |_: &String| None, - ) - .await; - - assert_eq!(result, Err("permission denied".to_string())); - assert_eq!( - attempts.recorded(), - vec![1], - "a deterministic failure should not wait out the backoff" - ); - } - - #[tokio::test(start_paused = true)] - async fn deadline_stops_retry_when_backoff_does_not_fit() { - let attempts = Attempts::default(); - let deadline = time::Instant::now() + Duration::from_secs(2); - - let result = retry_clone( - SandboxProviderKind::Docker, - Some(deadline), - |attempt| { - attempts.record(attempt); - async move { Err::<(), _>("temporary failure".to_string()) } - }, - ALWAYS_RETRY, - ) - .await; - - assert_eq!(result, Err("temporary failure".to_string())); - assert_eq!(attempts.recorded(), vec![1]); - assert_eq!(time::Instant::now() + Duration::from_secs(2), deadline); - } -} diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 8b14ee462..a932ae826 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fmt::Write; +use std::future::Future; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -15,6 +16,7 @@ use daytona_sdk::api_types::SignedPortPreviewUrl; use daytona_sdk::toolbox_types::Command as SessionCommandResult; use daytona_sdk::{DaytonaError, GitCloneOptions, SessionCommandLogsResult}; use fabro_github::GitHubCredentials; +use fabro_github::token_source::InstallationTokenSource; use fabro_static::EnvVars; use fabro_types::{CommandOutputStream, CommandTermination, RunId, SandboxProviderKind}; use fabro_util::time::elapsed_ms; @@ -25,8 +27,9 @@ use tokio::task::JoinHandle; use tokio::{fs, time}; use tokio_util::sync::CancellationToken; -use crate::clone_retry::{self, CloneRetryReason}; use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason}; +use crate::git_retry::{self, CredentialContext, GitRetryReason}; +use crate::push_credentials::{self, PushCredentialState}; use crate::redact::redact_auth_url; use crate::sandbox::{ self, BASH_ENV_VAR, BASH_PROBE_MARKER, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, REMOTE_BASH, @@ -62,8 +65,8 @@ pub const DEFAULT_DAYTONA_API_URL: &str = "https://app.daytona.io/api"; pub(crate) const DAYTONA_DASHBOARD_SANDBOXES_URL: &str = "https://app.daytona.io/dashboard/sandboxes"; const FABRO_SANDBOX_USER_AGENT: &str = concat!("fabro-sandbox/", env!("CARGO_PKG_VERSION")); -const DAYTONA_PROBE_TIMEOUT: Duration = Duration::from_secs(20); -const DAYTONA_START_TIMEOUT: Duration = Duration::from_mins(1); +pub const DAYTONA_CREDENTIAL_PROBE_TIMEOUT: Duration = Duration::from_secs(20); +const DAYTONA_BASH_SESSION_PROBE_TIMEOUT: Duration = Duration::from_secs(20); /// Shared budget for required setup after Daytona's native clone returns. /// /// The native clone has its own provider lifecycle. Starting this deadline @@ -85,6 +88,18 @@ const DAYTONA_MIN_SERVER_TIMEOUT: Duration = Duration::from_secs(1); /// deletion, temporary stdin files) so a stalled REST call cannot block /// cancellation/timeout paths indefinitely. const DAYTONA_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +/// Budget for waiting out an in-flight Daytona lifecycle transition (for +/// example an auto-stop racing an activation) before giving up. Transitions +/// normally finish within seconds; the budget only bounds a wedged sandbox. +const DAYTONA_STATE_CHANGE_TIMEOUT: Duration = Duration::from_mins(2); +/// Poll interval while waiting out an in-flight Daytona lifecycle transition. +const DAYTONA_STATE_CHANGE_POLL_INTERVAL: Duration = Duration::from_secs(1); +/// 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; fn daytona_git_clone_options( branch: Option, @@ -191,6 +206,24 @@ pub struct DaytonaKeyCheck { pub missing: Vec, } +#[derive(Debug, thiserror::Error)] +#[error("Daytona credential probe timed out after {timeout:?}")] +pub struct DaytonaCredentialProbeTimeout { + timeout: Duration, +} + +impl DaytonaCredentialProbeTimeout { + #[must_use] + pub const fn new(timeout: Duration) -> Self { + Self { timeout } + } + + #[must_use] + pub const fn timeout(&self) -> Duration { + self.timeout + } +} + impl DaytonaKeyCheck { pub fn ok(&self) -> bool { self.missing.is_empty() @@ -288,6 +321,23 @@ pub async fn check_daytona_api_key_with( org_id: Option<&str>, api_key: String, http_client: fabro_http::HttpClient, +) -> anyhow::Result { + check_daytona_api_key_with_timeout( + base_url, + org_id, + api_key, + http_client, + DAYTONA_CREDENTIAL_PROBE_TIMEOUT, + ) + .await +} + +pub async fn check_daytona_api_key_with_timeout( + base_url: &str, + org_id: Option<&str>, + api_key: String, + http_client: fabro_http::HttpClient, + probe_timeout: Duration, ) -> anyhow::Result { let work = async { let client = build_daytona_client_with( @@ -322,12 +372,21 @@ pub async fn check_daytona_api_key_with( }) }; - match time::timeout(DAYTONA_PROBE_TIMEOUT, work).await { + daytona_credential_probe_with_timeout(work, probe_timeout).await +} + +async fn daytona_credential_probe_with_timeout( + probe: F, + probe_timeout: Duration, +) -> anyhow::Result +where + F: Future>, +{ + match time::timeout(probe_timeout, probe).await { Ok(result) => result, - Err(_) => Err(anyhow::anyhow!( - "Daytona credential probe timed out after {}s", - DAYTONA_PROBE_TIMEOUT.as_secs() - )), + Err(_) => Err(anyhow::Error::new(DaytonaCredentialProbeTimeout::new( + probe_timeout, + ))), } } @@ -364,12 +423,39 @@ fn command_kind(command: &str) -> &'static str { } } +#[derive(Clone, Copy, strum::Display)] +#[strum(serialize_all = "lowercase")] +enum DaytonaLifecycleAction { + Start, + Stop, +} + +impl DaytonaLifecycleAction { + async fn execute( + self, + client: &daytona_sdk::Client, + sandbox_name: &str, + ) -> Result<(), DaytonaError> { + match self { + Self::Start => client.start(sandbox_name).await.map(drop), + Self::Stop => client.stop(sandbox_name).await.map(drop), + } + } + + fn is_complete(self, state: Option) -> bool { + match self { + Self::Start => state == Some(SandboxState::Started), + Self::Stop => matches!(state, Some(SandboxState::Stopped | SandboxState::Destroyed)), + } + } +} + /// Sandbox that runs all operations inside a Daytona cloud sandbox. pub struct DaytonaSandbox { config: DaytonaConfig, client: daytona_sdk::Client, api_key: Option, - github_app: Option, + push_credentials: PushCredentialState, sandbox: OnceCell, snapshot_name: OnceCell, rg_available: OnceCell, @@ -413,11 +499,15 @@ impl DaytonaSandbox { let client = build_daytona_client(api_key.clone()) .await .map_err(|e| crate::Error::context("Failed to create Daytona client", e))?; + let push_credentials = PushCredentialState::new(push_credentials::build_token_source( + github_app.as_ref(), + clone_origin_url.as_deref(), + )?); Ok(Self { config, client, api_key, - github_app, + push_credentials, sandbox: OnceCell::new(), snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), @@ -470,7 +560,7 @@ impl DaytonaSandbox { config: DaytonaConfig::default(), client, api_key, - github_app: None, + push_credentials: PushCredentialState::new(None), sandbox: sandbox_cell, snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), @@ -677,6 +767,19 @@ impl DaytonaSandbox { resolve_path(path, self.working_directory()) } + async fn upload_file_content(&self, resolved_path: &str, content: &str) -> crate::Result<()> { + let sandbox = self.sandbox()?; + let fs_svc = sandbox + .fs() + .await + .map_err(|e| crate::Error::context("Failed to get fs service", e))?; + + fs_svc + .upload_file_bytes(resolved_path, content.as_bytes()) + .await + .map_err(|e| crate::Error::context(format!("Failed to write file {resolved_path}"), e)) + } + /// Verify a Daytona sandbox evaluates commands as non-login Bash. /// /// Runs on a freshly created sandbox before any Fabro-owned setup, and @@ -742,16 +845,16 @@ impl DaytonaSandbox { /// non-POSIX, and completion assertions all hold. /// /// Costs one session round trip plus a single status poll per sandbox - /// lifecycle transition. `DAYTONA_PROBE_TIMEOUT` is the outer backstop for - /// a stalled REST call; the inner [`BASH_PROBE_TIMEOUT_MS`] is the deadline - /// for the command itself. Session cleanup runs outside that deadline under - /// its own bounded timeout. + /// lifecycle transition. `DAYTONA_BASH_SESSION_PROBE_TIMEOUT` is the outer + /// backstop for a stalled REST call; the inner [`BASH_PROBE_TIMEOUT_MS`] is + /// the deadline for the command itself. Session cleanup runs outside that + /// deadline under its own bounded timeout. async fn probe_bash_session(sandbox: &daytona_sdk::Sandbox) -> crate::Result<()> { - let deadline = time::Instant::now() + DAYTONA_PROBE_TIMEOUT; + let deadline = time::Instant::now() + DAYTONA_BASH_SESSION_PROBE_TIMEOUT; let timeout_error = || { crate::Error::message(format!( "Daytona Bash session check timed out after {}s", - DAYTONA_PROBE_TIMEOUT.as_secs() + DAYTONA_BASH_SESSION_PROBE_TIMEOUT.as_secs() )) }; let mut session = match time::timeout_at(deadline, DaytonaSession::create(sandbox)).await { @@ -931,7 +1034,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(), @@ -1056,6 +1162,157 @@ impl DaytonaSandbox { "Timed out waiting for snapshot '{name}' to become active" ))) } + + async fn wait_for_stable_state( + &self, + sandbox_name: &str, + ) -> Result, DaytonaError> { + loop { + time::sleep(DAYTONA_STATE_CHANGE_POLL_INTERVAL).await; + let state = self.client.get(sandbox_name).await?.state; + if !is_transitional_state(state) { + return Ok(state); + } + } + } + + async fn run_lifecycle_action( + &self, + sandbox_name: &str, + action: DaytonaLifecycleAction, + deadline: time::Instant, + ) -> crate::Result<()> { + loop { + let request = time::timeout_at(deadline, action.execute(&self.client, sandbox_name)); + match request.await { + Ok(Ok(())) => return Ok(()), + Ok(Err(source)) if is_state_change_in_progress(&source) => { + tracing::debug!( + action = %action, + sandbox = sandbox_name, + "Daytona lifecycle request rejected during state change" + ); + match time::timeout_at(deadline, self.wait_for_stable_state(sandbox_name)).await + { + Ok(Ok(state)) if action.is_complete(state) => return Ok(()), + Ok(Ok(_)) => {} + Ok(Err(wait_source)) => { + return Err(crate::Error::context( + format!( + "Failed to inspect Daytona sandbox while waiting to {action}" + ), + wait_source, + )); + } + Err(_) => { + return Err(crate::Error::context( + format!("Timed out waiting to {action} Daytona sandbox"), + source, + )); + } + } + } + Ok(Err(source)) => { + return Err(crate::Error::context( + format!("Failed to {action} Daytona sandbox"), + source, + )); + } + Err(_) => { + return Err(crate::Error::message(format!( + "Timed out waiting to {action} Daytona sandbox" + ))); + } + } + } + } + + fn start_error(&self, error: crate::Error) -> crate::Result<()> { + self.emit(SandboxEvent::StartFailed { + provider: "daytona".into(), + error: error.to_string(), + causes: error.causes(), + }); + Err(error) + } + + fn stop_error(&self, error: crate::Error) -> crate::Result<()> { + self.emit(SandboxEvent::StopFailed { + provider: "daytona".into(), + error: error.to_string(), + causes: error.causes(), + }); + Err(error) + } + + async fn start_with_deadline(&self, deadline: time::Instant) -> crate::Result<()> { + self.emit(SandboxEvent::StartStarted { + provider: "daytona".into(), + }); + let start = Instant::now(); + let result = async { + let sandbox = self.sandbox()?; + self.run_lifecycle_action(&sandbox.name, DaytonaLifecycleAction::Start, deadline) + .await?; + Self::probe_bash(sandbox).await + } + .await; + if let Err(error) = result { + return self.start_error(error); + } + self.emit(SandboxEvent::StartCompleted { + provider: "daytona".into(), + duration_ms: elapsed_ms(start), + }); + Ok(()) + } + + async fn stop_with_deadline(&self, deadline: time::Instant) -> crate::Result<()> { + self.emit(SandboxEvent::StopStarted { + provider: "daytona".into(), + }); + let start = Instant::now(); + let result = async { + let sandbox = self.sandbox()?; + self.run_lifecycle_action(&sandbox.name, DaytonaLifecycleAction::Stop, deadline) + .await + } + .await; + if let Err(error) = result { + return self.stop_error(error); + } + self.emit(SandboxEvent::StopCompleted { + provider: "daytona".into(), + duration_ms: elapsed_ms(start), + }); + Ok(()) + } +} + +fn is_state_change_in_progress(err: &DaytonaError) -> bool { + err.status_code() == Some(400) + && err + .message() + .to_ascii_lowercase() + .contains("state change in progress") +} + +fn is_transitional_state(state: Option) -> bool { + matches!( + state, + Some( + SandboxState::Creating + | SandboxState::Restoring + | SandboxState::Destroying + | SandboxState::Starting + | SandboxState::Stopping + | SandboxState::PendingBuild + | SandboxState::BuildingSnapshot + | SandboxState::PullingSnapshot + | SandboxState::Archiving + | SandboxState::Resizing + ) + ) } /// Detect the git remote URL and current branch from a local repository. @@ -1261,37 +1518,39 @@ impl Sandbox for DaytonaSandbox { let layout = clone_source::github_repo_layout(&origin_url, WORKING_DIRECTORY, REPOS_ROOT) .map_err(|err| self.fail_init(init_start, err))?; - let token_was_freshly_minted = self - .github_app - .as_ref() - .is_some_and(GitHubCredentials::mints_installation_token); self.emit(SandboxEvent::GitCloneStarted { url: origin_url.clone(), branch: branch.clone(), }); let clone_start = Instant::now(); - let (username, password) = match &self.github_app { - Some(creds) => fabro_github::resolve_clone_credentials( - &fabro_github::GitHubContext::new( - creds, - &fabro_github::github_api_base_url(), - ), - &layout.owner, - &layout.repo, - ) - .await - .map_err(|e| { - let err = crate::Error::message(format!( - "Failed to get GitHub App credentials for clone: {e}" - )); + // The clone mints its own token (never a warm-cache reuse) and + // seeds the shared source, so the first refresh compares + // against the clone token instead of believing nothing was + // ever embedded. + let resolved_token = match self.push_credentials.source() { + Some(source) => Some(source.mint_for_clone().await.map_err(|source| { + let err = crate::Error::context_anyhow( + "Failed to get GitHub App credentials for clone", + source, + ); self.emit(SandboxEvent::GitCloneFailed { url: origin_url.clone(), error: err.to_string(), causes: err.causes(), }); self.fail_init(init_start, err) - })?, + })?), + None => None, + }; + let clone_credential_context = CredentialContext::from_snapshot( + resolved_token.as_ref().map(|token| &token.snapshot), + ); + let (username, password) = match &resolved_token { + Some(token) => ( + Some("x-access-token".to_string()), + Some(token.token.expose().to_string()), + ), None => (None, None), }; @@ -1352,9 +1611,11 @@ impl Sandbox for DaytonaSandbox { self.fail_init(init_start, err) })?; - let clone_result = clone_retry::retry_clone( + let clone_plan = git_retry::RetryPlan::clone_default(None); + let clone_result = git_retry::retry_clone( SandboxProviderKind::Daytona, - None, + "clone", + &clone_plan, |_attempt| { let git_svc = &git_svc; let origin = origin_url.as_str(); @@ -1367,13 +1628,13 @@ impl Sandbox for DaytonaSandbox { ); async move { git_svc.clone(origin, target, options).await } }, - |err: &DaytonaError| classify_clone_failure(err, token_was_freshly_minted), + |err: &DaytonaError| classify_clone_failure(err, clone_credential_context), ) .await; match clone_result { Ok(()) => {} - Err(e) if self.github_app.is_none() => { + Err(e) if self.push_credentials.source().is_none() => { let err = crate::Error::context( "Git clone failed. If this is a private repository, configure a \ GitHub App with `fabro install` and install it for your organization.", @@ -1454,8 +1715,8 @@ impl Sandbox for DaytonaSandbox { let _ = self.origin_url.set(origin_url.clone()); self.set_working_directory(layout.execution_directory.clone()) .map_err(|err| self.fail_init(init_start, err))?; - if let Some(token) = password.as_deref() { - match fabro_github::embed_token_in_url(&origin_url, token) { + if let Some(resolved) = resolved_token { + match fabro_github::embed_token_in_url(&origin_url, resolved.token.expose()) { Ok(auth_url) => { let credential_deadline = time::Instant::now() + DAYTONA_CREDENTIAL_SETUP_TIMEOUT; @@ -1493,7 +1754,11 @@ impl Sandbox for DaytonaSandbox { sandbox will fail" ); } - Ok(_) => {} + Ok(_) => { + // Origin now carries this token; record it so refreshes compare + // against the clone generation. + self.push_credentials.record_embedded(resolved).await; + } Err(_) => { tracing::warn!( error_class = "daytona_set_url_exec_failed", @@ -1506,7 +1771,7 @@ impl Sandbox for DaytonaSandbox { } Err(e) => { tracing::warn!( - origin = %origin_url, + origin = %fabro_redact::redacted_url_for_log(&origin_url), error = %e, "Failed to build authenticated origin URL — \ subsequent git push from this sandbox will fail" @@ -1539,76 +1804,47 @@ impl Sandbox for DaytonaSandbox { } async fn start(&self) -> crate::Result<()> { - self.emit(SandboxEvent::StartStarted { - provider: "daytona".into(), - }); - let start = Instant::now(); - let sandbox = self.sandbox()?; - if let Err(e) = self.client.start(&sandbox.name).await { - let err = crate::Error::context("Failed to start Daytona sandbox", e); - self.emit(SandboxEvent::StartFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - if let Err(err) = Self::probe_bash(sandbox).await { - self.emit(SandboxEvent::StartFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - let duration_ms = elapsed_ms(start); - self.emit(SandboxEvent::StartCompleted { - provider: "daytona".into(), - duration_ms, - }); - Ok(()) + self.start_with_deadline(time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT) + .await } async fn activate(&self) -> crate::Result<()> { let sandbox = self.sandbox()?; - let current = self.client.get(&sandbox.name).await.map_err(|e| { - crate::Error::context("Failed to inspect Daytona sandbox before activation", e) - })?; - if current.state == Some(SandboxState::Started) { + let deadline = time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT; + let current = time::timeout_at(deadline, self.client.get(&sandbox.name)) + .await + .map_err(|_| { + crate::Error::message("Timed out inspecting Daytona sandbox before activation") + })? + .map_err(|e| { + crate::Error::context("Failed to inspect Daytona sandbox before activation", e) + })?; + let state = if is_transitional_state(current.state) { + time::timeout_at(deadline, self.wait_for_stable_state(&sandbox.name)) + .await + .map_err(|_| { + crate::Error::message( + "Timed out waiting for Daytona sandbox state change before activation", + ) + })? + .map_err(|e| { + crate::Error::context( + "Failed to wait for Daytona sandbox state change before activation", + e, + ) + })? + } else { + current.state + }; + if state == Some(SandboxState::Started) { return Ok(()); } - if current.state == Some(SandboxState::Starting) { - return current - .wait_for_start(Some(DAYTONA_START_TIMEOUT)) - .await - .map_err(|e| { - crate::Error::context("Failed to wait for Daytona sandbox activation", e) - }); - } - self.start().await + self.start_with_deadline(deadline).await } async fn stop(&self) -> crate::Result<()> { - self.emit(SandboxEvent::StopStarted { - provider: "daytona".into(), - }); - let start = Instant::now(); - let sandbox = self.sandbox()?; - if let Err(e) = self.client.stop(&sandbox.name).await { - let err = crate::Error::context("Failed to stop Daytona sandbox", e); - self.emit(SandboxEvent::StopFailed { - provider: "daytona".into(), - error: err.to_string(), - causes: err.causes(), - }); - return Err(err); - } - let duration_ms = elapsed_ms(start); - self.emit(SandboxEvent::StopCompleted { - provider: "daytona".into(), - duration_ms, - }); - Ok(()) + self.stop_with_deadline(time::Instant::now() + DAYTONA_STATE_CHANGE_TIMEOUT) + .await } async fn delete(&self) -> crate::Result<()> { @@ -1685,11 +1921,19 @@ impl Sandbox for DaytonaSandbox { )] } - async fn git_push_ref(&self, refspec: &str) -> crate::Result<()> { + async fn git_push_ref( + &self, + refspec: &str, + plan: &crate::RetryPlan, + ) -> Result { if !self.repo_cloned() { - return Ok(()); + return Ok(crate::PushReport::default()); } - crate::git_push_via_exec(self, refspec).await + let credentials = self + .origin_url + .get() + .map(|origin_url| (&self.push_credentials, origin_url.as_str())); + sandbox::git_push_via_exec(self, credentials, refspec, plan).await } async fn ssh_access_command(&self) -> crate::Result> { @@ -1722,52 +1966,23 @@ impl Sandbox for DaytonaSandbox { Ok(Some((preview.url, headers))) } + #[tracing::instrument(name = "git_op", skip_all, fields(op = "refresh-credentials"))] async fn refresh_push_credentials(&self) -> crate::Result { if !self.repo_cloned() { - return Ok(RefreshOutcome::Skipped); + return Ok(RefreshOutcome::none()); } let Some(origin_url) = self.origin_url.get() else { - return Ok(RefreshOutcome::Skipped); // no authenticated origin — nothing to refresh + return Ok(RefreshOutcome::none()); // no authenticated origin — nothing to refresh }; - let Some(creds) = &self.github_app else { - return Ok(RefreshOutcome::Skipped); - }; - // Only a GitHub App installation token can be re-minted; a static PAT or - // a pre-minted Installation token is fixed, so re-embedding it changes - // nothing. Short-circuit to Skipped before the resolve + set-url exec. - if !creds.mints_installation_token() { - return Ok(RefreshOutcome::Skipped); - } - - let auth_url = fabro_github::resolve_authenticated_url( - &fabro_github::GitHubContext::new(creds, &fabro_github::github_api_base_url()), - origin_url, - ) - .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: token_mint_failed") - })?; - - let cmd = format!( - "git -c maintenance.auto=0 remote set-url origin {}", - shell_quote(auth_url.as_raw_url().as_str()), - ); - let result = self - .exec_command(&cmd, 10_000, None, None, None) + self.push_credentials + .refresh(origin_url, |auth_url| { + push_credentials::set_auth_url_via_exec(self, auth_url) + }) .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: set_url_exec_failed") - })?; - if !result.is_success() { - return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (refresh push credentials)", - |s| redact_auth_url(s, Some(&auth_url)), - )); - } + } - // Static creds were short-circuited to Skipped above; reaching here means - // a GitHub App installation token was freshly minted. - Ok(RefreshOutcome::Refreshed) + fn push_token_source(&self) -> Option> { + self.push_credentials.source().cloned() } async fn set_autostop_interval(&self, minutes: i32) -> crate::Result<()> { @@ -1815,17 +2030,12 @@ impl Sandbox for DaytonaSandbox { } } - let fs_svc = sandbox - .fs() - .await - .map_err(|e| crate::Error::context("Failed to get fs service", e))?; + self.upload_file_content(&resolved, content).await + } - fs_svc - .upload_file_bytes(&resolved, content.as_bytes()) - .await - .map_err(|e| crate::Error::context(format!("Failed to write file {resolved}"), e))?; - - Ok(()) + async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> { + let resolved = self.resolve_path(path); + self.upload_file_content(&resolved, content).await } async fn delete_file(&self, path: &str) -> crate::Result<()> { @@ -2808,23 +3018,20 @@ fn daytona_bash_session_probe_outcome(execution: crate::Result) -> c /// inside the sandbox, so its stderr comes back through the toolbox as the /// error message — the credential race has to be matched on text. Daytona's own /// transport failures are visible structurally. -fn classify_clone_failure( - err: &DaytonaError, - token_was_freshly_minted: bool, -) -> Option { +fn classify_clone_failure(err: &DaytonaError, cred: CredentialContext) -> Option { // A Daytona request timeout does not prove that the remote clone stopped. // Retrying could overlap the still-running first request. if matches!(err, DaytonaError::Timeout { .. }) { return None; } - match clone_retry::classify_message(err.message(), token_was_freshly_minted) { - clone_retry::CloneMessageClass::Retry(reason) => Some(reason), - clone_retry::CloneMessageClass::Permanent => None, - clone_retry::CloneMessageClass::Unknown => match err { - DaytonaError::RateLimit { .. } => Some(CloneRetryReason::TransientInfra), + match git_retry::classify_message(err.message(), cred) { + git_retry::GitMessageClass::Retry(reason) => Some(reason), + git_retry::GitMessageClass::Permanent => None, + git_retry::GitMessageClass::Unknown => match err { + DaytonaError::RateLimit { .. } => Some(GitRetryReason::TransientInfra), DaytonaError::Api { status_code, .. } if (500..600).contains(status_code) => { - Some(CloneRetryReason::TransientInfra) + Some(GitRetryReason::TransientInfra) } DaytonaError::Timeout { .. } | DaytonaError::Api { .. } @@ -3253,7 +3460,7 @@ mod tests { config, client, api_key: Some(api_key.to_string()), - github_app: None, + push_credentials: PushCredentialState::new(None), sandbox: OnceCell::new(), snapshot_name: OnceCell::new(), rg_available: OnceCell::const_new(), @@ -3449,6 +3656,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())])) @@ -3462,6 +3673,28 @@ 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, + 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; @@ -3563,6 +3796,201 @@ mod tests { start_sandbox.assert_calls_async(0).await; } + #[tokio::test] + async fn activate_waits_out_a_stop_in_progress() { + let server = MockServer::start_async().await; + let response_count = Arc::new(AtomicU32::new(0)); + let get_sandbox = server + .mock_async({ + let response_count = Arc::clone(&response_count); + move |when, then| { + when.method(GET) + .path("/sandbox/test-sandbox") + .header("authorization", "Bearer dtn_test"); + then.respond_with(move |_| { + let state = if response_count.fetch_add(1, Ordering::Relaxed) == 1 { + SandboxState::Stopping + } else { + SandboxState::Started + }; + HttpMockResponse::builder() + .status(200) + .header("content-type", "application/json") + .body(sandbox_body("test-sandbox", state).to_string()) + .build() + }); + } + }) + .await; + let start_sandbox = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox/test-sandbox/start") + .header("authorization", "Bearer dtn_test"); + then.status(200) + .header("content-type", "application/json") + .json_body(sandbox_body("test-sandbox", SandboxState::Started)); + }) + .await; + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("test-sandbox") + .await + .expect("test sandbox should load"); + sandbox + .sandbox + .set(sdk_sandbox) + .expect("test sandbox should initialize once"); + + let get_calls_before = get_sandbox.calls_async().await; + sandbox + .activate() + .await + .expect("an in-progress stop should be waited out"); + + assert_eq!(get_sandbox.calls_async().await, get_calls_before + 2); + start_sandbox.assert_calls_async(0).await; + } + + #[tokio::test] + async fn stop_succeeds_when_a_pending_auto_stop_finishes_first() { + let server = MockServer::start_async().await; + let response_count = Arc::new(AtomicU32::new(0)); + let get_sandbox = server + .mock_async({ + let response_count = Arc::clone(&response_count); + move |when, then| { + when.method(GET) + .path("/sandbox/test-sandbox") + .header("authorization", "Bearer dtn_test"); + then.respond_with(move |_| { + let state = if response_count.fetch_add(1, Ordering::Relaxed) == 0 { + SandboxState::Started + } else { + SandboxState::Stopped + }; + HttpMockResponse::builder() + .status(200) + .header("content-type", "application/json") + .body(sandbox_body("test-sandbox", state).to_string()) + .build() + }); + } + }) + .await; + let stop_sandbox = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox/test-sandbox/stop") + .header("authorization", "Bearer dtn_test"); + then.status(400) + .header("content-type", "application/json") + .json_body(serde_json::json!({ + "message": "Sandbox state change in progress", + "statusCode": 400 + })); + }) + .await; + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("test-sandbox") + .await + .expect("test sandbox should load"); + sandbox + .sandbox + .set(sdk_sandbox) + .expect("test sandbox should initialize once"); + + let get_calls_before = get_sandbox.calls_async().await; + sandbox + .stop() + .await + .expect("a stop already in flight should count as stopped"); + + stop_sandbox.assert_calls_async(1).await; + assert_eq!(get_sandbox.calls_async().await, get_calls_before + 1); + } + + #[tokio::test] + async fn start_surfaces_state_change_rejection_after_the_deadline() { + let server = MockServer::start_async().await; + let _get_sandbox = server + .mock_async(|when, then| { + when.method(GET) + .path("/sandbox/test-sandbox") + .header("authorization", "Bearer dtn_test"); + then.status(200) + .header("content-type", "application/json") + .json_body(sandbox_body("test-sandbox", SandboxState::Stopping)); + }) + .await; + let start_sandbox = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox/test-sandbox/start") + .header("authorization", "Bearer dtn_test"); + then.status(400) + .header("content-type", "application/json") + .json_body(serde_json::json!({ + "message": "Sandbox state change in progress", + "statusCode": 400 + })); + }) + .await; + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("test-sandbox") + .await + .expect("test sandbox should load"); + sandbox + .sandbox + .set(sdk_sandbox) + .expect("test sandbox should initialize once"); + + let err = sandbox + .start_with_deadline(time::Instant::now() + Duration::from_millis(1500)) + .await + .expect_err("a state change that outlives the deadline should fail"); + + assert_eq!( + start_sandbox.calls_async().await, + 1, + "start should not be retried while the current transition is in flight" + ); + assert!( + err.causes().iter().any(|cause| cause + .to_ascii_lowercase() + .contains("state change in progress")), + "error should carry the Daytona rejection: {err}" + ); + } + + #[test] + fn state_change_in_progress_matcher_ignores_case_and_context() { + assert!(is_state_change_in_progress(&DaytonaError::api( + 400, + "Sandbox state change in progress" + ))); + assert!(is_state_change_in_progress(&DaytonaError::api( + 400, + "State Change In Progress" + ))); + assert!(!is_state_change_in_progress(&DaytonaError::api( + 400, + "Sandbox already started" + ))); + assert!(!is_state_change_in_progress(&DaytonaError::api( + 500, + "Sandbox state change in progress" + ))); + assert!(!is_state_change_in_progress(&DaytonaError::general( + "Sandbox state change in progress" + ))); + } + #[tokio::test] async fn base_params_merges_managed_daytona_labels() { let run_id: RunId = "01HY0000000000000000000000".parse().unwrap(); @@ -3632,11 +4060,11 @@ mod tests { let err = DaytonaError::general("repository not found: Repository not found."); assert_eq!( - classify_clone_failure(&err, true), - Some(CloneRetryReason::TokenReplication) + classify_clone_failure(&err, CredentialContext::FreshApp), + Some(GitRetryReason::TokenReplication) ); assert_eq!( - classify_clone_failure(&err, false), + classify_clone_failure(&err, CredentialContext::None), None, "without credentials there is no token to replicate" ); @@ -3649,8 +4077,8 @@ mod tests { DaytonaError::api(503, ""), ] { assert_eq!( - classify_clone_failure(&err, false), - Some(CloneRetryReason::TransientInfra), + classify_clone_failure(&err, CredentialContext::None), + Some(GitRetryReason::TransientInfra), "expected {err:?} to be transient" ); } @@ -3660,24 +4088,33 @@ mod tests { fn clone_timeout_is_not_retried_without_remote_termination() { let err = DaytonaError::timeout("request timed out"); - assert_eq!(classify_clone_failure(&err, true), None); + assert_eq!( + classify_clone_failure(&err, CredentialContext::FreshApp), + None + ); } #[test] fn clone_api_failure_message_takes_precedence_over_status() { let not_found = DaytonaError::api(500, "repository not found: Repository not found."); assert_eq!( - classify_clone_failure(¬_found, true), - Some(CloneRetryReason::TokenReplication) + classify_clone_failure(¬_found, CredentialContext::FreshApp), + Some(GitRetryReason::TokenReplication) + ); + assert_eq!( + classify_clone_failure(¬_found, CredentialContext::Static), + None ); - assert_eq!(classify_clone_failure(¬_found, false), None); for message in [ "fatal: destination path 'fabro' already exists", "remote: Permission to fabro-sh/fabro.git denied", ] { assert_eq!( - classify_clone_failure(&DaytonaError::api(500, message), true), + classify_clone_failure( + &DaytonaError::api(500, message), + CredentialContext::FreshApp + ), None, "expected {message:?} to take precedence over HTTP 500" ); @@ -3693,7 +4130,7 @@ mod tests { DaytonaError::general("fatal: could not read Username for 'https://github.com'"), ] { assert_eq!( - classify_clone_failure(&err, true), + classify_clone_failure(&err, CredentialContext::FreshApp), None, "expected {err:?} to fail fast" ); @@ -3858,6 +4295,25 @@ mod tests { auth.assert_async().await; } + #[tokio::test] + async fn daytona_credential_probe_reports_configured_timeout() { + let err = daytona_credential_probe_with_timeout( + std::future::pending::>(), + Duration::from_millis(1), + ) + .await + .expect_err("probe should time out"); + let timeout = err + .downcast_ref::() + .expect("timeout should preserve its type"); + + assert_eq!(timeout.timeout(), Duration::from_millis(1)); + assert_eq!( + err.to_string(), + "Daytona credential probe timed out after 1ms" + ); + } + #[tokio::test] async fn daytona_stdin_file_uploads_exact_bytes_and_is_deleted() { let server = MockServer::start_async().await; @@ -3932,6 +4388,62 @@ mod tests { delete.assert_async().await; } + #[tokio::test] + async fn write_existing_file_skips_parent_directory_creation() { + let server = MockServer::start_async().await; + let server_url = server.base_url(); + let sandbox_response = server + .mock_async(|when, then| { + when.method(GET).path("/sandbox/sandbox-edit"); + then.status(200) + .header("content-type", "application/json") + .json_body(sandbox_body("sandbox-edit", SandboxState::Started)); + }) + .await; + let toolbox_response = server + .mock_async(|when, then| { + when.method(GET) + .path("/sandbox/sandbox-edit/toolbox-proxy-url"); + then.status(200) + .header("content-type", "application/json") + .json_body(serde_json::json!({"url": server_url})); + }) + .await; + let folder = server + .mock_async(|when, then| { + when.method(POST).path("/sandbox-edit/files/folder"); + then.status(200); + }) + .await; + let upload = server + .mock_async(|when, then| { + when.method(POST) + .path("/sandbox-edit/files/upload") + .query_param("path", "/home/daytona/workspace/src/lib.rs") + .body_includes("updated contents"); + then.status(200); + }) + .await; + + let sandbox = mock_daytona_sandbox(&server, "dtn_test", DaytonaConfig::default()).await; + let sdk_sandbox = sandbox + .client + .get("sandbox-edit") + .await + .expect("get mock sandbox"); + assert!(sandbox.sandbox.set(sdk_sandbox).is_ok()); + + sandbox + .write_existing_file("src/lib.rs", "updated contents") + .await + .expect("write existing file"); + + sandbox_response.assert_async().await; + toolbox_response.assert_async().await; + upload.assert_async().await; + folder.assert_calls_async(0).await; + } + /// Recover the inner command a wrapper carries, proving it survives the /// base64 transport byte-for-byte. fn decode_wrapped_command(wrapped: &str) -> String { diff --git a/lib/components/fabro-sandbox/src/docker.rs b/lib/components/fabro-sandbox/src/docker.rs index 1ee22ce90..c3842ef9a 100644 --- a/lib/components/fabro-sandbox/src/docker.rs +++ b/lib/components/fabro-sandbox/src/docker.rs @@ -17,6 +17,7 @@ use bollard::exec::{CreateExecOptions, StartExecOptions, StartExecResults}; use bollard::image::CreateImageOptions; use bollard::models::{ContainerInspectResponse, HostConfig}; use fabro_github::GitHubCredentials; +use fabro_github::token_source::InstallationTokenSource; use fabro_types::{CommandOutputStream, CommandTermination, RunId, SandboxProviderKind}; use fabro_util::time::elapsed_ms; use futures::StreamExt; @@ -26,7 +27,9 @@ use tokio::{fs, time}; use tokio_util::sync::CancellationToken; use crate::clone_source::{self, CloneDecision, EmptyWorkspaceReason}; +use crate::git_retry::{self, CredentialContext}; use crate::managed_labels::{self, MANAGED_LABEL, RUN_ID_LABEL}; +use crate::push_credentials::{self, PushCredentialState}; use crate::redact::redact_auth_url; use crate::sandbox::{ self, BASH_ENV_VAR, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, REMOTE_BASH, @@ -37,7 +40,7 @@ use crate::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, GrepOptions, Sandbox, SandboxEvent, SandboxEventCallback, SandboxFile, StderrCollector, StdioProcess, StdioProcessHandle, - StdioProcessTermination, WalkOptions, clone_retry, format_lines_numbered, shell_quote, + StdioProcessTermination, WalkOptions, format_lines_numbered, shell_quote, }; const DOCKER_BASH_REQUIREMENT: &str = "Docker sandboxes require /bin/bash for every command, with no `sh` fallback; use an \ @@ -64,6 +67,11 @@ enum CloneStep { Local, } +struct DockerCloneFailure { + error: crate::Error, + retry_reason: Option, +} + fn env_entry_name(entry: &str) -> &str { entry.split_once('=').map_or(entry, |(name, _)| name) } @@ -134,7 +142,7 @@ impl Default for DockerSandboxOptions { pub struct DockerSandbox { docker: Docker, config: DockerSandboxOptions, - github_app: Option, + push_credentials: PushCredentialState, run_id: Option, clone_origin_url: Option, clone_branch: Option, @@ -165,7 +173,7 @@ enum ContainerStartAction { impl DockerSandbox { pub fn new( config: DockerSandboxOptions, - github_app: Option, + github_app: Option<&GitHubCredentials>, run_id: Option, clone_origin_url: Option, clone_branch: Option, @@ -180,7 +188,7 @@ impl DockerSandbox { )?; } let docker = Docker::connect_with_local_defaults().map_err(crate::Error::docker_connect)?; - Ok(Self::with_docker_client( + Self::with_docker_client( docker, config, github_app, @@ -188,22 +196,26 @@ impl DockerSandbox { clone_origin_url, clone_branch, clone_commit_sha, - )) + ) } fn with_docker_client( docker: Docker, config: DockerSandboxOptions, - github_app: Option, + github_app: Option<&GitHubCredentials>, run_id: Option, clone_origin_url: Option, clone_branch: Option, clone_commit_sha: Option, - ) -> Self { - Self { + ) -> crate::Result { + let push_credentials = PushCredentialState::new(push_credentials::build_token_source( + github_app, + clone_origin_url.as_deref(), + )?); + Ok(Self { docker, config, - github_app, + push_credentials, run_id, clone_origin_url, clone_branch, @@ -216,7 +228,7 @@ impl DockerSandbox { cached_os_version: std::sync::OnceLock::new(), rg_available: OnceCell::const_new(), event_callback: None, - } + }) } pub async fn reconnect( @@ -736,7 +748,7 @@ impl DockerSandbox { let error = result.into_exec_error_with_redactor(label, |output| redact_auth_url(output, auth_url)); let message = match step { - CloneStep::Network if self.github_app.is_none() => { + CloneStep::Network if self.push_credentials.source().is_none() => { "Git clone failed. If this is a private repository, configure a GitHub App with \ `fabro install` and install it for your organization." } @@ -791,20 +803,23 @@ impl DockerSandbox { async fn retry_git_transfer( &self, command: &str, + op: &'static str, label: &'static str, exec_label: &'static str, clone_deadline: time::Instant, - token_was_freshly_minted: bool, + credential_context: CredentialContext, auth_url: Option<&fabro_redact::DisplaySafeUrl>, - ) -> Result<(), clone_retry::CloneAttemptFailure> { - clone_retry::retry_clone( + ) -> Result<(), DockerCloneFailure> { + let plan = git_retry::RetryPlan::clone_default(Some(clone_deadline)); + git_retry::retry_clone( SandboxProviderKind::Docker, - Some(clone_deadline), + op, + &plan, |_attempt| async move { let remaining = clone_deadline.saturating_duration_since(time::Instant::now()); let timeout_ms = u64::try_from(remaining.as_millis()).unwrap_or(u64::MAX); if timeout_ms == 0 { - return Err(clone_retry::CloneAttemptFailure { + return Err(DockerCloneFailure { error: crate::Error::message(format!( "{label} deadline expired before retry" )), @@ -818,7 +833,7 @@ impl DockerSandbox { ..ExecStreamingRequest::new(command) }) .await - .map_err(|error| clone_retry::CloneAttemptFailure { + .map_err(|error| DockerCloneFailure { error: crate::Error::context( format!("{label} transport failed"), error, @@ -829,8 +844,8 @@ impl DockerSandbox { if result.is_success() { return Ok(()); } - let retry_reason = classify_docker_clone_result(&result, token_was_freshly_minted); - Err(clone_retry::CloneAttemptFailure { + let retry_reason = classify_docker_clone_result(&result, credential_context); + Err(DockerCloneFailure { error: self.clone_failure_error( result, exec_label, @@ -840,7 +855,7 @@ impl DockerSandbox { retry_reason, }) }, - |failure: &clone_retry::CloneAttemptFailure| failure.retry_reason, + |failure: &DockerCloneFailure| failure.retry_reason, ) .await } @@ -853,22 +868,31 @@ impl DockerSandbox { ) -> crate::Result<()> { self.verify_git_available().await?; let layout = clone_source::github_repo_layout(&origin_url, WORKING_DIRECTORY, REPOS_ROOT)?; - let token_was_freshly_minted = self - .github_app - .as_ref() - .is_some_and(GitHubCredentials::mints_installation_token); + // The clone mints its own token (never a warm-cache reuse) and seeds + // the shared source, so the first refresh compares against the clone + // token instead of believing nothing was ever embedded. + let resolved_token = match self.push_credentials.source() { + Some(source) => Some(source.mint_for_clone().await.map_err(|err| { + crate::Error::context_anyhow("Failed to get GitHub App credentials for clone", err) + })?), + None => None, + }; + // The clone call site maps its mint knowledge onto the credential + // context: a token minted for this clone is FreshApp; a static + // credential cannot become valid by waiting. + let clone_credential_context = + CredentialContext::from_snapshot(resolved_token.as_ref().map(|token| &token.snapshot)); - let auth_url = match &self.github_app { - Some(creds) => Some( - fabro_github::resolve_authenticated_url( - &fabro_github::GitHubContext::new(creds, &fabro_github::github_api_base_url()), - &origin_url, - ) - .await - .map_err(|error| crate::Error::Context { - message: "Failed to get GitHub App credentials for clone".to_string(), - source: error.into_boxed_dyn_error(), - })?, + let auth_url = match &resolved_token { + Some(token) => Some( + fabro_github::embed_token_in_url(&origin_url, token.token.expose()).map_err( + |err| { + crate::Error::context_anyhow( + "Failed to build authenticated GitHub clone URL", + err, + ) + }, + )?, ), None => None, }; @@ -935,10 +959,11 @@ impl DockerSandbox { if let Err(failure) = self .retry_git_transfer( &fetch_command, + "fetch", "Docker exact fetch", "git fetch exact commit", clone_deadline, - token_was_freshly_minted, + clone_credential_context, auth_url.as_ref(), ) .await @@ -972,10 +997,11 @@ impl DockerSandbox { if let Err(failure) = self .retry_git_transfer( &command, + "clone", "Docker git clone", "git clone", clone_deadline, - token_was_freshly_minted, + clone_credential_context, auth_url.as_ref(), ) .await @@ -1002,6 +1028,11 @@ impl DockerSandbox { let _ = self.repo_cloned.set(true); let _ = self.origin_url.set(origin_url.clone()); self.set_working_directory(layout.execution_directory.clone())?; + if let Some(token) = resolved_token { + // The clone URL embedded this token in `origin`; record it so + // refreshes compare against the clone generation. + self.push_credentials.record_embedded(token).await; + } if let Some(auth_url) = auth_url.as_ref() { let command = format!( @@ -1517,15 +1548,9 @@ fn git_clone_command(clone_url: &str, branch: Option<&str>, checkout_path: &str) fn classify_docker_clone_result( result: &ExecResult, - token_was_freshly_minted: bool, -) -> Option { - let stderr = clone_retry::classify_message(&result.stderr, token_was_freshly_minted); - match stderr { - clone_retry::CloneMessageClass::Unknown => { - clone_retry::classify_message(&result.stdout, token_was_freshly_minted).retry_reason() - } - class => class.retry_reason(), - } + cred: CredentialContext, +) -> Option { + git_retry::classify_output(&result.stderr, &result.stdout, cred).retry_reason() } fn host_config(config: &DockerSandboxOptions) -> HostConfig { @@ -2311,11 +2336,19 @@ impl Sandbox for DockerSandbox { )] } - async fn git_push_ref(&self, refspec: &str) -> crate::Result<()> { + async fn git_push_ref( + &self, + refspec: &str, + plan: &crate::RetryPlan, + ) -> Result { if !self.repo_cloned() { - return Ok(()); + return Ok(crate::PushReport::default()); } - crate::git_push_via_exec(self, refspec).await + let credentials = self + .origin_url + .get() + .map(|origin_url| (&self.push_credentials, origin_url.as_str())); + sandbox::git_push_via_exec(self, credentials, refspec, plan).await } fn origin_url(&self) -> Option<&str> { @@ -2325,49 +2358,23 @@ impl Sandbox for DockerSandbox { self.origin_url.get().map(String::as_str) } + #[tracing::instrument(name = "git_op", skip_all, fields(op = "refresh-credentials"))] async fn refresh_push_credentials(&self) -> crate::Result { if !self.repo_cloned() { - return Ok(RefreshOutcome::Skipped); + return Ok(RefreshOutcome::none()); } let Some(origin_url) = self.origin_url.get() else { - return Ok(RefreshOutcome::Skipped); + return Ok(RefreshOutcome::none()); }; - let Some(creds) = &self.github_app else { - return Ok(RefreshOutcome::Skipped); - }; - // Only a GitHub App installation token can be re-minted; a static PAT or - // a pre-minted Installation token is fixed, so re-embedding it changes - // nothing. Short-circuit to Skipped before the resolve + set-url exec. - if !creds.mints_installation_token() { - return Ok(RefreshOutcome::Skipped); - } + self.push_credentials + .refresh(origin_url, |auth_url| { + push_credentials::set_auth_url_via_exec(self, auth_url) + }) + .await + } - let auth_url = fabro_github::resolve_authenticated_url( - &fabro_github::GitHubContext::new(creds, &fabro_github::github_api_base_url()), - origin_url, - ) - .await - .map_err(|_| { - crate::Error::message("Failed to refresh push credentials: token_mint_failed") - })?; - - let command = format!( - "git -c maintenance.auto=0 remote set-url origin {}", - shell_quote(auth_url.as_raw_url().as_str()) - ); - let result = self - .docker_exec_shell(&command, 10_000, Some(self.working_directory()), None, None) - .await?; - if !result.is_success() { - return Err(result.into_exec_error_with_redactor( - "git remote set-url origin (refresh push credentials)", - |s| redact_auth_url(s, Some(&auth_url)), - )); - } - - // Static creds were short-circuited to Skipped above; reaching here means - // a GitHub App installation token was freshly minted. - Ok(RefreshOutcome::Refreshed) + fn push_token_source(&self) -> Option> { + self.push_credentials.source().cloned() } } @@ -2690,7 +2697,10 @@ mod tests { duration_ms: 1, }; - assert_eq!(classify_docker_clone_result(&result, true), None); + assert_eq!( + classify_docker_clone_result(&result, CredentialContext::FreshApp), + None + ); } #[test] @@ -2977,7 +2987,8 @@ mod tests { None, None, None, - ); + ) + .expect("test sandbox should build"); sandbox .container_id .set(container_id.to_string()) diff --git a/lib/components/fabro-sandbox/src/error.rs b/lib/components/fabro-sandbox/src/error.rs index 8d6bf0d73..76f096d4b 100644 --- a/lib/components/fabro-sandbox/src/error.rs +++ b/lib/components/fabro-sandbox/src/error.rs @@ -18,6 +18,13 @@ pub enum Error { source: Box, }, + #[error("{message}")] + AnyhowContext { + message: String, + #[source] + source: anyhow::Error, + }, + #[cfg(feature = "docker")] #[error("Failed to connect to Docker daemon")] DockerConnect { @@ -68,6 +75,13 @@ impl Error { } } + pub fn context_anyhow(message: impl Into, source: anyhow::Error) -> Self { + Self::AnyhowContext { + message: message.into(), + source, + } + } + pub fn exec(label: impl Into, result: ExecResult) -> Self { Self::Exec { label: label.into(), diff --git a/lib/components/fabro-sandbox/src/git_retry.rs b/lib/components/fabro-sandbox/src/git_retry.rs new file mode 100644 index 000000000..7b16cc256 --- /dev/null +++ b/lib/components/fabro-sandbox/src/git_retry.rs @@ -0,0 +1,762 @@ +//! Retry for git operations against GitHub from clone-based sandboxes. +//! +//! Clone-based providers can mint a GitHub App installation token and use it +//! immediately. GitHub can reject that first operation before the token is +//! available to the git endpoint. On a private repository, the rejection can +//! arrive as `Repository not found.` or an authentication failure. +//! +//! Only a token minted recently makes those messages safe to retry. Static +//! PATs and pre-minted installation tokens fail fast; a mature App token can +//! still hit a service-side blip that presents the same surface, so it +//! retries as transient infrastructure. +//! +//! Retries reuse the same token on purpose. Replication of a given token only +//! makes progress, so each attempt strictly improves the odds, while +//! re-minting would restart the replication clock. + +use std::future::Future; +use std::time::Duration; + +use chrono::Utc; +use fabro_github::token_source::TokenSnapshot; +#[cfg(test)] +use fabro_github::token_source::{REFRESH_MARGIN, TokenProvenance}; +use fabro_types::SandboxProviderKind; +pub use fabro_types::run_event::GitPushRetryReason as GitRetryReason; +use fabro_util::backoff::BackoffPolicy; +use tokio::time; + +/// How long after its mint a token is presumed to still be replicating to +/// GitHub's git endpoints. Matches the observed scale of the lag (seconds, +/// occasionally tens of seconds). +pub(crate) const REPLICATION_HORIZON: Duration = Duration::from_mins(1); + +/// What a git failure message tells us about retry safety. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum GitMessageClass { + Retry(GitRetryReason), + Permanent, + Unknown, +} + +impl GitMessageClass { + pub(crate) fn retry_reason(self) -> Option { + match self { + Self::Retry(reason) => Some(reason), + Self::Permanent | Self::Unknown => None, + } + } +} + +/// What the operation's credentials say about retrying auth-shaped failures. +/// +/// Derived from the [`TokenSnapshot`] of the token embedded for the attempt, +/// so classification reads provenance as data instead of threading booleans +/// through call stacks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CredentialContext { + /// An installation token younger than [`REPLICATION_HORIZON`] — a 404 or + /// auth failure is likely replication lag; retry with the same token. + FreshApp, + /// An installation token older than the horizon. A 404 with it is + /// indistinguishable from a service-side blip at this layer, so it stays + /// transient rather than proving access loss. + MatureApp, + /// A PAT or pre-minted token — it cannot become valid by waiting. + Static, + /// No credentials at all. + None, +} + +impl CredentialContext { + #[must_use] + pub fn from_snapshot(snapshot: Option<&TokenSnapshot>) -> Self { + match snapshot { + None => Self::None, + Some(snapshot) => match snapshot.age_at(Utc::now()) { + None => Self::Static, + Some(age) if age < REPLICATION_HORIZON => Self::FreshApp, + Some(_) => Self::MatureApp, + }, + } + } +} + +/// Message fragments that mean the operation failed on infrastructure. +/// +/// These are safe to retry whether or not the operation was authenticated. +const TRANSIENT_HINTS: &[&str] = &[ + "could not resolve host", + "temporary failure in name resolution", + "connection refused", + "connection reset", + "connection timed out", + "timed out", + "network is unreachable", + "no route to host", + "tls handshake", + "early eof", + "rpc failed", + "unexpected disconnect", + "the remote end hung up unexpectedly", + "index-pack failed", + "service unavailable", + "gateway timeout", + "too many requests", + "rate limit", +]; + +/// Message fragments GitHub uses when a token is not yet visible. +/// +/// Only meaningful when the operation carried credentials. The same lag +/// surfaces as 404 or as an auth failure depending on which endpoint answers +/// first. +const TOKEN_REPLICATION_HINTS: &[&str] = &[ + "repository not found", + "authentication failed", + "invalid username or password", + "bad credentials", + // git CLI over HTTP. + "the requested url returned error: 401", + "the requested url returned error: 403", + "the requested url returned error: 404", + // libgit2 (the run-metadata writer pushes through git2). + "unexpected http status code: 401", + "unexpected http status code: 403", + "unexpected http status code: 404", +]; + +/// Whether a failure message has the 404/auth-failure shape GitHub produces +/// for both token-replication lag and a drifted or missing embedded token. +pub(crate) fn matches_auth_failure_hints(message: &str) -> bool { + let lower = message.to_ascii_lowercase(); + TOKEN_REPLICATION_HINTS + .iter() + .any(|hint| lower.contains(hint)) +} + +pub(crate) fn output_matches_auth_failure_hints(stderr: &str, stdout: &str) -> bool { + matches_auth_failure_hints(stderr) || matches_auth_failure_hints(stdout) +} + +/// Classify a failed git operation by its rendered message. +/// +/// `cred` gates the reading of 404/auth-failure messages: a fresh App token +/// retries as replication lag, a mature one as transient infrastructure, and +/// a static credential (or none) fails fast because waiting cannot make it +/// valid. +pub(crate) fn classify_message(message: &str, cred: CredentialContext) -> GitMessageClass { + let lower = message.to_ascii_lowercase(); + + if TRANSIENT_HINTS.iter().any(|hint| lower.contains(hint)) { + return GitMessageClass::Retry(GitRetryReason::TransientInfra); + } + if TOKEN_REPLICATION_HINTS + .iter() + .any(|hint| lower.contains(hint)) + { + return match cred { + CredentialContext::FreshApp => GitMessageClass::Retry(GitRetryReason::TokenReplication), + CredentialContext::MatureApp => GitMessageClass::Retry(GitRetryReason::TransientInfra), + CredentialContext::Static | CredentialContext::None => GitMessageClass::Permanent, + }; + } + let permanent = lower.contains("could not read username") + || lower.contains("terminal prompts disabled") + || lower.contains("permission denied") + || (lower.contains("permission to") && lower.contains("denied")) + || (lower.contains("destination path") && lower.contains("already exists")) + || (lower.contains("remote branch") && lower.contains("not found")); + if permanent { + return GitMessageClass::Permanent; + } + GitMessageClass::Unknown +} + +pub(crate) fn classify_output( + stderr: &str, + stdout: &str, + cred: CredentialContext, +) -> GitMessageClass { + let by_stderr = classify_message(stderr, cred); + if by_stderr == GitMessageClass::Unknown { + classify_message(stdout, cred) + } else { + by_stderr + } +} + +/// Classify a rendered git failure message, returning the retry reason when +/// the failure is transient for these credentials. `None` means the failure +/// is permanent or unrecognized. +#[must_use] +pub fn classify_failure(message: &str, cred: CredentialContext) -> Option { + classify_message(message, cred).retry_reason() +} + +/// Backoff between attempts: 3s, then 9s. +/// +/// GitHub's guidance for token replication is to wait a few seconds and retry +/// with the same token. Sub-second delays land inside the same replication +/// window and spend an attempt for nothing. +fn clone_backoff() -> BackoffPolicy { + BackoffPolicy { + initial_delay: Duration::from_secs(3), + factor: 3.0, + max_delay: Duration::from_secs(10), + jitter: false, + } +} + +/// Attempt and time bounds for one retried git operation. +/// +/// All bounds are optional so existing behaviors are expressible unchanged. +/// The effective deadline is the minimum of the bounds that are present +/// (`start + max_elapsed`, `outer_deadline`); each attempt runs with +/// `min(per_attempt_timeout, remaining)` over the caps that are present, and +/// no attempt or backoff starts past the effective deadline. +#[derive(Debug, Clone)] +pub struct RetryPlan { + /// Total attempts, including the first. + pub max_attempts: u32, + pub backoff: BackoffPolicy, + /// Wall clock for this whole operation. + pub max_elapsed: Option, + /// Cap for any single attempt. + pub per_attempt_timeout: Option, + /// Caller-supplied absolute bound. + pub outer_deadline: Option, +} + +impl RetryPlan { + /// The clone policy both providers already trust: 3 attempts, 3s/9s + /// backoff, no plan-level bounds. Docker supplies its existing absolute + /// five-minute deadline through `outer_deadline`; Daytona supplies none. + #[must_use] + pub fn clone_default(outer_deadline: Option) -> Self { + Self { + max_attempts: 3, + backoff: clone_backoff(), + max_elapsed: None, + per_attempt_timeout: None, + outer_deadline, + } + } + + /// Checkpoint pushes stay cheap: the next checkpoint re-pushes the same + /// branch anyway. Worst case ~90 seconds of wall clock. + #[must_use] + pub fn checkpoint_push() -> Self { + Self { + max_attempts: 3, + backoff: clone_backoff(), + max_elapsed: Some(Duration::from_secs(90)), + per_attempt_timeout: Some(Duration::from_mins(1)), + outer_deadline: None, + } + } + + /// The terminal publish push guards the whole run's value, so it gets a + /// real budget: 5 attempts with growing backoff (~3s/10s/33s/60s), + /// bounded at 4 minutes of wall clock. The 4-minute bound must stay + /// under the token source's `REFRESH_MARGIN` (see the margin-invariant + /// test) so a pinned token always outlives the operation. + #[must_use] + pub fn publish_push() -> Self { + Self { + max_attempts: 5, + backoff: BackoffPolicy { + initial_delay: Duration::from_secs(3), + factor: 10.0 / 3.0, + max_delay: Duration::from_mins(1), + jitter: false, + }, + max_elapsed: Some(Duration::from_mins(4)), + per_attempt_timeout: Some(Duration::from_mins(1)), + outer_deadline: None, + } + } + + /// The absolute deadline this operation must finish by, if any bound is + /// present. + pub(crate) fn effective_deadline(&self, start: time::Instant) -> Option { + let elapsed_deadline = self.max_elapsed.map(|max| start + max); + match (elapsed_deadline, self.outer_deadline) { + (Some(a), Some(b)) => Some(a.min(b)), + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + (None, None) => None, + } + } + + /// Time cap for an attempt starting now: the per-attempt cap bounded by + /// the time remaining before the effective deadline. + pub(crate) fn attempt_timeout(&self, deadline: Option) -> Option { + let remaining = deadline.map(|d| d.saturating_duration_since(time::Instant::now())); + match (self.per_attempt_timeout, remaining) { + (Some(cap), Some(remaining)) => Some(cap.min(remaining)), + (Some(cap), None) => Some(cap), + (None, remaining) => remaining, + } + } + + pub(crate) fn retry_delay( + &self, + attempt_number: u32, + deadline: Option, + ) -> Option { + let delay = self.backoff.delay_for_attempt(attempt_number); + if deadline.is_some_and(|deadline| { + delay >= deadline.saturating_duration_since(time::Instant::now()) + }) { + None + } else { + Some(delay) + } + } +} + +/// Run a clone operation, repeating it while the failure looks transient. +/// +/// `attempt` receives the 1-based attempt number. `classify` decides whether +/// an error is worth repeating; `None` returns it to the caller untouched. +/// A retry starts only when its backoff fits before the plan's effective +/// deadline. The final error is returned as-is. +pub(crate) async fn retry_clone( + provider: SandboxProviderKind, + op: &str, + plan: &RetryPlan, + mut attempt: Attempt, + classify: Classify, +) -> Result +where + Attempt: FnMut(u32) -> Fut, + Fut: Future>, + Classify: Fn(&E) -> Option, +{ + let deadline = plan.effective_deadline(time::Instant::now()); + + for attempt_number in 1..plan.max_attempts.max(1) { + match attempt(attempt_number).await { + Ok(value) => return Ok(value), + Err(err) => { + let Some(reason) = classify(&err) else { + return Err(err); + }; + let Some(delay) = plan.retry_delay(attempt_number, deadline) else { + return Err(err); + }; + // The failure text can carry git stderr, so log the category + // rather than the message. The caller still reports the full + // error if the attempts run out. + tracing::warn!( + provider = %provider, + op, + attempt = attempt_number, + max_attempts = plan.max_attempts, + reason = %reason, + delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), + "Git operation failed, retrying" + ); + time::sleep(delay).await; + } + } + } + + attempt(plan.max_attempts.max(1)).await +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use super::*; + + /// Records the attempt numbers a closure was called with. + #[derive(Default)] + struct Attempts(Mutex>); + + impl Attempts { + fn record(&self, attempt: u32) { + self.0.lock().expect("attempt log mutex").push(attempt); + } + + fn recorded(&self) -> Vec { + self.0.lock().expect("attempt log mutex").clone() + } + } + + /// A classifier that treats every failure as worth repeating. + const ALWAYS_RETRY: fn(&String) -> Option = + |_| Some(GitRetryReason::TokenReplication); + + fn fresh_snapshot(age: Duration, ttl: Duration) -> TokenSnapshot { + let now = Utc::now(); + TokenSnapshot { + generation: 1, + provenance: TokenProvenance::Minted { + minted_at: now - chrono::Duration::from_std(age).unwrap(), + expires_at: now + chrono::Duration::from_std(ttl).unwrap(), + }, + } + } + + #[test] + fn credential_context_reads_token_age_from_provenance() { + assert_eq!( + CredentialContext::from_snapshot(None), + CredentialContext::None + ); + assert_eq!( + CredentialContext::from_snapshot(Some(&TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + })), + CredentialContext::Static + ); + assert_eq!( + CredentialContext::from_snapshot(Some(&fresh_snapshot( + Duration::from_secs(5), + Duration::from_hours(1) + ))), + CredentialContext::FreshApp + ); + assert_eq!( + CredentialContext::from_snapshot(Some(&fresh_snapshot( + Duration::from_mins(2), + Duration::from_hours(1) + ))), + CredentialContext::MatureApp + ); + } + + #[test] + fn private_repo_not_found_with_a_fresh_token_is_a_replication_lag() { + assert_eq!( + classify_message( + "repository not found: Repository not found.", + CredentialContext::FreshApp + ), + GitMessageClass::Retry(GitRetryReason::TokenReplication) + ); + } + + #[test] + fn not_found_with_a_mature_token_is_transient_not_permanent() { + // A service-side blip is indistinguishable from access loss at this + // layer, so a mature-App 404 stays retryable. + assert_eq!( + classify_message( + "repository not found: Repository not found.", + CredentialContext::MatureApp + ), + GitMessageClass::Retry(GitRetryReason::TransientInfra) + ); + } + + #[test] + fn not_found_with_static_or_no_credentials_is_permanent() { + for cred in [CredentialContext::Static, CredentialContext::None] { + assert_eq!( + classify_message("repository not found: Repository not found.", cred), + GitMessageClass::Permanent, + "{cred:?} cannot become valid by waiting" + ); + } + } + + #[test] + fn auth_failure_classification_follows_the_credential_context() { + let message = "fatal: Authentication failed for 'https://github.com/owner/repo'"; + assert_eq!( + classify_message(message, CredentialContext::FreshApp), + GitMessageClass::Retry(GitRetryReason::TokenReplication) + ); + assert_eq!( + classify_message(message, CredentialContext::MatureApp), + GitMessageClass::Retry(GitRetryReason::TransientInfra) + ); + assert_eq!( + classify_message(message, CredentialContext::Static), + GitMessageClass::Permanent + ); + } + + #[test] + fn infra_failures_retry_without_credentials() { + for message in [ + "fatal: unable to access: Could not resolve host: github.com", + "error: RPC failed; curl 56 recv failure", + "fatal: early EOF", + "Operation timed out", + ] { + assert_eq!( + classify_message(message, CredentialContext::None), + GitMessageClass::Retry(GitRetryReason::TransientInfra), + "expected {message:?} to be transient" + ); + } + } + + #[test] + fn genuine_failures_are_not_retried() { + for message in [ + "fatal: could not read Username for 'https://github.com'", + "remote: Permission to owner/repo.git denied", + "fatal: destination path 'repo' already exists", + ] { + assert_eq!( + classify_message(message, CredentialContext::FreshApp), + GitMessageClass::Permanent, + "expected {message:?} to fail fast" + ); + } + } + + #[test] + fn unrecognized_failures_remain_unknown() { + assert_eq!( + classify_message( + "git operation stopped for an unexpected reason", + CredentialContext::FreshApp + ), + GitMessageClass::Unknown + ); + } + + #[test] + fn backoff_waits_seconds_not_milliseconds() { + let plan = RetryPlan::clone_default(None); + assert_eq!(plan.backoff.delay_for_attempt(1), Duration::from_secs(3)); + assert_eq!(plan.backoff.delay_for_attempt(2), Duration::from_secs(9)); + } + + #[test] + fn publish_backoff_grows_toward_a_one_minute_cap() { + let plan = RetryPlan::publish_push(); + assert_eq!(plan.backoff.delay_for_attempt(1), Duration::from_secs(3)); + assert_eq!(plan.backoff.delay_for_attempt(2), Duration::from_secs(10)); + assert!(plan.backoff.delay_for_attempt(3) < Duration::from_secs(35)); + assert_eq!(plan.backoff.delay_for_attempt(4), Duration::from_mins(1)); + } + + /// `REFRESH_MARGIN` must exceed every push plan's `max_elapsed`: a push + /// pins the token of its single successful resolve, and any token the + /// source returns has at least the margin of validity left, so the pinned + /// token must outlive the whole operation. + #[test] + fn refresh_margin_exceeds_every_push_plan_elapsed_bound() { + for plan in [RetryPlan::checkpoint_push(), RetryPlan::publish_push()] { + let max_elapsed = plan.max_elapsed.expect("push plans bound elapsed time"); + assert!( + REFRESH_MARGIN > max_elapsed, + "margin invariant violated: {max_elapsed:?}" + ); + } + } + + #[test] + fn effective_deadline_takes_the_minimum_of_present_bounds() { + let start = time::Instant::now(); + let outer = start + Duration::from_secs(30); + + let unbounded = RetryPlan::clone_default(None); + assert_eq!(unbounded.effective_deadline(start), None); + + let outer_only = RetryPlan::clone_default(Some(outer)); + assert_eq!(outer_only.effective_deadline(start), Some(outer)); + + let mut both = RetryPlan::checkpoint_push(); + both.outer_deadline = Some(outer); + assert_eq!(both.effective_deadline(start), Some(outer)); + + both.outer_deadline = Some(start + Duration::from_mins(10)); + assert_eq!( + both.effective_deadline(start), + Some(start + Duration::from_secs(90)) + ); + } + + #[tokio::test(start_paused = true)] + async fn attempt_timeout_is_capped_by_the_remaining_deadline() { + let plan = RetryPlan::checkpoint_push(); + let deadline = Some(time::Instant::now() + Duration::from_secs(20)); + assert_eq!( + plan.attempt_timeout(deadline), + Some(Duration::from_secs(20)) + ); + assert_eq!(plan.attempt_timeout(None), Some(Duration::from_mins(1))); + + let unbounded = RetryPlan::clone_default(None); + assert_eq!(unbounded.attempt_timeout(None), None); + } + + #[tokio::test(start_paused = true)] + async fn first_success_runs_one_attempt() { + let attempts = Attempts::default(); + + let result = retry_clone( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Ok::<_, String>(attempt) } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!(result, Ok(1)); + assert_eq!(attempts.recorded(), vec![1]); + } + + #[tokio::test(start_paused = true)] + async fn retries_until_a_later_attempt_succeeds() { + let attempts = Attempts::default(); + + let result = retry_clone( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { + if attempt < 3 { + Err("Repository not found.".to_string()) + } else { + Ok(attempt) + } + } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!(result, Ok(3)); + assert_eq!(attempts.recorded(), vec![1, 2, 3]); + } + + #[tokio::test(start_paused = true)] + async fn exhausted_attempts_return_the_final_error() { + let attempts = Attempts::default(); + + let result = retry_clone( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>(format!("Repository not found. (attempt {attempt})")) } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!( + result, + Err("Repository not found. (attempt 3)".to_string()), + "the caller should see the last failure, not the first" + ); + assert_eq!(attempts.recorded(), vec![1, 2, 3]); + } + + #[tokio::test(start_paused = true)] + async fn unretryable_failure_stops_immediately() { + let attempts = Attempts::default(); + + let result = retry_clone( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("permission denied".to_string()) } + }, + |_: &String| None, + ) + .await; + + assert_eq!(result, Err("permission denied".to_string())); + assert_eq!( + attempts.recorded(), + vec![1], + "a deterministic failure should not wait out the backoff" + ); + } + + /// Docker clone parity: the caller's absolute deadline stops retries when + /// the backoff no longer fits before it. + #[tokio::test(start_paused = true)] + async fn outer_deadline_stops_retry_when_backoff_does_not_fit() { + let attempts = Attempts::default(); + let deadline = time::Instant::now() + Duration::from_secs(2); + + let result = retry_clone( + SandboxProviderKind::Docker, + "clone", + &RetryPlan::clone_default(Some(deadline)), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("temporary failure".to_string()) } + }, + ALWAYS_RETRY, + ) + .await; + + assert_eq!(result, Err("temporary failure".to_string())); + assert_eq!(attempts.recorded(), vec![1]); + assert_eq!(time::Instant::now() + Duration::from_secs(2), deadline); + } + + /// Daytona clone parity: with no bounds at all, attempts are limited only + /// by `max_attempts` and backoff. + #[tokio::test(start_paused = true)] + async fn unbounded_plan_runs_all_attempts() { + let attempts = Attempts::default(); + + let result = retry_clone( + SandboxProviderKind::Daytona, + "clone", + &RetryPlan::clone_default(None), + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("temporary failure".to_string()) } + }, + |_: &String| Some(GitRetryReason::TransientInfra), + ) + .await; + + assert!(result.is_err()); + assert_eq!(attempts.recorded(), vec![1, 2, 3]); + } + + #[tokio::test(start_paused = true)] + async fn max_elapsed_stops_retry_when_backoff_does_not_fit() { + let attempts = Attempts::default(); + let plan = RetryPlan { + max_attempts: 5, + backoff: clone_backoff(), + max_elapsed: Some(Duration::from_secs(4)), + per_attempt_timeout: None, + outer_deadline: None, + }; + + let result = retry_clone( + SandboxProviderKind::Docker, + "push", + &plan, + |attempt| { + attempts.record(attempt); + async move { Err::<(), _>("temporary failure".to_string()) } + }, + ALWAYS_RETRY, + ) + .await; + + assert!(result.is_err()); + // Attempt 1 fails instantly, 3s backoff fits inside 4s, attempt 2 + // fails, and the 9s backoff no longer fits. + assert_eq!(attempts.recorded(), vec![1, 2]); + } +} diff --git a/lib/components/fabro-sandbox/src/lib.rs b/lib/components/fabro-sandbox/src/lib.rs index 3a5e91ab2..fb442e9fb 100644 --- a/lib/components/fabro-sandbox/src/lib.rs +++ b/lib/components/fabro-sandbox/src/lib.rs @@ -9,13 +9,13 @@ pub mod sandbox_spec; #[cfg(any(feature = "docker", feature = "daytona"))] mod clone_source; -#[cfg(any(feature = "docker", feature = "daytona", test))] -mod clone_retry; +mod git_retry; #[cfg(any(feature = "docker", feature = "daytona", test))] mod managed_labels; -#[cfg(any(feature = "docker", feature = "daytona", test))] +mod push_credentials; + pub mod redact; pub mod details; @@ -39,7 +39,11 @@ pub use details::sandbox_details; #[cfg(feature = "docker")] pub use docker::{DockerSandbox, DockerSandboxOptions}; pub use error::{Error, Result, default_redacted_output_tail, display_for_log}; +pub use fabro_github::token_source::{ + InstallationTokenSource, ResolvedToken, TokenProvenance, TokenSnapshot, +}; pub use fabro_types::{RunSandboxInstance, SandboxProviderKind}; +pub use git_retry::{CredentialContext, GitRetryReason, RetryPlan, classify_failure}; pub use local::LocalSandbox; #[cfg(feature = "daytona")] pub use provider::daytona::DaytonaSandboxProvider; @@ -49,13 +53,15 @@ pub use provider::{ LocalSandboxProvider, SandboxCreateSpec, SandboxLookupError, SandboxProvider, SandboxProviderRegistry, }; +pub use push_credentials::RefreshErrorKind; pub use reconnect::{reconnect, reconnect_for_run, reconnect_for_run_with_callback}; pub use sandbox::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, ExecStreamingRequest, ExecStreamingResult, GitRunInfo, GitSetupIntent, GrepOptions, - RefreshOutcome, Sandbox, SandboxEvent, SandboxEventCallback, SandboxFile, StderrCollector, - StdioProcess, StdioProcessHandle, StdioProcessTermination, WalkOptions, format_lines_numbered, - git_push_via_exec, redacted_output_tail, setup_git_via_exec, shell_quote, + PushAttempt, PushError, PushReport, RefreshOutcome, RemoteCredentialAction, Sandbox, + SandboxEvent, SandboxEventCallback, SandboxFile, StderrCollector, StdioProcess, + StdioProcessHandle, StdioProcessTermination, WalkOptions, format_lines_numbered, + redacted_output_tail, setup_git_via_exec, shell_quote, }; pub use sandbox_spec::SandboxSpec; pub use terminal::{TerminalSession, TerminalSize, open_terminal_for_run}; diff --git a/lib/components/fabro-sandbox/src/local.rs b/lib/components/fabro-sandbox/src/local.rs index 49737780d..650d57c47 100644 --- a/lib/components/fabro-sandbox/src/local.rs +++ b/lib/components/fabro-sandbox/src/local.rs @@ -13,8 +13,8 @@ use tokio::{fs, time}; use tokio_util::sync::CancellationToken; use crate::sandbox::{ - BASH_ENV_VAR, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, StdioProcessControl, optional_timeout, - validate_bash_probe, write_process_stdin, + self, BASH_ENV_VAR, BASH_PROBE_SCRIPT, BASH_PROBE_TIMEOUT_MS, StdioProcessControl, + optional_timeout, validate_bash_probe, write_process_stdin, }; use crate::{ CommandOutputCallback, DEFAULT_EXEC_OUTPUT_TAIL_BYTES, DirEntry, ExecResult, @@ -878,20 +878,31 @@ impl Sandbox for LocalSandbox { Ok(()) } - async fn git_push_ref(&self, refspec: &str) -> crate::Result<()> { + async fn git_push_ref( + &self, + refspec: &str, + plan: &crate::RetryPlan, + ) -> Result { let has_origin = match self .exec_command("git remote get-url origin", 10_000, None, None, None) .await { Ok(result) if result.is_success() => true, Ok(_) => false, - Err(err) => return Err(crate::Error::context("git remote get-url origin", err)), + Err(err) => { + return Err(crate::PushError { + report: crate::PushReport::default(), + error: crate::Error::context("git remote get-url origin", err), + }); + } }; if !has_origin { - return Ok(()); + return Ok(crate::PushReport::default()); } - crate::git_push_via_exec(self, refspec).await + // Local pushes use whatever credentials the host repository already + // carries; there is no managed credential state to lease. + sandbox::git_push_via_exec(self, None, refspec, plan).await } async fn cleanup(&self) -> crate::Result<()> { diff --git a/lib/components/fabro-sandbox/src/provider/docker.rs b/lib/components/fabro-sandbox/src/provider/docker.rs index 75c9068c8..37337eaed 100644 --- a/lib/components/fabro-sandbox/src/provider/docker.rs +++ b/lib/components/fabro-sandbox/src/provider/docker.rs @@ -100,7 +100,7 @@ impl SandboxProvider for DockerSandboxProvider { let sandbox = DockerSandbox::new( config, - github_app, + github_app.as_ref(), run_id, clone_origin_url, clone_branch, diff --git a/lib/components/fabro-sandbox/src/push_credentials.rs b/lib/components/fabro-sandbox/src/push_credentials.rs new file mode 100644 index 000000000..d1896b120 --- /dev/null +++ b/lib/components/fabro-sandbox/src/push_credentials.rs @@ -0,0 +1,629 @@ +//! Shared push-credential state for clone-based sandbox providers. +//! +//! Docker and Daytona embed GitHub credentials into the cloned repository's +//! `origin` remote and refresh them before pushes. Both providers hold this +//! state so the compare → `set-url` → record sequence, the generation +//! tracking, and the refresh-error logging behave identically across +//! providers. The token cache itself sits below the providers, in +//! [`fabro_github::token_source::InstallationTokenSource`]. + +use std::future::Future; +use std::sync::Arc; + +use fabro_github::GitHubCredentials; +use fabro_github::token_source::{InstallationTokenSource, ResolvedToken, TokenSnapshot}; +use fabro_redact::DisplaySafeUrl; +pub use fabro_types::run_event::GitCredentialRefreshError as RefreshErrorKind; +use tokio::sync::{Mutex, MutexGuard}; + +use crate::redact; +use crate::sandbox::{RefreshOutcome, RemoteCredentialAction}; + +/// Build the shared installation-token source for a clone-based sandbox. +/// +/// Returns `None` when there are no managed credentials or no GitHub origin +/// to scope them to. Minted tokens carry the same `contents: write` +/// permission the clone token uses. +pub(crate) fn build_token_source( + github_app: Option<&GitHubCredentials>, + clone_origin_url: Option<&str>, +) -> crate::Result>> { + let Some(creds) = github_app else { + return Ok(None); + }; + let Some(origin_url) = clone_origin_url.filter(|url| !url.trim().is_empty()) else { + return Ok(None); + }; + let normalized = fabro_github::normalize_repo_origin_url(origin_url); + let Ok((owner, repo)) = fabro_github::parse_github_owner_repo(&normalized) else { + // Non-GitHub origins never clone in these providers, so there is no + // remote to keep credentials fresh for. + return Ok(None); + }; + InstallationTokenSource::for_repository( + creds, + owner, + repo, + serde_json::json!({ "contents": "write" }), + ) + .map(Some) + .map_err(|err| crate::Error::context_anyhow("Failed to build GitHub token source", err)) +} + +/// Push-credential state one provider instance tracks for its `origin` +/// remote. +pub(crate) struct PushCredentialState { + source: Option>, + /// Serializes compare → `set-url` → record. The token source's + /// single-flight ends before the sandbox exec, so without this lock a + /// refresh-ahead tick and a push could both see the old embedded + /// generation and race on `.git/config.lock`. Holds the last + /// successfully embedded token: its secret is already in the remote URL + /// inside the sandbox, so retaining it adds no exposure, and it is what + /// a push falls back to when a refresh fails. The tracked value is local + /// belief, not ground truth — agent code inside the sandbox can rewrite + /// `origin`. + embedded: Mutex>, +} + +impl PushCredentialState { + pub(crate) fn new(source: Option>) -> Self { + Self { + source, + embedded: Mutex::new(None), + } + } + + pub(crate) fn source(&self) -> Option<&Arc> { + self.source.as_ref() + } + + /// Record the token embedded in `origin` outside the refresh path — the + /// clone is the first operation to embed a token, and it seeds this + /// state so the first refresh compares against the clone token instead + /// of believing nothing was ever embedded. + pub(crate) async fn record_embedded(&self, token: ResolvedToken) { + *self.embedded.lock().await = Some(token); + } + + /// Refresh the credentials embedded in `origin`. + /// + /// Resolves through the shared source, skips the `set-url` exec when the + /// resolved generation is already embedded, and records the new + /// generation only after `set_url` succeeds. `set_url` receives the + /// authenticated URL to embed and runs under the embed lock. + pub(crate) async fn refresh( + &self, + origin_url: &str, + set_url: F, + ) -> crate::Result + where + F: FnOnce(DisplaySafeUrl) -> Fut, + Fut: Future>, + { + let Some(source) = &self.source else { + return Ok(RefreshOutcome::none()); + }; + let mut embedded = self.embedded.lock().await; + let resolved = match source.resolve().await { + Ok(resolved) => resolved, + Err(err) => { + // The refresh-error path is defined, not incidental: the push + // proceeds with the last embedded token, so log which one + // that is instead of losing the credential state. + if let Some(prev) = embedded.as_ref() { + tracing::warn!( + error = %format!("{err:#}"), + generation = prev.snapshot.generation, + provenance = %prev.snapshot.provenance, + token_age_ms = prev.snapshot.age_ms(), + "GitHub token refresh failed; origin keeps the last embedded credentials" + ); + } else { + tracing::warn!( + error = %format!("{err:#}"), + "GitHub token refresh failed and no credentials were ever embedded" + ); + } + return Err(crate::Error::context_anyhow( + "Failed to refresh push credentials", + err, + )); + } + }; + if embedded + .as_ref() + .is_some_and(|prev| prev.snapshot.generation == resolved.snapshot.generation) + { + return Ok(RefreshOutcome::unchanged(resolved.snapshot)); + } + let auth_url = fabro_github::embed_token_in_url(origin_url, resolved.token.expose()) + .map_err(|err| { + crate::Error::context_anyhow("Failed to build authenticated origin URL", err) + })?; + set_url(auth_url).await?; + let snapshot = resolved.snapshot; + *embedded = Some(resolved); + Ok(RefreshOutcome::embedded(snapshot)) + } +} + +/// What [`CredentialLease::ensure_embedded`] did for one push attempt. +#[derive(Debug, Clone, Copy)] +pub(crate) struct EnsureOutcome { + pub action: RemoteCredentialAction, + /// The token embedded in the remote right now — never an unembedded mint. + pub token: Option, + pub refresh_error: Option, +} + +/// Scoped pin of push credentials for one push operation. +/// +/// Holds the provider's embed mutex until dropped, so no other refresh can +/// re-embed mid-operation — a refresh-ahead tick crossing the cache margin +/// during a retrying push waits here instead of swapping the remote out from +/// under the pin. Internally retains up to two secrets: the last successfully +/// embedded token (the fallback) and the operation's resolved target, so both +/// drift re-embedding and the refresh-error fallback work. Only non-secret +/// snapshots leave the lease. +/// +/// A successful resolve happens at most once per operation and is never +/// replaced; the pin transitions to the target only through a successful +/// embed. The token source's refresh margin exceeds every push plan's elapsed +/// bound, so the pinned token always outlives the operation. +pub(crate) struct CredentialLease<'a> { + source: Option<&'a InstallationTokenSource>, + /// Embed-mutex guard: the last successfully embedded token. + embedded: MutexGuard<'a, Option>, + /// The operation's resolved target, including a cached fallback when a + /// refresh mint failed. + target: Option, + /// Skip an immediate duplicate resolve after lease acquisition already + /// failed. A later push attempt can retry after backoff. + defer_resolve_once: bool, +} + +impl PushCredentialState { + /// Acquire the push-credential lease for one push operation. + /// + /// Resolves the operation's target token up front. A failed refresh can + /// return a valid cached token; the first attempt uses it, and + /// [`CredentialLease::ensure_embedded`] retries the refresh after push + /// backoff. A resolve with no cached or embedded token fails acquisition. + pub(crate) async fn lease(&self) -> crate::Result> { + let embedded = self.embedded.lock().await; + let Some(source) = self.source.as_deref() else { + return Ok(CredentialLease { + source: None, + embedded, + target: None, + defer_resolve_once: false, + }); + }; + match source.resolve().await { + Ok(resolved) => { + let defer_resolve_once = resolved.refresh_failed; + Ok(CredentialLease { + source: Some(source), + embedded, + target: Some(resolved), + defer_resolve_once, + }) + } + Err(err) => { + if let Some(prev) = embedded.as_ref() { + tracing::warn!( + error = %format!("{err:#}"), + generation = prev.snapshot.generation, + provenance = %prev.snapshot.provenance, + token_age_ms = prev.snapshot.age_ms(), + "token resolve failed; push pins the last embedded credentials" + ); + Ok(CredentialLease { + source: Some(source), + embedded, + target: None, + defer_resolve_once: true, + }) + } else { + tracing::warn!( + error = %format!("{err:#}"), + "token resolve failed and no credentials were ever embedded" + ); + Err(crate::Error::message( + "Failed to refresh push credentials: token_mint_failed", + )) + } + } + } + } +} + +impl CredentialLease<'_> { + /// Non-secret description of the token embedded in the remote right now. + pub(crate) fn snapshot(&self) -> Option { + self.embedded.as_ref().map(|token| token.snapshot) + } + + /// Embed the pinned generation if the remote does not carry it. + /// + /// One call covers the initial embed, a deferred embed after an earlier + /// failure, and drift repair (`force` re-embeds even when the tracked + /// generation matches, for remotes rewritten inside the sandbox). While + /// the lease has no target, this retries the failed `resolve()` first — + /// retrying a failed resolve discards no fresh token, so it cannot + /// restart any replication clock. Refresh failures are recorded, never + /// propagated: the push proceeds with the last embedded token. + pub(crate) async fn ensure_embedded( + &mut self, + sandbox: &dyn crate::Sandbox, + origin_url: &str, + force: bool, + ) -> crate::Result { + let Some(source) = self.source else { + return Ok(EnsureOutcome { + action: RemoteCredentialAction::None, + token: None, + refresh_error: None, + }); + }; + let mut refresh_error = self.defer_resolve_once.then_some(RefreshErrorKind::Mint); + if self.defer_resolve_once { + self.defer_resolve_once = false; + } else if self + .target + .as_ref() + .is_none_or(|resolved| resolved.refresh_failed) + { + match source.resolve().await { + Ok(resolved) => { + refresh_error = resolved.refresh_failed.then_some(RefreshErrorKind::Mint); + self.target = Some(resolved); + } + Err(err) => { + tracing::warn!( + error = %format!("{err:#}"), + "token resolve retry failed; pushing with the last embedded token" + ); + refresh_error = Some(RefreshErrorKind::Mint); + } + } + } + let Some(desired) = self.target.as_ref().or(self.embedded.as_ref()).cloned() else { + // Managed credentials with nothing resolved or embedded: + // acquisition fails before any attempt runs, so pushes never see + // this state. + return Ok(EnsureOutcome { + action: RemoteCredentialAction::None, + token: None, + refresh_error, + }); + }; + let embedded_generation = self + .embedded + .as_ref() + .map(|token| token.snapshot.generation); + if !force && embedded_generation == Some(desired.snapshot.generation) { + return Ok(EnsureOutcome { + action: RemoteCredentialAction::Unchanged, + token: Some(desired.snapshot), + refresh_error, + }); + } + match set_url_via_exec(sandbox, origin_url, &desired).await { + Ok(()) => { + let snapshot = desired.snapshot; + *self.embedded = Some(desired); + Ok(EnsureOutcome { + action: RemoteCredentialAction::Embedded, + token: Some(snapshot), + refresh_error, + }) + } + Err(err) => { + if matches!( + &err, + crate::Error::Exec { result, .. } + if result.termination != fabro_types::CommandTermination::Exited + ) { + return Err(err); + } + tracing::warn!( + error = %crate::display_for_log(&err), + "embedding push credentials in origin failed; pushing with the last embedded token" + ); + Ok(EnsureOutcome { + action: RemoteCredentialAction::Unchanged, + token: self.snapshot(), + refresh_error: Some(RefreshErrorKind::SetUrl), + }) + } + } + } +} + +/// Rewrite `origin` with the token embedded, through the sandbox's uniform +/// exec surface. +async fn set_url_via_exec( + sandbox: &dyn crate::Sandbox, + origin_url: &str, + token: &ResolvedToken, +) -> crate::Result<()> { + let auth_url = + fabro_github::embed_token_in_url(origin_url, token.token.expose()).map_err(|err| { + crate::Error::context( + "Failed to build authenticated origin URL", + RedactedSetUrlError(fabro_redact::redact_string(&format!("{err:#}"))), + ) + })?; + set_auth_url_via_exec(sandbox, auth_url).await +} + +pub(crate) async fn set_auth_url_via_exec( + sandbox: &dyn crate::Sandbox, + auth_url: DisplaySafeUrl, +) -> crate::Result<()> { + let command = format!( + "git -c maintenance.auto=0 remote set-url origin {}", + crate::shell_quote(auth_url.as_raw_url().as_str()) + ); + let result = sandbox + .exec_command(&command, 10_000, None, None, None) + .await + .map_err(|err| { + let message = redact::redact_auth_url(&crate::display_for_log(&err), Some(&auth_url)); + crate::Error::context( + "Failed to refresh push credentials: set_url_exec_failed", + RedactedSetUrlError(message), + ) + })?; + if !result.is_success() { + return Err(result.into_exec_error_with_redactor( + "git remote set-url origin (refresh push credentials)", + |s| redact::redact_auth_url(s, Some(&auth_url)), + )); + } + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +struct RedactedSetUrlError(String); + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use chrono::Utc; + use fabro_github::InstallationToken; + use fabro_github::test_support::{InstallationTokenMinter, installation_token_source}; + use tokio::time::sleep; + + use super::*; + use crate::sandbox::RemoteCredentialAction; + + struct FixedMinter { + calls: AtomicUsize, + ttl: chrono::Duration, + } + + #[async_trait::async_trait] + impl InstallationTokenMinter for FixedMinter { + async fn mint(&self) -> anyhow::Result { + let call = self.calls.fetch_add(1, Ordering::SeqCst) + 1; + Ok(InstallationToken { + token: format!("ghs_gen{call}"), + expires_at: Utc::now() + self.ttl, + }) + } + } + + struct FailingMinter; + + #[async_trait::async_trait] + impl InstallationTokenMinter for FailingMinter { + async fn mint(&self) -> anyhow::Result { + Err(anyhow::anyhow!("mint failed")) + } + } + + fn minting_state(ttl: chrono::Duration) -> PushCredentialState { + PushCredentialState::new(Some(installation_token_source( + "owner/repo", + Arc::new(FixedMinter { + calls: AtomicUsize::new(0), + ttl, + }), + ))) + } + + const ORIGIN: &str = "https://github.com/owner/repo"; + /// Long enough for a blocked task to be observably pending on paused time. + const SHORT_WAIT: std::time::Duration = std::time::Duration::from_secs(5); + + /// A refresh-ahead tick crossing the cache margin during a push waits on + /// the embed mutex until the operation releases the lease, so the remote + /// can never be swapped out from under the pinned generation. + #[tokio::test(start_paused = true)] + async fn refresh_waits_for_the_lease_to_release() { + let state = std::sync::Arc::new(minting_state(chrono::Duration::minutes(60))); + + let lease = state.lease().await.expect("lease acquires"); + + let refresh_task = { + let state = std::sync::Arc::clone(&state); + tokio::spawn(async move { + state + .refresh(ORIGIN, |_| async { Ok(()) }) + .await + .expect("refresh succeeds after the lease releases") + }) + }; + + // The refresh must be blocked while the lease holds the embed mutex. + sleep(SHORT_WAIT).await; + assert!( + !refresh_task.is_finished(), + "refresh must wait on the embed mutex" + ); + + drop(lease); + let outcome = refresh_task.await.expect("refresh task completes"); + // The lease's resolve minted generation 1; the deferred refresh + // reuses it (the operation never embedded, so the refresh embeds). + assert_eq!(outcome.token().unwrap().generation, 1); + } + + #[tokio::test] + async fn refresh_without_managed_credentials_reports_none() { + let state = PushCredentialState::new(None); + let outcome = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap(); + assert_eq!(outcome, RefreshOutcome::none()); + } + + #[tokio::test] + async fn refresh_embeds_a_new_generation_and_skips_matching_ones() { + let state = minting_state(chrono::Duration::minutes(60)); + let set_url_calls = AtomicUsize::new(0); + + let first = state + .refresh(ORIGIN, |auth_url| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + assert!(auth_url.as_raw_url().as_str().contains("ghs_gen1")); + async { Ok(()) } + }) + .await + .unwrap(); + assert_eq!(first.action(), RemoteCredentialAction::Embedded); + assert_eq!(first.token().unwrap().generation, 1); + + // The cached token is fresh, so the second refresh must skip set-url. + let second = state + .refresh(ORIGIN, |_| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + async { Ok(()) } + }) + .await + .unwrap(); + assert_eq!(second.action(), RemoteCredentialAction::Unchanged); + assert_eq!(second.token().unwrap().generation, 1); + assert_eq!(set_url_calls.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn refresh_embeds_again_when_the_source_mints_a_new_generation() { + // Tokens expire inside the margin, so every resolve re-mints. + let state = minting_state(chrono::Duration::minutes(5)); + let set_url_calls = AtomicUsize::new(0); + + let first = state + .refresh(ORIGIN, |_| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + async { Ok(()) } + }) + .await + .unwrap(); + let second = state + .refresh(ORIGIN, |_| { + set_url_calls.fetch_add(1, Ordering::SeqCst); + async { Ok(()) } + }) + .await + .unwrap(); + + assert_eq!(first.token().unwrap().generation, 1); + assert_eq!(second.action(), RemoteCredentialAction::Embedded); + assert_eq!(second.token().unwrap().generation, 2); + assert_eq!(set_url_calls.load(Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn clone_seed_makes_the_first_refresh_a_no_op() { + let state = minting_state(chrono::Duration::minutes(60)); + let clone_token = state.source().unwrap().mint_for_clone().await.unwrap(); + state.record_embedded(clone_token).await; + + let outcome = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap(); + assert_eq!(outcome.action(), RemoteCredentialAction::Unchanged); + assert_eq!(outcome.token().unwrap().generation, 1); + } + + #[tokio::test] + async fn failed_set_url_does_not_record_the_new_generation() { + let state = minting_state(chrono::Duration::minutes(60)); + + let err = state + .refresh(ORIGIN, |_| async { + Err(crate::Error::message("set-url failed")) + }) + .await + .unwrap_err(); + assert!(err.to_string().contains("set-url failed")); + + // The generation was not recorded, so the retry embeds again instead + // of wrongly skipping. + let retried = state.refresh(ORIGIN, |_| async { Ok(()) }).await.unwrap(); + assert_eq!(retried.action(), RemoteCredentialAction::Embedded); + assert_eq!(retried.token().unwrap().generation, 1); + } + + #[tokio::test] + async fn static_credentials_seeded_at_clone_skip_set_url() { + let source = InstallationTokenSource::for_origin( + &GitHubCredentials::Pat("ghp_pat".to_string()), + ORIGIN, + serde_json::json!({ "contents": "write" }), + ) + .unwrap(); + let state = PushCredentialState::new(Some(source)); + let clone_token = state.source().unwrap().mint_for_clone().await.unwrap(); + state.record_embedded(clone_token).await; + + let outcome = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap(); + assert_eq!(outcome.action(), RemoteCredentialAction::Unchanged); + assert!(outcome.token().unwrap().is_static()); + } + + #[tokio::test] + async fn mint_failure_preserves_the_mint_error_chain() { + let state = PushCredentialState::new(Some(installation_token_source( + "owner/repo", + Arc::new(FailingMinter), + ))); + + let err = state + .refresh(ORIGIN, |_| async { panic!("set-url must not run") }) + .await + .unwrap_err(); + assert_eq!(err.causes(), vec![ + "minting GitHub installation access token", + "mint failed" + ]); + } + + #[test] + fn token_source_requires_managed_credentials_and_a_github_origin() { + assert!(build_token_source(None, Some(ORIGIN)).unwrap().is_none()); + let pat = GitHubCredentials::Pat("ghp_pat".to_string()); + assert!(build_token_source(Some(&pat), None).unwrap().is_none()); + assert!( + build_token_source(Some(&pat), Some("https://gitlab.com/owner/repo")) + .unwrap() + .is_none() + ); + assert!( + build_token_source(Some(&pat), Some(ORIGIN)) + .unwrap() + .is_some() + ); + } +} diff --git a/lib/components/fabro-sandbox/src/sandbox.rs b/lib/components/fabro-sandbox/src/sandbox.rs index 94c9a6a54..f9d153b2e 100644 --- a/lib/components/fabro-sandbox/src/sandbox.rs +++ b/lib/components/fabro-sandbox/src/sandbox.rs @@ -7,6 +7,8 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; +use fabro_github::token_source::{InstallationTokenSource, TokenSnapshot}; +pub use fabro_types::run_event::GitCredentialAction as RemoteCredentialAction; use fabro_types::{CommandOutputStream, CommandTermination}; use fabro_util::shell; use fabro_util::workspace_glob::WorkspaceGlob; @@ -17,6 +19,9 @@ use tokio::task::JoinHandle; use tokio::time; use tokio_util::sync::CancellationToken; +use crate::git_retry::{self, CredentialContext, GitRetryReason, RetryPlan}; +use crate::push_credentials::{CredentialLease, PushCredentialState, RefreshErrorKind}; + /// Git command prefix that disables background maintenance. pub(crate) const GIT: &str = "git -c maintenance.auto=0 -c gc.auto=0"; @@ -280,6 +285,12 @@ macro_rules! delegate_sandbox { self.$field.refresh_push_credentials().await } + fn push_token_source( + &self, + ) -> Option> { + self.$field.push_token_source() + } + async fn set_autostop_interval(&self, minutes: i32) -> $crate::Result<()> { self.$field.set_autostop_interval(minutes).await } @@ -292,8 +303,12 @@ macro_rules! delegate_sandbox { self.$field.resume_setup_commands(run_branch) } - async fn git_push_ref(&self, refspec: &str) -> $crate::Result<()> { - self.$field.git_push_ref(refspec).await + async fn git_push_ref( + &self, + refspec: &str, + plan: &$crate::RetryPlan, + ) -> Result<$crate::PushReport, $crate::PushError> { + self.$field.git_push_ref(refspec, plan).await } async fn ssh_access_command(&self) -> $crate::Result> { @@ -1013,16 +1028,52 @@ pub struct GrepOptions { pub max_results: Option, } -/// Outcome of [`Sandbox::refresh_push_credentials`]: whether a fresh token was -/// actually minted and applied to the origin remote, or the call was a no-op -/// (no clone, no authenticated origin, or no GitHub App credentials to rotate). -/// Lets callers log accurately instead of assuming every `Ok` re-minted. +/// Outcome of [`Sandbox::refresh_push_credentials`]: what this call did to the +/// remote, and the non-secret description of the token embedded in it. +/// `token` is `None` only when `action` is [`RemoteCredentialAction::None`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RefreshOutcome { - /// A fresh token was minted and the origin remote URL was updated. - Refreshed, - /// Nothing to refresh (no clone / no origin / no managed credentials). - Skipped, + /// No managed credentials exist for this sandbox. + None, + /// The remote already carried this token generation. + Unchanged(TokenSnapshot), + /// The remote was updated to carry this token generation. + Embedded(TokenSnapshot), +} + +impl RefreshOutcome { + /// No managed credentials to refresh. + #[must_use] + pub const fn none() -> Self { + Self::None + } + + #[must_use] + pub const fn unchanged(token: TokenSnapshot) -> Self { + Self::Unchanged(token) + } + + #[must_use] + pub const fn embedded(token: TokenSnapshot) -> Self { + Self::Embedded(token) + } + + #[must_use] + pub const fn action(self) -> RemoteCredentialAction { + match self { + Self::None => RemoteCredentialAction::None, + Self::Unchanged(_) => RemoteCredentialAction::Unchanged, + Self::Embedded(_) => RemoteCredentialAction::Embedded, + } + } + + #[must_use] + pub const fn token(self) -> Option { + match self { + Self::None => None, + Self::Unchanged(token) | Self::Embedded(token) => Some(token), + } + } } #[async_trait] @@ -1048,6 +1099,16 @@ pub trait Sandbox: Send + Sync { } async fn write_file(&self, path: &str, content: &str) -> crate::Result<()>; + + /// Write a file that the caller has already confirmed exists. + /// + /// Providers can override this method to skip setup that is only needed + /// when creating a new path. The default preserves the behavior of + /// [`Sandbox::write_file`]. + async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> { + self.write_file(path, content).await + } + async fn delete_file(&self, path: &str) -> crate::Result<()>; async fn file_exists(&self, path: &str) -> crate::Result; async fn list_directory( @@ -1232,11 +1293,22 @@ pub trait Sandbox: Send + Sync { } /// Refresh git push credentials (e.g. rotate an expiring GitHub App token). - /// Default is a no-op; Docker/Daytona override to update the remote URL - /// with a fresh token. Returns [`RefreshOutcome`] so callers can tell - /// an actual re-mint from a skipped no-op. + /// Default is a no-op; Docker/Daytona override to resolve a token through + /// the shared source and update the remote URL when the embedded + /// generation is stale. Returns [`RefreshOutcome`] so callers can tell + /// what happened to the remote and which token it carries. async fn refresh_push_credentials(&self) -> crate::Result { - Ok(RefreshOutcome::Skipped) + Ok(RefreshOutcome::none()) + } + + /// The shared installation-token source feeding this sandbox's push + /// credentials, when the provider manages GitHub credentials. + /// + /// Consumers outside the sandbox (e.g. the run-metadata writer) share + /// this source so every GitHub-token consumer for the origin repository + /// reuses one cached token instead of minting its own. + fn push_token_source(&self) -> Option> { + None } /// Set the auto-stop interval in minutes (0 to disable). @@ -1258,11 +1330,18 @@ pub trait Sandbox: Send + Sync { Vec::new() } - /// Push a full refspec to origin from inside the sandbox. - async fn git_push_ref(&self, _refspec: &str) -> crate::Result<()> { - Err(crate::Error::message( - "git_push_ref not implemented for this sandbox", - )) + /// Push a full refspec to origin from inside the sandbox, retrying per + /// `plan` with a pinned credential generation. Failures keep their + /// attempt history in the returned [`PushError`]. + async fn git_push_ref( + &self, + _refspec: &str, + _plan: &RetryPlan, + ) -> Result { + Err(PushError { + report: PushReport::default(), + error: crate::Error::message("git_push_ref not implemented for this sandbox"), + }) } /// Return an SSH command string for connecting to this sandbox, if @@ -1455,6 +1534,7 @@ pub async fn setup_git_via_exec( }) } +#[tracing::instrument(name = "git_op", skip_all, fields(op = "fetch"))] pub(crate) async fn fetch_source_run_ref( sandbox: &dyn Sandbox, source_run_id: &str, @@ -1501,25 +1581,936 @@ pub(crate) async fn fetch_source_run_ref( Err(crate::Error::message(last_error)) } -/// Helper for sandbox implementations that manage git internally. -/// Pushes a refspec to origin via exec_command inside the sandbox. -pub async fn git_push_via_exec(sandbox: &dyn Sandbox, refspec: &str) -> crate::Result<()> { - if let Err(e) = sandbox.refresh_push_credentials().await { - tracing::warn!( - refspec = %refspec, - error = %crate::display_for_log(&e), - "Failed to refresh push credentials before git push" - ); +/// One push attempt inside a retried push operation. Runtime detail only — +/// the durable serialized shape lives in `fabro-types` and the workflow layer +/// owns the conversion. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PushAttempt { + /// 1-based attempt number within this operation. + pub attempt: u32, + pub started_at: chrono::DateTime, + pub success: bool, + /// The classifier's verdict for a failed attempt — recorded on the + /// terminal attempt too; whether a retry actually followed is positional + /// (every entry except the last). + pub retry_reason: Option, + /// Redacted, bounded output tail; failed attempts only. + pub exec_output_tail: Option, + /// The token embedded in the remote during this attempt. + pub token: Option, + /// What `ensure_embedded` did to the remote this attempt. + pub credential_action: Option, + /// A mint or `set-url` failure this attempt pushed through. + pub refresh_error: Option, +} + +/// The attempt history of one push operation. +#[derive(Debug, Clone, Default)] +pub struct PushReport { + pub attempts: Vec, +} + +/// A failed push operation: the final typed error plus the attempt history. +/// The error type stays the safety boundary for output tails. +#[derive(Debug, thiserror::Error)] +#[error("git push failed")] +pub struct PushError { + pub report: PushReport, + #[source] + pub error: crate::Error, +} + +/// Classify a failed push attempt by the failure's rendered output. +fn classify_push_error(error: &crate::Error, cred: CredentialContext) -> Option { + let class = match error { + crate::Error::Exec { result, .. } if result.termination != CommandTermination::Exited => { + return None; + } + crate::Error::Exec { result, .. } => { + git_retry::classify_output(&result.stderr, &result.stdout, cred) + } + other => git_retry::classify_message(&crate::display_for_log(other), cred), + }; + class.retry_reason() +} + +/// Whether a failed push attempt has the 404/auth-failure shape that a +/// drifted or missing embedded token also produces. +fn push_failure_looks_auth_shaped(error: &crate::Error) -> bool { + match error { + crate::Error::Exec { result, .. } => { + git_retry::output_matches_auth_failure_hints(&result.stderr, &result.stdout) + } + other => git_retry::matches_auth_failure_hints(&crate::display_for_log(other)), } +} + +/// Helper for sandbox implementations that manage git internally. +/// +/// Pushes a refspec to origin via `exec_command` inside the sandbox, +/// retrying per `plan` with one pinned credential generation for the whole +/// operation. `credentials` is the provider's push-credential state plus the +/// origin URL; `None` pushes with whatever the remote already carries (the +/// local sandbox, or a workspace without managed credentials). +#[tracing::instrument(name = "git_op", skip_all, fields(op = "push"))] +pub(crate) async fn git_push_via_exec( + sandbox: &dyn Sandbox, + credentials: Option<(&PushCredentialState, &str)>, + refspec: &str, + plan: &RetryPlan, +) -> Result { + use CredentialContext; + use CredentialLease; + + let start = time::Instant::now(); + let deadline = plan.effective_deadline(start); + + // The lease pins one token generation and owns the embed mutex for the + // whole operation; no concurrent refresh can re-embed mid-operation, and + // no attempt can cross the refresh margin and restart the replication + // clock. + let mut lease: Option<(CredentialLease<'_>, &str)> = match credentials { + Some((state, origin_url)) => match match deadline { + Some(deadline) => match time::timeout_at(deadline, state.lease()).await { + Ok(result) => result, + Err(_) => { + return Err(push_deadline_error( + Vec::new(), + "while acquiring credentials", + )); + } + }, + None => state.lease().await, + } { + Ok(lease) => Some((lease, origin_url)), + Err(error) => { + return Err(PushError { + report: PushReport::default(), + error, + }); + } + }, + None => None, + }; + + let mut attempts: Vec = Vec::new(); + let mut force_reembed = false; + let mut drift_repaired = false; let cmd = format!("{GIT} push origin {}", shell_quote(refspec)); let label = format!("git push origin {refspec}"); - sandbox - .exec_command(&cmd, 60_000, None, None, None) + + loop { + let attempt_number = u32::try_from(attempts.len()).unwrap_or(u32::MAX) + 1; + let started_at = chrono::Utc::now(); + let attempt_timeout = plan + .attempt_timeout(deadline) + .unwrap_or(Duration::from_mins(1)); + if attempt_timeout.is_zero() { + return Err(push_deadline_error(attempts, "before the next attempt")); + } + let attempt_deadline = time::Instant::now() + attempt_timeout; + let (token, credential_action, refresh_error) = match lease.as_mut() { + Some((lease, origin_url)) => { + let ensured = match time::timeout_at( + attempt_deadline, + lease.ensure_embedded(sandbox, origin_url, force_reembed), + ) + .await + { + Ok(Ok(ensured)) => ensured, + Ok(Err(error)) => { + return Err(PushError { + report: PushReport { attempts }, + error, + }); + } + Err(_) => { + return Err(push_deadline_error( + attempts, + "while refreshing credentials", + )); + } + }; + force_reembed = false; + (ensured.token, Some(ensured.action), ensured.refresh_error) + } + None => (None, None, None), + }; + + let remaining = attempt_deadline.saturating_duration_since(time::Instant::now()); + let timeout_ms = u64::try_from(remaining.as_millis()).unwrap_or(u64::MAX); + if timeout_ms == 0 { + return Err(push_deadline_error(attempts, "before running git push")); + } + let push_result = match sandbox + .exec_command(&cmd, timeout_ms, None, None, None) + .await + { + Ok(result) => result.into_result(&label).map(|_| ()), + Err(err) => Err(crate::Error::context(label.clone(), err)), + }; + + match push_result { + Ok(()) => { + attempts.push(PushAttempt { + attempt: attempt_number, + started_at, + success: true, + retry_reason: None, + exec_output_tail: None, + token, + credential_action, + refresh_error, + }); + tracing::info!( + refspec = %refspec, + attempt = attempt_number, + token_generation = token.map(|token| token.generation), + token_age_ms = token.and_then(|token| token.age_ms()), + "Pushed git ref to origin" + ); + return Ok(PushReport { attempts }); + } + Err(error) => { + // Drift recovery: the tracked generation is local belief, and + // agent code inside the sandbox can rewrite `origin`. The + // first auth/not-found failure earns one forced re-embed of + // the pinned token, inside the same retry budget. + if !drift_repaired && lease.is_some() && push_failure_looks_auth_shaped(&error) { + drift_repaired = true; + force_reembed = true; + } + let cred = CredentialContext::from_snapshot(token.as_ref()); + let retry_reason = classify_push_error(&error, cred); + attempts.push(PushAttempt { + attempt: attempt_number, + started_at, + success: false, + retry_reason, + exec_output_tail: error.default_redacted_output_tail(), + token, + credential_action, + refresh_error, + }); + + let exhausted = attempt_number >= plan.max_attempts.max(1); + let Some(reason) = retry_reason.filter(|_| !exhausted) else { + return Err(PushError { + report: PushReport { attempts }, + error, + }); + }; + let Some(delay) = plan.retry_delay(attempt_number, deadline) else { + return Err(PushError { + report: PushReport { attempts }, + error, + }); + }; + // The failure text can carry git stderr, so log the category + // rather than the message. + tracing::warn!( + refspec = %refspec, + attempt = attempt_number, + max_attempts = plan.max_attempts, + reason = %reason, + token_generation = token.map(|token| token.generation), + token_age_ms = token.and_then(|token| token.age_ms()), + delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), + "Git push failed, retrying with the same token" + ); + time::sleep(delay).await; + } + } + } +} + +fn push_deadline_error(attempts: Vec, stage: &str) -> PushError { + PushError { + report: PushReport { attempts }, + error: crate::Error::message(format!("Git push retry deadline expired {stage}")), + } +} + +#[cfg(test)] +mod push_tests { + use std::collections::VecDeque; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + + use chrono::Utc; + use fabro_github::InstallationToken; + use fabro_github::test_support::{InstallationTokenMinter, installation_token_source}; + use fabro_github::token_source::{InstallationTokenSource, REFRESH_MARGIN}; + use tokio::sync::Mutex as AsyncMutex; + + use super::*; + use crate::git_retry::{GitRetryReason, RetryPlan}; + use crate::push_credentials::{PushCredentialState, RefreshErrorKind}; + + const ORIGIN: &str = "https://github.com/fabro-testing/repo"; + const REFSPEC: &str = "refs/heads/fabro/run/01M0DH033P2XSTHAGVBHG6922F"; + + fn ok_exec() -> ExecResult { + ExecResult { + stdout: String::new(), + stderr: String::new(), + exit_code: Some(0), + termination: CommandTermination::Exited, + duration_ms: 5, + } + } + + fn failed_exec(stderr: &str) -> ExecResult { + ExecResult { + stdout: String::new(), + stderr: stderr.to_string(), + exit_code: Some(128), + termination: CommandTermination::Exited, + duration_ms: 5, + } + } + + fn timed_out_exec() -> ExecResult { + ExecResult { + stdout: String::new(), + stderr: "Command timed out".to_string(), + exit_code: None, + termination: CommandTermination::TimedOut, + duration_ms: 60_000, + } + } + + /// Sandbox stub that scripts `git push` results and records the exec + /// commands the push driver runs. `git remote set-url` execs succeed + /// unless scripted otherwise. + struct ScriptedGitSandbox { + push_results: Mutex>, + set_url_results: Mutex>, + push_commands: Mutex>, + set_url_commands: Mutex>, + } + + impl ScriptedGitSandbox { + fn new(push_results: Vec) -> Self { + Self { + push_results: Mutex::new(push_results.into()), + set_url_results: Mutex::new(VecDeque::new()), + push_commands: Mutex::new(Vec::new()), + set_url_commands: Mutex::new(Vec::new()), + } + } + + fn with_set_url_results(self, results: Vec) -> Self { + *self.set_url_results.lock().unwrap() = results.into(); + self + } + + fn push_count(&self) -> usize { + self.push_commands.lock().unwrap().len() + } + + fn set_url_commands(&self) -> Vec { + self.set_url_commands.lock().unwrap().clone() + } + } + + #[async_trait] + impl Sandbox for ScriptedGitSandbox { + async fn exec_command( + &self, + command: &str, + _timeout_ms: u64, + _working_dir: Option<&str>, + _env_vars: Option<&HashMap>, + _cancel_token: Option, + ) -> crate::Result { + if command.contains("remote set-url") { + self.set_url_commands + .lock() + .unwrap() + .push(command.to_string()); + return Ok(self + .set_url_results + .lock() + .unwrap() + .pop_front() + .unwrap_or_else(ok_exec)); + } + assert!( + command.contains("push origin"), + "unexpected exec: {command}" + ); + self.push_commands.lock().unwrap().push(command.to_string()); + Ok(self + .push_results + .lock() + .unwrap() + .pop_front() + .expect("push script exhausted")) + } + + async fn read_file_bytes(&self, _path: &str) -> crate::Result> { + unimplemented!() + } + + async fn write_file(&self, _path: &str, _content: &str) -> crate::Result<()> { + unimplemented!() + } + + async fn delete_file(&self, _path: &str) -> crate::Result<()> { + unimplemented!() + } + + async fn file_exists(&self, _path: &str) -> crate::Result { + unimplemented!() + } + + async fn list_directory( + &self, + _path: &str, + _depth: Option, + ) -> crate::Result> { + unimplemented!() + } + + async fn grep( + &self, + _pattern: &str, + _path: &str, + _options: &GrepOptions, + ) -> crate::Result> { + unimplemented!() + } + + async fn download_file_to_local( + &self, + _remote_path: &str, + _local_path: &Path, + ) -> crate::Result<()> { + unimplemented!() + } + + async fn upload_file_from_local( + &self, + _local_path: &Path, + _remote_path: &str, + ) -> crate::Result<()> { + unimplemented!() + } + + async fn initialize(&self) -> crate::Result<()> { + Ok(()) + } + + async fn cleanup(&self) -> crate::Result<()> { + Ok(()) + } + + fn working_directory(&self) -> &'static str { + "/workspace" + } + + fn platform(&self) -> &'static str { + "linux" + } + + fn os_version(&self) -> String { + "linux".to_string() + } + } + + enum MintAction { + Token(&'static str, chrono::Duration), + Error(&'static str), + } + + struct ScriptedMinter { + calls: AtomicUsize, + script: AsyncMutex>, + } + + impl ScriptedMinter { + fn new(script: Vec) -> std::sync::Arc { + std::sync::Arc::new(Self { + calls: AtomicUsize::new(0), + script: AsyncMutex::new(script.into()), + }) + } + + fn calls(&self) -> usize { + self.calls.load(Ordering::SeqCst) + } + } + + #[async_trait] + impl InstallationTokenMinter for ScriptedMinter { + async fn mint(&self) -> anyhow::Result { + self.calls.fetch_add(1, Ordering::SeqCst); + match self.script.lock().await.pop_front().expect("mint script") { + MintAction::Token(token, ttl) => Ok(InstallationToken { + token: token.to_string(), + expires_at: Utc::now() + ttl, + }), + MintAction::Error(message) => Err(anyhow::anyhow!(message)), + } + } + } + + struct SlowMinter; + + #[async_trait] + impl InstallationTokenMinter for SlowMinter { + async fn mint(&self) -> anyhow::Result { + time::sleep(Duration::from_secs(2)).await; + Ok(InstallationToken { + token: "ghs_slow".to_string(), + expires_at: Utc::now() + chrono::Duration::hours(1), + }) + } + } + + fn minting_state( + script: Vec, + ) -> (PushCredentialState, std::sync::Arc) { + let minter = ScriptedMinter::new(script); + let source = installation_token_source( + "fabro-testing/repo", + std::sync::Arc::clone(&minter) as std::sync::Arc, + ); + (PushCredentialState::new(Some(source)), minter) + } + + async fn seed_clone_token(state: &PushCredentialState) { + let clone_token = state + .source() + .expect("state has a source") + .mint_for_clone() + .await + .expect("clone mint succeeds"); + state.record_embedded(clone_token).await; + } + + /// Regression for run `01M0DH033P2XSTHAGVBHG6922F` (the push variant of + /// `clone_not_found_after_a_successful_mint_is_retried`): GitHub rejected + /// pushes with 404 "Repository not found" milliseconds after a token + /// mint. The retry must reuse the same token — replication of a given + /// token only makes progress — and recover inside the plan's budget. + #[tokio::test(start_paused = true)] + async fn push_not_found_after_a_successful_mint_is_retried_with_the_same_token() { + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::minutes(60), + )]); + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) .await - .map_err(|e| crate::Error::context(label.clone(), e))? - .into_result(&label)?; - tracing::info!(refspec = %refspec, "Pushed git ref to origin"); - Ok(()) + .expect("push should recover within the checkpoint plan"); + + assert_eq!(report.attempts.len(), 3); + assert_eq!(minter.calls(), 1, "retries must not re-mint"); + for attempt in &report.attempts { + assert_eq!(attempt.token.expect("token recorded").generation, 1); + } + assert_eq!( + report.attempts[0].retry_reason, + Some(GitRetryReason::TokenReplication) + ); + assert!(report.attempts[0].exec_output_tail.is_some()); + assert_eq!( + report.attempts[0].credential_action, + Some(RemoteCredentialAction::Embedded), + "first attempt embeds the resolved token" + ); + assert!(report.attempts[2].success); + assert!(report.attempts[2].exec_output_tail.is_none()); + assert_eq!(sandbox.push_count(), 3); + } + + /// The publish plan gives the terminal push a real budget: four + /// replication-lag failures still recover on the fifth attempt. + #[tokio::test(start_paused = true)] + async fn publish_plan_survives_four_not_found_failures() { + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::minutes(60), + )]); + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::publish_push(), + ) + .await + .expect("push should recover within the publish plan"); + + assert_eq!(report.attempts.len(), 5); + assert_eq!(minter.calls(), 1); + assert!(report.attempts[4].success); + } + + /// Margin-boundary pinning: a token resolved just above the refresh + /// margin stays pinned through a full retry sequence — the operation + /// never re-resolves mid-flight, so no fresh mint can restart the + /// replication clock. + #[tokio::test(start_paused = true)] + async fn token_resolved_just_above_the_margin_stays_pinned_through_retries() { + let ttl = REFRESH_MARGIN + Duration::from_secs(5); + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::from_std(ttl).unwrap(), + )]); + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("remote: Repository not found."), + failed_exec("remote: Repository not found."), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("push should recover"); + + assert_eq!(minter.calls(), 1, "no mid-operation mint"); + let generations: Vec = report + .attempts + .iter() + .map(|attempt| attempt.token.expect("token recorded").generation) + .collect(); + assert_eq!(generations, vec![1, 1, 1]); + } + + #[tokio::test(start_paused = true)] + async fn static_credential_auth_failure_fails_fast() { + let source = InstallationTokenSource::for_origin( + &fabro_github::GitHubCredentials::Pat("ghp_pat".to_string()), + ORIGIN, + serde_json::json!({ "contents": "write" }), + ) + .unwrap(); + let state = PushCredentialState::new(Some(source)); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![failed_exec( + "fatal: Authentication failed for 'https://github.com/fabro-testing/repo'", + )]); + + let push_error = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::publish_push(), + ) + .await + .expect_err("static credentials cannot become valid by waiting"); + + assert_eq!(push_error.report.attempts.len(), 1); + assert_eq!(push_error.report.attempts[0].retry_reason, None); + assert!(push_error.report.attempts[0].token.unwrap().is_static()); + } + + /// Clone seeding closes the "nothing was ever embedded" hole: when the + /// first refresh mint fails, the push falls back to the clone token + /// recorded as last-embedded instead of aborting. + #[tokio::test(start_paused = true)] + async fn mint_failure_falls_back_to_the_clone_token() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_clone", chrono::Duration::minutes(5)), + // The clone token is inside the margin, so lease acquisition + // re-mints and fails. + MintAction::Error("mint failed"), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ok_exec()]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("push proceeds with the still-valid clone token"); + + assert_eq!(minter.calls(), 2); + let attempt = &report.attempts[0]; + assert!(attempt.success); + assert_eq!(attempt.refresh_error, Some(RefreshErrorKind::Mint)); + assert_eq!( + attempt.token.expect("fallback token recorded").generation, + 1, + "attempts classify against the embedded clone token, never None" + ); + assert_eq!( + attempt.credential_action, + Some(RemoteCredentialAction::Unchanged) + ); + } + + #[tokio::test(start_paused = true)] + async fn acquisition_fails_when_mint_fails_and_nothing_was_embedded() { + let (state, _minter) = minting_state(vec![MintAction::Error("mint failed")]); + let sandbox = ScriptedGitSandbox::new(vec![]); + + let push_error = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect_err("there is nothing to push with"); + + assert!(push_error.report.attempts.is_empty()); + assert!(push_error.error.to_string().contains("token_mint_failed")); + assert_eq!(sandbox.push_count(), 0); + } + + /// Late-mint recovery: the fallback push fails on the expired-ish old + /// token, a later attempt's resolve retry succeeds, the target embeds, + /// and the push recovers — all inside one operation's budget. + #[tokio::test(start_paused = true)] + async fn late_mint_recovery_lands_the_target_inside_the_operation() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), + MintAction::Error("mint failed"), + MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("fatal: Authentication failed for 'https://github.com'"), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("late mint should recover the push"); + + assert_eq!(minter.calls(), 3); + let first = &report.attempts[0]; + assert_eq!(first.refresh_error, Some(RefreshErrorKind::Mint)); + assert_eq!(first.token.unwrap().generation, 1); + let second = &report.attempts[1]; + assert!(second.success); + assert_eq!(second.refresh_error, None); + assert_eq!(second.token.unwrap().generation, 2); + assert_eq!( + second.credential_action, + Some(RemoteCredentialAction::Embedded), + "the report shows the single generation transition" + ); + } + + /// A failed `set-url` defers the embed: attempt 1 records the old + /// generation with the refresh error, attempt 2 lands the target, and the + /// report shows the one generation transition via `credential_action`. + #[tokio::test(start_paused = true)] + async fn set_url_failure_defers_the_embed_until_the_next_attempt() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), + MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec("error: RPC failed; connection reset by peer"), + ok_exec(), + ]) + .with_set_url_results(vec![failed_exec("error: could not lock config file")]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("deferred embed should land on the retry"); + + assert_eq!( + minter.calls(), + 2, + "the successful resolve is never repeated" + ); + let first = &report.attempts[0]; + assert_eq!(first.refresh_error, Some(RefreshErrorKind::SetUrl)); + assert_eq!( + first.token.unwrap().generation, + 1, + "pin stays on the old token" + ); + assert_eq!( + first.credential_action, + Some(RemoteCredentialAction::Unchanged) + ); + let second = &report.attempts[1]; + assert_eq!(second.token.unwrap().generation, 2); + assert_eq!( + second.credential_action, + Some(RemoteCredentialAction::Embedded) + ); + assert!(second.success); + } + + #[tokio::test(start_paused = true)] + async fn timed_out_set_url_stops_before_push_while_it_may_still_run() { + let (state, minter) = minting_state(vec![ + MintAction::Token("ghs_gen1", chrono::Duration::minutes(5)), + MintAction::Token("ghs_gen2", chrono::Duration::minutes(60)), + ]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![]).with_set_url_results(vec![timed_out_exec()]); + + let push_error = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect_err("a timed-out set-url can still rewrite origin later"); + + assert_eq!(minter.calls(), 2); + assert!(push_error.report.attempts.is_empty()); + assert_eq!(sandbox.push_count(), 0); + } + + /// Remote drift: agent code rewrote `origin`, so the push fails on auth + /// even though the tracked generation looks current. The first + /// auth-shaped failure earns one forced re-embed of the pinned token. + #[tokio::test(start_paused = true)] + async fn remote_drift_gets_one_forced_reembed_of_the_pinned_token() { + let (state, minter) = minting_state(vec![MintAction::Token( + "ghs_gen1", + chrono::Duration::minutes(60), + )]); + seed_clone_token(&state).await; + let sandbox = ScriptedGitSandbox::new(vec![ + failed_exec( + "fatal: could not read Username for 'https://github.com': No such device or address\nremote: Repository not found.", + ), + ok_exec(), + ]); + + let report = git_push_via_exec( + &sandbox, + Some((&state, ORIGIN)), + REFSPEC, + &RetryPlan::checkpoint_push(), + ) + .await + .expect("drift repair should restore the pinned credentials"); + + assert_eq!(minter.calls(), 1, "drift repair re-embeds, never re-mints"); + assert_eq!( + report.attempts[0].credential_action, + Some(RemoteCredentialAction::Unchanged), + "before the failure the tracked generation matched" + ); + assert_eq!( + report.attempts[1].credential_action, + Some(RemoteCredentialAction::Embedded), + "the retry force-re-embeds the pinned token" + ); + let set_urls = sandbox.set_url_commands(); + assert_eq!(set_urls.len(), 1); + assert!(set_urls[0].contains("ghs_gen1")); + } + + #[tokio::test(start_paused = true)] + async fn push_without_managed_credentials_reports_no_token() { + let sandbox = ScriptedGitSandbox::new(vec![ok_exec()]); + + let report = git_push_via_exec(&sandbox, None, REFSPEC, &RetryPlan::checkpoint_push()) + .await + .expect("push succeeds"); + + assert_eq!(report.attempts.len(), 1); + assert_eq!(report.attempts[0].token, None); + assert_eq!(report.attempts[0].credential_action, None); + } + + #[tokio::test(start_paused = true)] + async fn unauthenticated_auth_failure_is_permanent() { + let sandbox = ScriptedGitSandbox::new(vec![failed_exec( + "fatal: Authentication failed for 'https://github.com/fabro-testing/repo'", + )]); + + let push_error = git_push_via_exec(&sandbox, None, REFSPEC, &RetryPlan::publish_push()) + .await + .expect_err("no credentials to wait on"); + + assert_eq!(push_error.report.attempts.len(), 1); + assert_eq!(push_error.report.attempts[0].retry_reason, None); + } + + #[tokio::test(start_paused = true)] + async fn timed_out_push_is_not_retried_while_the_remote_process_may_still_run() { + let sandbox = ScriptedGitSandbox::new(vec![timed_out_exec()]); + + let push_error = git_push_via_exec(&sandbox, None, REFSPEC, &RetryPlan::publish_push()) + .await + .expect_err("an unconfirmed timeout must fail without another push"); + + assert_eq!(sandbox.push_count(), 1); + assert_eq!(push_error.report.attempts.len(), 1); + assert_eq!(push_error.report.attempts[0].retry_reason, None); + } + + #[tokio::test(start_paused = true)] + async fn retry_deadline_includes_credential_lease_acquisition() { + let source = installation_token_source("fabro-testing/repo", Arc::new(SlowMinter)); + let state = PushCredentialState::new(Some(source)); + let sandbox = ScriptedGitSandbox::new(vec![]); + let mut plan = RetryPlan::checkpoint_push(); + plan.max_elapsed = Some(Duration::from_secs(1)); + + let push_error = git_push_via_exec(&sandbox, Some((&state, ORIGIN)), REFSPEC, &plan) + .await + .expect_err("credential acquisition must stop at the operation deadline"); + + assert!(push_error.report.attempts.is_empty()); + assert_eq!(sandbox.push_count(), 0); + assert!(push_error.error.to_string().contains("deadline expired")); + } + + #[tokio::test(start_paused = true)] + async fn expired_retry_deadline_does_not_launch_a_zero_timeout_push() { + let sandbox = ScriptedGitSandbox::new(vec![]); + let mut plan = RetryPlan::checkpoint_push(); + plan.max_elapsed = Some(Duration::ZERO); + + let push_error = git_push_via_exec(&sandbox, None, REFSPEC, &plan) + .await + .expect_err("an expired operation must stop before exec"); + + assert!(push_error.report.attempts.is_empty()); + assert_eq!(sandbox.push_count(), 0); + } } #[cfg(test)] diff --git a/lib/components/fabro-sandbox/src/sandbox_spec.rs b/lib/components/fabro-sandbox/src/sandbox_spec.rs index 9550ff949..06565eb95 100644 --- a/lib/components/fabro-sandbox/src/sandbox_spec.rs +++ b/lib/components/fabro-sandbox/src/sandbox_spec.rs @@ -208,7 +208,7 @@ impl SandboxSpec { } => { let mut sandbox = DockerSandbox::new( config.clone(), - github_app.clone(), + github_app.as_ref(), *run_id, clone_origin_url.clone(), clone_branch.clone(), diff --git a/lib/components/fabro-sandbox/src/test_support.rs b/lib/components/fabro-sandbox/src/test_support.rs index 7364dc8b0..d25d9bbf4 100644 --- a/lib/components/fabro-sandbox/src/test_support.rs +++ b/lib/components/fabro-sandbox/src/test_support.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::Mutex; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; @@ -29,6 +29,8 @@ pub struct MockSandbox { pub os_version_str: String, /// Captures (path, content) pairs from `write_file` calls. pub written_files: Mutex>, + /// Counts calls to `write_existing_file`. + pub existing_file_writes: AtomicUsize, /// Captures the `timeout_ms` argument from `exec_command` calls. pub captured_timeout: Mutex>, /// Captures the `command` argument from `exec_command` calls (last only). @@ -104,6 +106,10 @@ impl MockSandbox { .expect("delete_calls lock poisoned") } + pub fn existing_file_write_count(&self) -> usize { + self.existing_file_writes.load(Ordering::Relaxed) + } + pub fn set_stdio_process(&self, process: MockStdioProcess) { *self .stdio_process @@ -156,6 +162,7 @@ impl Default for MockSandbox { platform_str: "darwin", os_version_str: "Darwin 24.0.0".into(), written_files: Mutex::new(Vec::new()), + existing_file_writes: AtomicUsize::new(0), captured_timeout: Mutex::new(None), captured_command: Mutex::new(None), captured_commands: Mutex::new(Vec::new()), @@ -250,6 +257,11 @@ impl Sandbox for MockSandbox { Ok(()) } + async fn write_existing_file(&self, path: &str, content: &str) -> crate::Result<()> { + self.existing_file_writes.fetch_add(1, Ordering::Relaxed); + self.write_file(path, content).await + } + async fn delete_file(&self, _path: &str) -> crate::Result<()> { Ok(()) } 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..6d19ed978 --- /dev/null +++ b/lib/components/fabro-store/src/blob_store.rs @@ -0,0 +1,301 @@ +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) + } +} + +/// 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), +} + +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?); + 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()) + .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/run_state.rs b/lib/components/fabro-store/src/run_state.rs index c89587e59..0c59af57a 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -1046,6 +1046,7 @@ fn projection_from_created(event: &EventEnvelope) -> Result { provenance: props.provenance.clone(), manifest_blob: props.manifest_blob, definition_blob: None, + spec_blob: props.spec_blob, git: props.git.clone(), fork_source_ref: props.fork_source_ref.clone(), }; @@ -1360,8 +1361,7 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { .conclusion .as_ref() .map(|conclusion| conclusion.timing); - let terminal_total = terminal_total_usd_micros(state); - let current_total = projected_billing(state).total_usd_micros; + let total_usd_micros = projected_billing(state).total_usd_micros; Run { id: *run_id, @@ -1405,10 +1405,10 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { completed_at, }, timing: run_timing, - billing: terminal_total.map(|total_usd_micros| RunBillingSummary { + billing: total_usd_micros.map(|total_usd_micros| RunBillingSummary { total_usd_micros: Some(total_usd_micros), }), - size: RunSize::from_total_usd_micros(current_total), + size: RunSize::from_total_usd_micros(total_usd_micros), ask_fabro: AskFabro::default(), diff: diff_summary, pull_request: state.pull_request.clone(), @@ -1421,14 +1421,6 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run { } } -fn terminal_total_usd_micros(state: &RunProjection) -> Option { - state - .conclusion - .as_ref() - .and_then(|conclusion| conclusion.billing.as_ref()) - .and_then(|billing| billing.total_usd_micros) -} - pub(crate) fn projected_billing(state: &RunProjection) -> BilledTokenCounts { if let Some(billing) = state .conclusion @@ -1694,11 +1686,12 @@ mod tests { 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, - SuccessReason, WorkflowSettings, first_event_seq, fixtures, test_support, + RunApprovalState, RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, + RunStatus, Speed, StageContextWindowBreakdownItem, StageContextWindowCategory, + StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, + StageContextWindowWarning, StageHandler, StageModelUsage, StageOutcome, StageState, + StageTiming, SubAgentStatus, SuccessReason, WorkflowSettings, first_event_seq, fixtures, + test_support, }; use serde_json::json; @@ -2281,19 +2274,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 +4068,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 +4084,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); @@ -5284,7 +5247,12 @@ mod tests { let summary = build_summary(&state, &fixtures::RUN_1); assert_eq!(summary.size, RunSize::S); - assert_eq!(summary.billing, None); + assert_eq!( + summary.billing, + Some(RunBillingSummary { + total_usd_micros: Some(20_000_001), + }) + ); } #[test] diff --git a/lib/components/fabro-store/src/run_summary_store.rs b/lib/components/fabro-store/src/run_summary_store.rs index 5db1a49b1..dcbec847e 100644 --- a/lib/components/fabro-store/src/run_summary_store.rs +++ b/lib/components/fabro-store/src/run_summary_store.rs @@ -601,6 +601,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/components/fabro-store/src/slate/blob_store.rs b/lib/components/fabro-store/src/slate/blob_store.rs deleted file mode 100644 index 68c6a9a54..000000000 --- a/lib/components/fabro-store/src/slate/blob_store.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::sync::Arc; - -use bytes::Bytes; -use fabro_types::BlobHash; -use futures::StreamExt; -use tracing::warn; - -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) - } -} - -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, id: &BlobHash) -> Result> { - Ok(self.repo.get(id).await?.map(|blob| blob.0)) - } - - 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)] -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 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; - 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 8f9c16ac7..6158e18a4 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)) @@ -595,6 +593,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: Some(fabro_types::GitContext { origin_url: "https://github.com/fabro-sh/fabro".to_string(), branch: "main".to_string(), @@ -836,12 +835,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 +850,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,11 +858,9 @@ 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()) ); - 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..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; @@ -561,12 +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 list_blobs(&self) -> Result> { - self.inner.blob_store.list().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 { @@ -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) } 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-version/src/lib.rs b/lib/components/fabro-workflow-version/src/lib.rs index 320d09139..75731fb49 100644 --- a/lib/components/fabro-workflow-version/src/lib.rs +++ b/lib/components/fabro-workflow-version/src/lib.rs @@ -9,7 +9,9 @@ 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, @@ -17,12 +19,13 @@ use fabro_template::{ 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,10 @@ pub struct ValidatedWorkflowVersion(WorkflowVersion); impl ValidatedWorkflowVersion { pub fn new(version: WorkflowVersion) -> Result { - validate_config(&version)?; - validate_graph_closure(&version)?; + 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)) } @@ -104,7 +109,35 @@ impl ValidatedWorkflowVersion { } } -fn validate_config(version: &WorkflowVersion) -> Result<(), WorkflowVersionError> { +/// 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_roots: &mut TemplateRoots, +) -> 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 +166,36 @@ fn validate_config(version: &WorkflowVersion) -> Result<(), WorkflowVersionError for image in layer.environment_images() { 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(version.entrypoint(), unresolved_source(goal)); + } + Some(RunGoalLayer::File { file }) => { + let (_, content) = validate_config_file_reference( + version, + &config_path, + ReferenceKind::RunGoalFile, + &unresolved_source(file), + )?; + template_roots.push(version.entrypoint(), 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, @@ -144,20 +204,33 @@ 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) -> 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_roots: &mut TemplateRoots, +) -> Result<(), WorkflowVersionError> { let mut queue = VecDeque::from([version.entrypoint().clone()]); let mut visited = BTreeSet::new(); let mut child_workflows = BTreeSet::new(); @@ -185,10 +258,12 @@ 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(&target, content); + Ok(()) } GraphReference::GoalInline { content } | GraphReference::InlinePrompt { content } => { - validate_template(&path, content, &template_store, &template_root) + template_roots.push(&path, content); + Ok(()) } GraphReference::Import { reference } => { let target = resolve_reference(&path, ReferenceKind::Import, reference)?; @@ -206,7 +281,7 @@ 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(&target, content); } Ok(()) } @@ -234,24 +309,24 @@ 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 { + WorkflowPath::new(error.source_path().to_string()) + .expect("template paths sourced from a workflow version must be valid") +} + fn template_store(version: &WorkflowVersion) -> BundleTemplateStore { BundleTemplateStore::new( version @@ -300,6 +375,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 +411,38 @@ mod tests { ) } + fn version_with_config( + config: impl Into, + 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.into()); + 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(); + let config = format!("_version = 1\n[run.goal]\nfile = {reference}\n"); + version_with_config(config, []) + } + + 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 +541,245 @@ mod tests { )); } + #[test] + fn rejects_missing_workflow_goal_file() { + let error = version_with_goal_file("prompts/goal.md").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() { + // 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 "prompts/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_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| { + 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\" %}"] { + 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.fabro")); + assert!(matches!( + source.as_ref(), + TemplateDiscoveryError::Missing { parent, reference } + if parent.to_string() == "workflow.fabro" && 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.fabro" + )); + + 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 { + source: TemplateLoadError::EscapesRoot { parent, .. }, + .. + } if parent.to_string() == "workflow.fabro" + )); + } + + #[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 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( diff --git a/lib/components/fabro-workflow-version/src/store.rs b/lib/components/fabro-workflow-version/src/store.rs index 80dcebd53..2d468619f 100644 --- a/lib/components/fabro-workflow-version/src/store.rs +++ b/lib/components/fabro-workflow-version/src/store.rs @@ -40,6 +40,49 @@ 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. +/// +/// 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, + dependencies: BTreeMap, +} + +impl LoadedWorkflowVersionClosure { + #[must_use] + pub fn root_id(&self) -> WorkflowVersionId { + self.root_id + } + + #[must_use] + pub fn root(&self) -> &WorkflowVersion { + self.root.version() + } + + #[must_use] + pub fn get(&self, id: &WorkflowVersionId) -> Option<&WorkflowVersion> { + if *id == self.root_id { + return Some(self.root.version()); + } + self.dependencies + .get(id) + .map(ValidatedWorkflowVersion::version) + } + + pub fn versions(&self) -> impl Iterator + '_ { + std::iter::once((self.root_id, self.root.version())).chain( + self.dependencies + .iter() + .map(|(id, version)| (*id, version.version())), + ) + } +} + /// Content-addressed storage for validated workflow versions. /// /// `put` only accepts semantically validated versions; `get` re-validates @@ -61,7 +104,7 @@ impl WorkflowVersionStore { version: &ValidatedWorkflowVersion, ) -> Result { let canonical = version.version().canonical_bytes()?; - self.validate_dependency_closure(version.version().workflow_dependencies()) + self.walk_dependency_closure(version.version().workflow_dependencies(), |_, _| ()) .await?; self.blobs .write(&canonical) @@ -77,19 +120,38 @@ impl WorkflowVersionStore { let Some(version) = self.load_one(id).await? else { return Ok(None); }; - self.validate_dependency_closure(version.version().workflow_dependencies()) + self.walk_dependency_closure(version.version().workflow_dependencies(), |_, _| ()) .await?; Ok(Some(version)) } + 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 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, + root, + dependencies, + })) + } + async fn load_one( &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 { @@ -105,9 +167,12 @@ impl WorkflowVersionStore { Ok(Some(validated)) } - async fn validate_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 visit: impl FnMut(WorkflowVersionId, ValidatedWorkflowVersion), ) -> Result<(), WorkflowVersionStoreError> { let mut pending = dependencies .iter() @@ -128,6 +193,7 @@ impl WorkflowVersionStore { .iter() .map(|(path, id)| (path.clone(), *id)), ); + visit(id, dependency); } Ok(None) => { return Err(WorkflowVersionStoreError::DependencyNotFound { path, id }); @@ -180,6 +246,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()), @@ -197,12 +269,15 @@ 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); - 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)); } @@ -227,16 +302,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!( @@ -262,9 +333,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(), @@ -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/components/fabro-workflow/Cargo.toml b/lib/components/fabro-workflow/Cargo.toml index b3ab70260..0024c8b79 100644 --- a/lib/components/fabro-workflow/Cargo.toml +++ b/lib/components/fabro-workflow/Cargo.toml @@ -76,6 +76,7 @@ toml.workspace = true fabro-vault = { path = "../../foundation/fabro-vault" } [dev-dependencies] fabro-auth = { path = "../../foundation/fabro-auth", features = ["test-support"] } +fabro-github = { path = "../fabro-github", features = ["test-support"] } base64.workspace = true fabro-acp = { path = "../fabro-acp", features = ["test-support"] } fabro-workflow = { path = ".", features = ["test-support"] } diff --git a/lib/components/fabro-workflow/src/artifact.rs b/lib/components/fabro-workflow/src/artifact.rs index ef892b442..a457e7395 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(()) } @@ -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) } @@ -232,17 +241,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 +276,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 +343,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) => { @@ -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,14 +387,16 @@ 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 { 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, 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(_) => {} @@ -413,17 +433,18 @@ 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, + 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? { - let path = local_materialized_blob_path(run_dir, blob_id); + 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_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 +460,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 +478,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 { @@ -502,17 +523,33 @@ 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, } -fn local_materialized_blob_path(run_dir: &Path, blob_id: &BlobHash) -> PathBuf { +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 { RunScratch::new(run_dir) .runtime_dir() .join("blobs") - .join(format!("{blob_id}.json")) + .join(format!("{blob_hash}.json")) } #[cfg(test)] @@ -549,7 +586,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 +598,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 +628,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 @@ -787,15 +824,43 @@ 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_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 +872,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 +935,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 +948,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 +956,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 +977,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))) ); } @@ -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) } 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/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/error.rs b/lib/components/fabro-workflow/src/error.rs index f8c08ca15..290aacf91 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] = &[ @@ -307,6 +309,10 @@ pub enum Error { message: String, failure_class: FailureCategory, exec_output_tail: Option, + /// Structured context lines appended after the source chain in + /// `causes()` — e.g. one line per push attempt on a publish push + /// failure. + extra_causes: Vec, #[source] source: Option, }, @@ -355,6 +361,7 @@ impl Error { message, failure_class, exec_output_tail, + extra_causes: Vec::new(), source: None, } } @@ -366,16 +373,29 @@ impl Error { message: impl Into, source: impl Into, exec_output_tail: Option, + ) -> Self { + Self::stage_with_source_details(stage, message, source, None, exec_output_tail, Vec::new()) + } + + fn stage_with_source_details( + stage: ErrorStage, + message: impl Into, + source: impl Into, + failure_class: Option, + exec_output_tail: Option, + extra_causes: Vec, ) -> Self { let message = message.into(); let source = SharedError::new(source.into()); - let failure_class = - classify_failure_reason(&render_with_causes(&message, &collect_chain(&source))); + let failure_class = failure_class.unwrap_or_else(|| { + classify_failure_reason(&render_with_causes(&message, &collect_chain(&source))) + }); Self::Stage { stage, message, failure_class, exec_output_tail, + extra_causes, source: Some(source), } } @@ -452,12 +472,42 @@ impl Error { Self::stage_with_source(ErrorStage::Publish, message, source, exec_output_tail) } + /// Build a publish error with an explicitly determined failure category, + /// for callers that know more than message sniffing can recover — e.g. + /// exhausted push retries whose attempts all classified as transient. + /// `extra_causes` lines land after the source chain in the failure + /// detail (one line per push attempt). + pub fn publish_with_source_and_class( + message: impl Into, + source: impl Into, + failure_class: FailureCategory, + exec_output_tail: Option, + extra_causes: Vec, + ) -> Self { + Self::stage_with_source_details( + ErrorStage::Publish, + message, + source, + Some(failure_class), + exec_output_tail, + extra_causes, + ) + } + #[must_use] pub fn causes(&self) -> Vec { match self { - Self::Stage { source, .. } => source - .as_ref() - .map_or_else(Vec::new, |source| collect_chain(source)), + Self::Stage { + source, + extra_causes, + .. + } => { + let mut causes = source + .as_ref() + .map_or_else(Vec::new, |source| collect_chain(source)); + causes.extend(extra_causes.iter().cloned()); + causes + } Self::Template { source, .. } => collect_chain(source), Self::ScriptInterpolation { source, .. } => collect_chain(source), Self::Llm(err) => collect_causes(err), @@ -807,6 +857,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 +1343,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 +1512,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!( diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index 6403d8933..85a02769a 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -23,6 +23,44 @@ fn stage_status_from_string(status: &str) -> StageOutcome { }) } +/// Project the sandbox layer's runtime push attempts into the durable +/// `git.push` attempt shape. +/// +/// This is the only place the runtime attempt record crosses into stored +/// events: the token snapshot flattens into the three flat `token_*` fields +/// (a nested provenance enum never appears in stored events), and the retry +/// classifier's verdict becomes `classified_reason`. +fn git_push_attempt_props( + attempts: &[fabro_sandbox::PushAttempt], +) -> Vec { + attempts + .iter() + .map(|attempt| fabro_types::GitPushAttemptProps { + attempt: attempt.attempt, + started_at: attempt.started_at, + success: attempt.success, + classified_reason: attempt.retry_reason, + exec_output_tail: attempt.exec_output_tail.clone(), + token_generation: attempt.token.map(|token| token.generation), + token_provenance: attempt.token.map(|token| match token.provenance { + fabro_sandbox::TokenProvenance::Minted { .. } => { + fabro_types::GitTokenProvenance::Minted + } + fabro_sandbox::TokenProvenance::Reused { .. } => { + fabro_types::GitTokenProvenance::Reused + } + fabro_sandbox::TokenProvenance::Static => fabro_types::GitTokenProvenance::Static, + }), + token_age_ms: attempt + .token + .and_then(|token| token.age_at(attempt.started_at)) + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)), + credential_action: attempt.credential_action, + refresh_error: attempt.refresh_error, + }) + .collect() +} + fn event_body_from_event(event: &Event) -> EventBody { match event { Event::RunCreated { @@ -36,6 +74,7 @@ fn event_body_from_event(event: &Event) -> EventBody { automation, provenance, manifest_blob, + spec_blob, git, fork_source_ref, retried_from, @@ -54,6 +93,7 @@ fn event_body_from_event(event: &Event) -> EventBody { automation: automation.clone(), provenance: provenance.clone(), manifest_blob: *manifest_blob, + spec_blob: *spec_blob, git: git.clone(), fork_source_ref: fork_source_ref.clone(), retried_from: *retried_from, @@ -520,10 +560,12 @@ fn event_body_from_event(event: &Event) -> EventBody { branch, success, exec_output_tail, + attempts, } => EventBody::GitPush(fabro_types::GitPushProps { branch: branch.clone(), success: *success, exec_output_tail: exec_output_tail.clone(), + attempts: git_push_attempt_props(attempts), }), Event::GitFetch { branch, success } => EventBody::GitFetch(fabro_types::GitFetchProps { branch: branch.clone(), @@ -2170,12 +2212,142 @@ mod tests { } } + /// The `git.push` attempts contract: every runtime attempt fact + /// round-trips through `GitPushAttemptProps`, the token snapshot is + /// flattened to the three flat token fields (a nested provenance enum + /// never appears in stored events), and optional failure fields are + /// omitted when absent. + #[test] + fn git_push_attempts_round_trip_through_the_durable_shape() { + let started_at = Utc::now(); + let minted_at = started_at - chrono::Duration::milliseconds(180); + let expires_at = started_at + chrono::Duration::minutes(60); + let runtime_attempts = vec![ + fabro_sandbox::PushAttempt { + attempt: 1, + started_at, + success: false, + retry_reason: Some(fabro_sandbox::GitRetryReason::TokenReplication), + exec_output_tail: Some(exec_tail()), + token: Some(fabro_sandbox::TokenSnapshot { + generation: 14, + provenance: fabro_sandbox::TokenProvenance::Minted { + minted_at, + expires_at, + }, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Embedded), + refresh_error: None, + }, + // Terminal classified failure with a refresh error: the last + // attempt carries its classification too. + fabro_sandbox::PushAttempt { + attempt: 2, + started_at: started_at + chrono::Duration::seconds(3), + success: false, + retry_reason: Some(fabro_sandbox::GitRetryReason::TransientInfra), + exec_output_tail: Some(exec_tail()), + token: Some(fabro_sandbox::TokenSnapshot { + generation: 14, + provenance: fabro_sandbox::TokenProvenance::Reused { + minted_at, + expires_at, + }, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), + refresh_error: Some(fabro_sandbox::RefreshErrorKind::SetUrl), + }, + ]; + let expected_attempts = git_push_attempt_props(&runtime_attempts); + + let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { + branch: "fabro/run/01M0DH033P2XSTHAGVBHG6922F".to_string(), + success: false, + exec_output_tail: Some(exec_tail()), + attempts: runtime_attempts, + }); + + let json = serde_json::to_value(&stored).unwrap(); + let serialized = &json["properties"]["attempts"]; + assert_eq!(serialized[0]["attempt"], 1); + assert_eq!(serialized[0]["classified_reason"], "token_replication"); + assert_eq!(serialized[0]["token_generation"], 14); + assert_eq!(serialized[0]["token_provenance"], "minted"); + assert_eq!(serialized[0]["token_age_ms"], 180); + assert_eq!(serialized[0]["credential_action"], "embedded"); + assert!(serialized[0].get("refresh_error").is_none()); + assert_eq!(serialized[1]["classified_reason"], "transient_infra"); + assert_eq!(serialized[1]["token_provenance"], "reused"); + assert_eq!(serialized[1]["refresh_error"], "set_url"); + // The provenance enum never nests in stored events. + assert!(serialized[0].get("token").is_none()); + + let round_tripped: ::fabro_types::RunEvent = serde_json::from_value(json).unwrap(); + match round_tripped.body { + EventBody::GitPush(props) => { + assert!(!props.success); + assert_eq!(props.attempts, expected_attempts); + } + other => panic!("expected GitPush body, got {other:?}"), + } + } + + #[test] + fn successful_single_attempt_push_omits_failure_fields() { + let attempts = vec![fabro_sandbox::PushAttempt { + attempt: 1, + started_at: Utc::now(), + success: true, + retry_reason: None, + exec_output_tail: None, + token: Some(fabro_sandbox::TokenSnapshot { + generation: 0, + provenance: fabro_sandbox::TokenProvenance::Static, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), + refresh_error: None, + }]; + let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { + branch: "fabro/run/run-1".to_string(), + success: true, + exec_output_tail: None, + attempts, + }); + + let json = serde_json::to_value(&stored).unwrap(); + let attempt = &json["properties"]["attempts"][0]; + assert_eq!(attempt["success"], true); + assert_eq!(attempt["token_provenance"], "static"); + for absent in [ + "classified_reason", + "exec_output_tail", + "token_age_ms", + "refresh_error", + ] { + assert!(attempt.get(absent).is_none(), "{absent} should be omitted"); + } + } + + /// Events stored before attempts were recorded deserialize with the field + /// absent; the pre-existing three fields are untouched. + #[test] + fn stored_git_push_without_attempts_still_deserializes() { + let json = serde_json::json!({ + "branch": "fabro/run/old", + "success": true + }); + let props: fabro_types::GitPushProps = serde_json::from_value(json).unwrap(); + assert!(props.attempts.is_empty()); + assert!(props.exec_output_tail.is_none()); + } + #[test] fn git_push_maps_exec_output_tail_to_props() { let stored = to_run_event(&fixtures::RUN_1, &Event::GitPush { branch: "refs/heads/run:refs/heads/run".to_string(), success: false, exec_output_tail: Some(exec_tail()), + attempts: Vec::new(), }); match stored.body { @@ -2669,6 +2841,7 @@ mod tests { automation: Some(automation.clone()), provenance, manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index a38cefbd0..03ffe816a 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -41,6 +41,8 @@ pub enum Event { #[serde(default, skip_serializing_if = "Option::is_none")] manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + spec_blob: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] fork_source_ref: Option, @@ -432,6 +434,9 @@ pub enum Event { success: bool, #[serde(default, skip_serializing_if = "Option::is_none")] exec_output_tail: Option, + /// Per-attempt history of the push operation. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + attempts: Vec, }, GitFetch { branch: String, @@ -1230,14 +1235,16 @@ impl Event { branch, success, exec_output_tail, + attempts, } => { if *success { - debug!(branch, "Git push succeeded"); + debug!(branch, attempts = attempts.len(), "Git push succeeded"); } else { let tail = fabro_types::ExecOutputTail::trace_summary(exec_output_tail.as_ref()); warn!( branch, + attempts = attempts.len(), exec_output_tail_present = tail.present, exec_stdout_tail_bytes = tail.stdout_bytes, exec_stderr_tail_bytes = tail.stderr_bytes, diff --git a/lib/components/fabro-workflow/src/event/sink.rs b/lib/components/fabro-workflow/src/event/sink.rs index f6abb1724..150c69f72 100644 --- a/lib/components/fabro-workflow/src/event/sink.rs +++ b/lib/components/fabro-workflow/src/event/sink.rs @@ -290,6 +290,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/git.rs b/lib/components/fabro-workflow/src/git.rs index de144b6a1..e7ca4feda 100644 --- a/lib/components/fabro-workflow/src/git.rs +++ b/lib/components/fabro-workflow/src/git.rs @@ -364,6 +364,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/github_token_source.rs b/lib/components/fabro-workflow/src/github_token_source.rs deleted file mode 100644 index 2abf95bd3..000000000 --- a/lib/components/fabro-workflow/src/github_token_source.rs +++ /dev/null @@ -1,274 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context as _; -use fabro_github::{GitHubAppCredentials, InstallationToken}; -use tokio::sync::Mutex; -use tracing::warn; - -const REFRESH_THRESHOLD: Duration = Duration::from_mins(15); - -#[async_trait::async_trait] -pub trait IatMinter: Send + Sync { - async fn mint(&self) -> anyhow::Result; -} - -pub struct AppIatMinter { - creds: GitHubAppCredentials, - http: fabro_http::HttpClient, - owner: String, - repo: String, - api_base: String, - install_url: Option, - permissions: serde_json::Value, -} - -impl AppIatMinter { - #[must_use] - pub fn new( - creds: GitHubAppCredentials, - http: fabro_http::HttpClient, - owner: String, - repo: String, - api_base: String, - install_url: Option, - permissions: serde_json::Value, - ) -> Self { - Self { - creds, - http, - owner, - repo, - api_base, - install_url, - permissions, - } - } -} - -#[async_trait::async_trait] -impl IatMinter for AppIatMinter { - async fn mint(&self) -> anyhow::Result { - self.creds - .mint_installation_token( - &self.http, - &self.owner, - &self.repo, - &self.api_base, - self.permissions.clone(), - self.install_url.as_deref(), - ) - .await - } -} - -pub struct GitHubTokenSource { - state: SourceState, -} - -enum SourceState { - Pat(String), - StaticIat(InstallationToken), - Mintable { - minter: Arc, - cache: Mutex>, - }, -} - -impl GitHubTokenSource { - #[must_use] - pub fn pat(token: String) -> Self { - Self { - state: SourceState::Pat(token), - } - } - - #[must_use] - pub fn static_iat(token: InstallationToken) -> Self { - Self { - state: SourceState::StaticIat(token), - } - } - - #[must_use] - pub fn mintable(minter: Arc) -> Self { - Self { - state: SourceState::Mintable { - minter, - cache: Mutex::new(None), - }, - } - } - - #[must_use] - pub fn is_refreshable(&self) -> bool { - matches!(self.state, SourceState::Mintable { .. }) - } - - pub async fn current_token(&self) -> anyhow::Result { - match &self.state { - SourceState::Pat(token) => Ok(token.clone()), - SourceState::StaticIat(token) => token.valid_token().map(str::to_owned), - SourceState::Mintable { minter, cache } => { - let mut cache = cache.lock().await; - let cached_is_fresh = cache - .as_ref() - .is_some_and(|token| !token.near_expiry(REFRESH_THRESHOLD)); - - if !cached_is_fresh { - match minter.mint().await { - Ok(token) => *cache = Some(token), - Err(err) => { - if let Some(token) = cache.as_ref() { - if let Ok(value) = token.valid_token() { - warn!( - error = %err, - "GitHub installation token refresh failed; using cached token" - ); - return Ok(value.to_owned()); - } - } - return Err(err) - .context("failed to mint GitHub installation access token"); - } - } - } - - let token = cache - .as_ref() - .ok_or_else(|| anyhow::anyhow!("mintable token source has no cached token"))?; - token.valid_token().map(str::to_owned) - } - } - } -} - -#[cfg(test)] -mod tests { - use std::collections::VecDeque; - use std::sync::atomic::{AtomicUsize, Ordering}; - - use anyhow::anyhow; - - use super::*; - - enum MintAction { - Token(&'static str, chrono::DateTime), - Error(&'static str), - } - - struct MockMinter { - calls: AtomicUsize, - script: Mutex>, - } - - impl MockMinter { - fn new(script: Vec) -> Self { - Self { - calls: AtomicUsize::new(0), - script: Mutex::new(script.into()), - } - } - - fn calls(&self) -> usize { - self.calls.load(Ordering::SeqCst) - } - } - - #[async_trait::async_trait] - impl IatMinter for MockMinter { - async fn mint(&self) -> anyhow::Result { - self.calls.fetch_add(1, Ordering::SeqCst); - match self.script.lock().await.pop_front().expect("mint script") { - MintAction::Token(token, expires_at) => Ok(InstallationToken { - token: token.to_string(), - expires_at, - }), - MintAction::Error(message) => Err(anyhow!(message)), - } - } - } - - #[tokio::test] - async fn pat_returns_same_token_without_minting() { - let source = GitHubTokenSource::pat("ghp_pat".to_string()); - - assert_eq!(source.current_token().await.unwrap(), "ghp_pat"); - assert_eq!(source.current_token().await.unwrap(), "ghp_pat"); - assert!(!source.is_refreshable()); - } - - #[tokio::test] - async fn static_iat_returns_valid_token_and_rejects_expired_token() { - let valid = GitHubTokenSource::static_iat(InstallationToken { - token: "ghs_valid".to_string(), - expires_at: chrono::Utc::now() + chrono::Duration::minutes(30), - }); - assert_eq!(valid.current_token().await.unwrap(), "ghs_valid"); - assert!(!valid.is_refreshable()); - - let expired = GitHubTokenSource::static_iat(InstallationToken { - token: "ghs_expired".to_string(), - expires_at: chrono::Utc::now() - chrono::Duration::seconds(1), - }); - assert!(expired.current_token().await.is_err()); - } - - #[tokio::test] - async fn mintable_reuses_cached_token_until_refresh_threshold() { - let minter = Arc::new(MockMinter::new(vec![MintAction::Token( - "ghs_cached", - chrono::Utc::now() + chrono::Duration::minutes(30), - )])); - let source = GitHubTokenSource::mintable(minter.clone()); - - assert!(source.is_refreshable()); - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(minter.calls(), 1); - } - - #[tokio::test] - async fn mintable_refreshes_cached_token_near_expiry() { - let minter = Arc::new(MockMinter::new(vec![ - MintAction::Token( - "ghs_first", - chrono::Utc::now() + chrono::Duration::minutes(10), - ), - MintAction::Token( - "ghs_second", - chrono::Utc::now() + chrono::Duration::minutes(30), - ), - ])); - let source = GitHubTokenSource::mintable(minter.clone()); - - assert_eq!(source.current_token().await.unwrap(), "ghs_first"); - assert_eq!(source.current_token().await.unwrap(), "ghs_second"); - assert_eq!(minter.calls(), 2); - } - - #[tokio::test] - async fn mintable_uses_valid_cached_token_when_refresh_fails() { - let minter = Arc::new(MockMinter::new(vec![ - MintAction::Token( - "ghs_cached", - chrono::Utc::now() + chrono::Duration::minutes(10), - ), - MintAction::Error("mint failed"), - ])); - let source = GitHubTokenSource::mintable(minter.clone()); - - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(source.current_token().await.unwrap(), "ghs_cached"); - assert_eq!(minter.calls(), 2); - } - - #[tokio::test] - async fn mintable_errors_when_no_cached_token_can_cover_mint_failure() { - let minter = Arc::new(MockMinter::new(vec![MintAction::Error("mint failed")])); - let source = GitHubTokenSource::mintable(minter); - - let err = format!("{:#}", source.current_token().await.unwrap_err()); - assert!(err.contains("mint failed"), "got: {err}"); - } -} diff --git a/lib/components/fabro-workflow/src/handler/agent.rs b/lib/components/fabro-workflow/src/handler/agent.rs index 48b234a8b..99d20431b 100644 --- a/lib/components/fabro-workflow/src/handler/agent.rs +++ b/lib/components/fabro-workflow/src/handler/agent.rs @@ -501,6 +501,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/handler/command.rs b/lib/components/fabro-workflow/src/handler/command.rs index f4fef254c..db9682a1c 100644 --- a/lib/components/fabro-workflow/src/handler/command.rs +++ b/lib/components/fabro-workflow/src/handler/command.rs @@ -374,6 +374,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, @@ -390,16 +391,19 @@ 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> { - 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>> { @@ -471,6 +475,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -1690,7 +1695,7 @@ mod tests { } #[async_trait::async_trait] - impl crate::github_token_source::IatMinter for RefreshingMinter { + impl fabro_github::test_support::InstallationTokenMinter for RefreshingMinter { async fn mint(&self) -> anyhow::Result { let call = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1; Ok(fabro_github::InstallationToken { @@ -1831,8 +1836,9 @@ mod tests { calls: std::sync::atomic::AtomicUsize::new(0), }); let mut services = make_sandbox_services(spy.clone()); - services.github_token = Some(std::sync::Arc::new( - crate::github_token_source::GitHubTokenSource::mintable(minter.clone()), + services.github_token = Some(fabro_github::test_support::installation_token_source( + "owner/repo", + minter.clone(), )); let handler = CommandHandler; diff --git a/lib/components/fabro-workflow/src/handler/llm/acp.rs b/lib/components/fabro-workflow/src/handler/llm/acp.rs index 10837272a..cfd498beb 100644 --- a/lib/components/fabro-workflow/src/handler/llm/acp.rs +++ b/lib/components/fabro-workflow/src/handler/llm/acp.rs @@ -13,6 +13,7 @@ use fabro_acp::{ use fabro_agent::{ AgentEvent, RefreshOutcome, Sandbox, StaticEnvProvider, SteeringItem, ToolEnvProvider, }; +use fabro_github::token_source::REFRESH_MARGIN; use fabro_graphviz::graph::Node; use fabro_static::EnvVars; use fabro_types::{ @@ -32,8 +33,13 @@ use crate::handler::NodeTimeoutPolicy; use crate::steering_hub::{ActiveControlHandle, SteeringHub}; /// Default refresh-ahead interval — comfortably under the ~60-min GitHub App -/// installation-token TTL. +/// installation-token TTL. Used as the loop cadence when a tick reports no +/// managed credentials; ticks that see a real token reschedule from its +/// expiry instead. const REFRESH_INTERVAL_DEFAULT: Duration = Duration::from_mins(45); +/// Floor for expiry-driven rescheduling, so a token already inside the cache +/// margin cannot pin the loop in a hot cycle. +const REFRESH_RESCHEDULE_FLOOR: Duration = Duration::from_secs(30); /// Upper bound on a single push-credential refresh (token mint + `git remote /// set-url` exec). The turn-entry refresh runs before the ACP process spawns /// and the ACP node uses `NodeTimeoutPolicy::HandlerManaged`, so without this @@ -100,19 +106,39 @@ fn push_cred_refresh_interval() -> Option { ) } -/// Background loop that re-mints the sandbox's push credentials every -/// `interval` for the duration of one ACP turn, so a single turn that outlives -/// the installation-token TTL still pushes with a fresh token. Bounded by -/// `cancel` (the drop-guard cancels it at turn end). A failed or timed-out tick -/// retries after a shorter delay so a transient error does not leave a -/// longer-than-interval window with an expired token. +/// Delay until the next refresh-ahead tick after a successful refresh. +/// +/// With a cached token source, a fixed interval is unsafe: a tick landing +/// just outside the cache margin returns a reused token, and a fixed +/// 45-minute sleep would leave the embedded token expired until the next +/// tick. Schedule from the token's own `expires_at` instead: wake when the +/// cache margin opens, so that tick re-mints. `None` disables the loop — +/// static credentials cannot be re-minted by waiting. +fn next_refresh_delay(outcome: &RefreshOutcome) -> Option { + let token = outcome.token()?; + let expires_at = token.expires_at()?; + let margin = chrono::Duration::from_std(REFRESH_MARGIN).unwrap_or(chrono::Duration::MAX); + let until_margin = ((expires_at - margin) - chrono::Utc::now()) + .to_std() + .unwrap_or(Duration::ZERO); + Some(until_margin.max(REFRESH_RESCHEDULE_FLOOR)) +} + +/// Background loop that keeps the sandbox's push credentials fresh for the +/// duration of one ACP turn, so a single turn that outlives the +/// installation-token TTL still pushes with a fresh token. Bounded by +/// `cancel` (the drop-guard cancels it at turn end). Each successful tick +/// reschedules from the embedded token's expiry ([`next_refresh_delay`]); a +/// failed or timed-out tick retries after a shorter delay so a transient +/// error does not leave a longer-than-interval window with an expired token. async fn refresh_ahead_loop( sandbox: Arc, cancel: CancellationToken, interval: Duration, + initial_delay: Duration, ) { let retry_delay = interval.min(Duration::from_mins(1)); - let mut delay = interval; + let mut delay = initial_delay; loop { tokio::select! { () = cancel.cancelled() => break, @@ -120,19 +146,34 @@ async fn refresh_ahead_loop( match timeout(REFRESH_MINT_TIMEOUT, sandbox.refresh_push_credentials()) .await { - Ok(Ok(RefreshOutcome::Refreshed)) => { - tracing::info!( - interval_secs = interval.as_secs(), - "refresh-ahead re-minted push credentials mid-turn" - ); - delay = interval; - } - Ok(Ok(RefreshOutcome::Skipped)) => { - tracing::debug!( - interval_secs = interval.as_secs(), - "refresh-ahead tick: no managed push credentials to refresh" - ); - delay = interval; + Ok(Ok(outcome)) => { + match outcome { + RefreshOutcome::Embedded(token) => { + tracing::info!( + generation = token.generation, + "refresh-ahead re-embedded push credentials mid-turn" + ); + } + RefreshOutcome::Unchanged(token) => { + tracing::debug!( + generation = token.generation, + "refresh-ahead tick: embedded push credentials still fresh" + ); + } + RefreshOutcome::None => { + tracing::debug!( + "refresh-ahead tick: no managed push credentials to refresh" + ); + } + } + if let Some(next) = next_refresh_delay(&outcome) { + delay = next; + } else { + tracing::debug!( + "refresh-ahead loop stopped: static credentials cannot be re-minted" + ); + break; + } } Ok(Err(e)) => { tracing::warn!( @@ -267,64 +308,57 @@ impl AgentAcpBackend { }) as Arc) + Send + Sync> }); - // Keep the sandbox's push credentials fresh for the duration of this ACP - // turn so the agent's own `git push` uses a live token instead of the one - // baked into the clone at run start. - // - // Part 2 (turn-entry): re-mint + rewrite the origin URL before the ACP - // process spawns, covering a push early in the turn. Non-fatal and - // timeout-bounded — a stalled mint must neither fail nor hang node entry. - // Part 3 (loop): a background task re-mints every ~45 min so a single turn - // that itself outlives the ~60-min installation-token TTL still pushes - // with a fresh token; a normal sub-interval turn never ticks (the - // drop-guard aborts the task at turn end before the first tick). - // - // FABRO_PUSH_CRED_REFRESH_AHEAD=0 (or false/off/no/empty, case- - // insensitive) disables the WHOLE feature — turn-entry re-mint AND loop — - // so an operator who manages `origin` themselves can opt out of all - // fabro-side origin rewriting. FABRO_PUSH_CRED_REFRESH_INTERVAL_SECONDS - // overrides the loop interval; 0 disables just the loop. - // - // Known limitations tracked as follow-ups (not addressed here): (a) - // resumed/parked runs reconnect the sandbox with no GitHub App creds, so - // refresh no-ops until those creds are threaded through the reconnect - // path; (b) the turn-entry re-mint has no freshness check, so it mints - // once per node entry even when the current token is still fresh; (c) the - // background `git remote set-url` can contend with the agent's own git on - // `.git/config.lock`; (d) parallel ACP branches each run their own loop; - // (e) this refresh lives in the ACP handler only, though the stale-origin - // problem is stage-type-agnostic (native/command stages that push are not - // covered); (f) refresh failures are logged via tracing but not surfaced - // as a RunNotice event on the run stream. + // Refresh before launch for early pushes. Schedule later refreshes from + // token expiry so the loop cannot sleep past the cache margin. let refresh_enabled = push_cred_refresh_enabled(); - if refresh_enabled { + let refresh_interval = refresh_enabled.then(push_cred_refresh_interval).flatten(); + let refresh_schedule = if refresh_enabled { match timeout(REFRESH_MINT_TIMEOUT, sandbox.refresh_push_credentials()).await { - Ok(Ok(RefreshOutcome::Refreshed)) => { - tracing::debug!("refreshed sandbox push credentials at ACP turn entry"); + Ok(Ok(outcome)) => { + match outcome { + RefreshOutcome::Embedded(token) => { + tracing::debug!( + generation = token.generation, + "refreshed sandbox push credentials at ACP turn entry" + ); + } + RefreshOutcome::Unchanged(token) => { + tracing::debug!( + generation = token.generation, + "sandbox push credentials already fresh at ACP turn entry" + ); + } + RefreshOutcome::None => {} + } + refresh_interval.zip(next_refresh_delay(&outcome)) } - Ok(Ok(RefreshOutcome::Skipped)) => {} Ok(Err(e)) => { tracing::warn!( error = %fabro_sandbox::display_for_log(&e), "node-entry push-credential refresh failed (non-fatal)" ); + refresh_interval + .map(|interval| (interval, interval.min(Duration::from_mins(1)))) } Err(_elapsed) => { tracing::warn!( timeout_secs = REFRESH_MINT_TIMEOUT.as_secs(), "node-entry push-credential refresh timed out (non-fatal)" ); + refresh_interval + .map(|interval| (interval, interval.min(Duration::from_mins(1)))) } } - } - let _refresh_ahead_guard: Option = refresh_enabled - .then(push_cred_refresh_interval) - .flatten() - .map(|interval| { + } else { + None + }; + let _refresh_ahead_guard: Option = + refresh_schedule.map(|(interval, initial_delay)| { AbortOnDrop(tokio::spawn(refresh_ahead_loop( Arc::clone(sandbox), cancel_token.child_token(), interval, + initial_delay, ))) }); @@ -611,14 +645,18 @@ mod tests { use fabro_acp::test_support::fake_acp_agent_script; use fabro_acp::{AcpError, AcpProcessExit}; - use fabro_agent::{LocalSandbox, RefreshOutcome, Sandbox, shell_quote}; + use fabro_agent::{ + LocalSandbox, RefreshOutcome, RemoteCredentialAction, Sandbox, TokenProvenance, + TokenSnapshot, shell_quote, + }; use fabro_graphviz::graph::{AttrValue, Node}; use fabro_sandbox::test_support::MockSandbox; use fabro_types::{CommandTermination, EventBody, ExecOutputTail}; use tokio_util::sync::CancellationToken; use super::{ - AgentAcpBackend, acp_error_to_workflow, parse_refresh_enabled, parse_refresh_interval, + AgentAcpBackend, REFRESH_RESCHEDULE_FLOOR, acp_error_to_workflow, next_refresh_delay, + parse_refresh_enabled, parse_refresh_interval, refresh_ahead_loop, }; use crate::context::Context; use crate::event::Emitter; @@ -671,17 +709,312 @@ mod tests { } #[tokio::test] - async fn refresh_reports_skipped_without_managed_credentials() { - // MockSandbox uses the trait default (no GitHub App creds), so refresh is - // a no-op that must report Skipped — the signal the refresh-ahead loop - // relies on to log at debug rather than falsely claim a re-mint. + async fn refresh_reports_no_action_without_managed_credentials() { + // MockSandbox uses the trait default (no GitHub App creds), so refresh + // is a no-op that must report no remote action and no token — the + // signal the refresh-ahead loop relies on to log at debug rather than + // falsely claim a re-embed. let sandbox = MockSandbox::linux(); assert_eq!( sandbox.refresh_push_credentials().await.unwrap(), - RefreshOutcome::Skipped + RefreshOutcome::none() ); } + fn minted_outcome( + action: RemoteCredentialAction, + generation: u64, + minted_ago: chrono::Duration, + expires_in: chrono::Duration, + reused: bool, + ) -> RefreshOutcome { + let now = chrono::Utc::now(); + let minted_at = now - minted_ago; + let expires_at = now + expires_in; + let provenance = if reused { + TokenProvenance::Reused { + minted_at, + expires_at, + } + } else { + TokenProvenance::Minted { + minted_at, + expires_at, + } + }; + let token = TokenSnapshot { + generation, + provenance, + }; + match action { + RemoteCredentialAction::Embedded => RefreshOutcome::embedded(token), + RemoteCredentialAction::Unchanged => RefreshOutcome::unchanged(token), + RemoteCredentialAction::None => RefreshOutcome::none(), + } + } + + fn static_outcome() -> RefreshOutcome { + RefreshOutcome::unchanged(TokenSnapshot { + generation: 0, + provenance: TokenProvenance::Static, + }) + } + + #[test] + fn next_refresh_delay_schedules_from_token_expiry_minus_margin() { + let outcome = minted_outcome( + RemoteCredentialAction::Embedded, + 1, + chrono::Duration::zero(), + chrono::Duration::minutes(60), + false, + ); + let delay = next_refresh_delay(&outcome).unwrap(); + // Expiry minus the 10-minute refresh margin: ~50 minutes out. + assert!(delay > Duration::from_mins(49), "{delay:?}"); + assert!(delay <= Duration::from_mins(50), "{delay:?}"); + } + + #[test] + fn next_refresh_delay_floors_when_the_margin_is_already_open() { + let outcome = minted_outcome( + RemoteCredentialAction::Unchanged, + 1, + chrono::Duration::minutes(55), + chrono::Duration::minutes(5), + true, + ); + assert_eq!(next_refresh_delay(&outcome), Some(REFRESH_RESCHEDULE_FLOOR)); + } + + #[test] + fn next_refresh_delay_disables_the_loop_for_static_credentials() { + assert_eq!(next_refresh_delay(&static_outcome()), None); + } + + #[test] + fn next_refresh_delay_disables_the_loop_without_managed_credentials() { + assert_eq!(next_refresh_delay(&RefreshOutcome::none()), None); + } + + /// Sandbox stub whose refresh outcomes are scripted, recording when each + /// refresh tick lands on the (paused) tokio clock. + struct ScriptedRefreshSandbox { + script: Mutex>, + ticks: Mutex>, + } + + impl ScriptedRefreshSandbox { + fn new(script: Vec) -> Arc { + Arc::new(Self { + script: Mutex::new(script.into()), + ticks: Mutex::new(Vec::new()), + }) + } + + fn ticks(&self) -> Vec { + self.ticks.lock().expect("ticks lock").clone() + } + } + + #[async_trait::async_trait] + impl Sandbox for ScriptedRefreshSandbox { + async fn refresh_push_credentials(&self) -> fabro_sandbox::Result { + self.ticks + .lock() + .expect("ticks lock") + .push(tokio::time::Instant::now()); + Ok(self + .script + .lock() + .expect("script lock") + .pop_front() + .expect("refresh script exhausted")) + } + + async fn read_file_bytes(&self, _path: &str) -> fabro_sandbox::Result> { + unimplemented!("refresh loop only calls refresh_push_credentials") + } + + async fn write_file(&self, _path: &str, _content: &str) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn delete_file(&self, _path: &str) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn file_exists(&self, _path: &str) -> fabro_sandbox::Result { + unimplemented!() + } + + async fn list_directory( + &self, + _path: &str, + _depth: Option, + ) -> fabro_sandbox::Result> { + unimplemented!() + } + + async fn exec_command( + &self, + _command: &str, + _timeout_ms: u64, + _working_dir: Option<&str>, + _env_vars: Option<&HashMap>, + _cancel_token: Option, + ) -> fabro_sandbox::Result { + unimplemented!() + } + + async fn grep( + &self, + _pattern: &str, + _path: &str, + _options: &fabro_sandbox::GrepOptions, + ) -> fabro_sandbox::Result> { + unimplemented!() + } + + async fn download_file_to_local( + &self, + _remote_path: &str, + _local_path: &std::path::Path, + ) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn upload_file_from_local( + &self, + _local_path: &std::path::Path, + _remote_path: &str, + ) -> fabro_sandbox::Result<()> { + unimplemented!() + } + + async fn initialize(&self) -> fabro_sandbox::Result<()> { + Ok(()) + } + + async fn cleanup(&self) -> fabro_sandbox::Result<()> { + Ok(()) + } + + fn working_directory(&self) -> &str { + "/workspace" + } + + fn platform(&self) -> &str { + "linux" + } + + fn os_version(&self) -> String { + "linux".to_string() + } + } + + /// Long-turn timeline: the clone/turn-entry mint happened at minute 0 with + /// a 60-minute TTL. The loop's first tick at minute 45 sees the cached + /// token reused with ~15 minutes left and must NOT sleep another fixed 45 + /// minutes (that would cross expiry at minute 60) — it reschedules for the + /// margin opening (~5 minutes out). That margin-crossing tick re-mints and + /// reschedules from the fresh token's expiry (~50 minutes out). + #[tokio::test(start_paused = true)] + async fn refresh_ahead_reschedules_from_token_expiry_across_a_long_turn() { + let interval = Duration::from_mins(45); + let sandbox = ScriptedRefreshSandbox::new(vec![ + // Minute 45: cache still fresh (expires minute 60, margin opens + // minute 50). + minted_outcome( + RemoteCredentialAction::Unchanged, + 1, + chrono::Duration::minutes(45), + chrono::Duration::minutes(15), + true, + ), + // Minute ~50: margin open → the source minted generation 2. + minted_outcome( + RemoteCredentialAction::Embedded, + 2, + chrono::Duration::zero(), + chrono::Duration::minutes(60), + false, + ), + // Minute ~100: generation 2 still fresh. + minted_outcome( + RemoteCredentialAction::Unchanged, + 2, + chrono::Duration::minutes(50), + chrono::Duration::minutes(10), + true, + ), + ]); + let cancel = CancellationToken::new(); + let start = tokio::time::Instant::now(); + let loop_task = tokio::spawn(refresh_ahead_loop( + Arc::clone(&sandbox) as Arc, + cancel.clone(), + interval, + interval, + )); + + while sandbox.ticks().len() < 3 { + tokio::time::sleep(Duration::from_secs(1)).await; + } + cancel.cancel(); + loop_task.await.expect("refresh loop should exit cleanly"); + + let ticks = sandbox.ticks(); + assert_eq!(ticks[0] - start, interval, "first tick uses the interval"); + // Reused token expiring in 15 minutes → next tick when the 10-minute + // margin opens, ~5 minutes later (never another fixed 45 minutes). + let second_gap = ticks[1] - ticks[0]; + assert!(second_gap <= Duration::from_mins(5), "{second_gap:?}"); + assert!(second_gap > Duration::from_mins(4), "{second_gap:?}"); + // Fresh 60-minute token → next tick ~50 minutes out. + let third_gap = ticks[2] - ticks[1]; + assert!(third_gap <= Duration::from_mins(50), "{third_gap:?}"); + assert!(third_gap > Duration::from_mins(49), "{third_gap:?}"); + } + + #[tokio::test(start_paused = true)] + async fn refresh_ahead_honors_the_expiry_based_initial_delay() { + let interval = Duration::from_mins(45); + let entry_outcome = minted_outcome( + RemoteCredentialAction::Unchanged, + 1, + chrono::Duration::minutes(45), + chrono::Duration::minutes(15), + true, + ); + let initial_delay = next_refresh_delay(&entry_outcome).unwrap(); + let sandbox = ScriptedRefreshSandbox::new(vec![minted_outcome( + RemoteCredentialAction::Embedded, + 2, + chrono::Duration::zero(), + chrono::Duration::minutes(60), + false, + )]); + let cancel = CancellationToken::new(); + let start = tokio::time::Instant::now(); + let loop_task = tokio::spawn(refresh_ahead_loop( + Arc::clone(&sandbox) as Arc, + cancel.clone(), + interval, + initial_delay, + )); + + while sandbox.ticks().is_empty() { + tokio::time::sleep(Duration::from_secs(1)).await; + } + cancel.cancel(); + loop_task.await.expect("refresh loop should exit cleanly"); + + let first_tick = sandbox.ticks()[0] - start; + assert!(first_tick <= Duration::from_mins(5), "{first_tick:?}"); + assert!(first_tick > Duration::from_mins(4), "{first_tick:?}"); + } + #[tokio::test] async fn acp_backend_run_sends_prompt_and_returns_text() { let tempdir = tempfile::tempdir().unwrap(); diff --git a/lib/components/fabro-workflow/src/handler/parallel.rs b/lib/components/fabro-workflow/src/handler/parallel.rs index f20d5d5f5..82773470b 100644 --- a/lib/components/fabro-workflow/src/handler/parallel.rs +++ b/lib/components/fabro-workflow/src/handler/parallel.rs @@ -956,6 +956,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -1917,7 +1918,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 +1934,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/handler/prompt.rs b/lib/components/fabro-workflow/src/handler/prompt.rs index d586ca6b7..630332878 100644 --- a/lib/components/fabro-workflow/src/handler/prompt.rs +++ b/lib/components/fabro-workflow/src/handler/prompt.rs @@ -279,6 +279,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/lib.rs b/lib/components/fabro-workflow/src/lib.rs index c34bec62c..3178542d7 100644 --- a/lib/components/fabro-workflow/src/lib.rs +++ b/lib/components/fabro-workflow/src/lib.rs @@ -293,7 +293,6 @@ pub mod error; pub mod event; pub mod file_resolver; pub mod git; -pub mod github_token_source; pub(crate) mod graph; pub mod handler; mod hook_context; diff --git a/lib/components/fabro-workflow/src/lifecycle/event.rs b/lib/components/fabro-workflow/src/lifecycle/event.rs index 8922708e0..6303ea1f3 100644 --- a/lib/components/fabro-workflow/src/lifecycle/event.rs +++ b/lib/components/fabro-workflow/src/lifecycle/event.rs @@ -493,6 +493,7 @@ impl RunLifecycle for EventLifecycle { branch: push.branch.clone(), success: push.success, exec_output_tail: push.exec_output_tail.clone(), + attempts: push.attempts.clone(), }); } } diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index 73100e6c1..b8e06088e 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -18,7 +18,10 @@ use crate::event::{Emitter, Event, RunNoticeCode, RunNoticeLevel, StageScope}; use crate::graph::{WorkflowGraph, WorkflowNode}; use crate::lifecycle::event::stage_scope_for; use crate::outcome::BilledModelUsage; -use crate::run_metadata::{MetadataSnapshot, RunMetadataRuntime, RunMetadataWriterHandle}; +use crate::run_metadata::{ + MetadataSnapshot, RunMetadataRuntime, RunMetadataWriterHandle, + metadata_push_failure_is_transient, +}; use crate::run_options::RunOptions; use crate::runtime_store::RunStoreHandle; use crate::sandbox_git::{ @@ -72,18 +75,22 @@ pub(crate) struct PushResult { pub branch: String, pub success: bool, pub exec_output_tail: Option, + pub attempts: Vec, } /// Push a run branch to its remote counterpart. /// /// Owns the refspec convention so the checkpoint push and the terminal publish -/// push cannot drift apart. +/// push cannot drift apart. The caller picks the retry budget: cheap for +/// checkpoint pushes (the next checkpoint re-pushes the same branch anyway), +/// generous for the terminal publish push. pub(crate) async fn push_run_branch( sandbox: &dyn fabro_sandbox::Sandbox, branch: &str, -) -> fabro_sandbox::Result<()> { + plan: &fabro_sandbox::RetryPlan, +) -> Result { sandbox - .git_push_ref(&format!("refs/heads/{branch}:refs/heads/{branch}")) + .git_push_ref(&format!("refs/heads/{branch}:refs/heads/{branch}"), plan) .await } @@ -117,7 +124,7 @@ impl RunLifecycle for GitLifecycle { "git lifecycle mutex should not be poisoned: no code panics while holding this lock", ) = None; if let Some(meta_branch) = self.metadata_branch().map(str::to_string) { - if self.metadata_writer.is_none() || self.metadata_runtime.metadata_degraded() { + if self.metadata_writer.is_none() || self.metadata_runtime.metadata_suspended() { return Ok(()); } let phase = MetadataSnapshotPhase::Init; @@ -154,6 +161,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); } }, @@ -174,6 +182,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); } } @@ -208,7 +217,9 @@ impl RunLifecycle for GitLifecycle { None, ); let shadow_sha = if let Some(meta_branch) = self.metadata_branch().map(str::to_string) { - if self.metadata_writer.is_none() || self.metadata_runtime.metadata_degraded() { + if self.metadata_writer.is_none() + || self.metadata_runtime.metadata_checkpoint_suspended() + { None } else { let phase = MetadataSnapshotPhase::Checkpoint; @@ -253,6 +264,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); None } @@ -276,6 +288,7 @@ impl RunLifecycle for GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); None } @@ -320,30 +333,41 @@ impl RunLifecycle for GitLifecycle { .as_ref() .and_then(|g| g.run_branch.as_ref()) { - let (push_ok, exec_output_tail) = - match push_run_branch(self.sandbox.as_ref(), branch).await { - Ok(()) => (true, None), - Err(err) => { + let plan = fabro_sandbox::RetryPlan::checkpoint_push(); + let (push_ok, exec_output_tail, attempts) = + match push_run_branch(self.sandbox.as_ref(), branch, &plan).await { + Ok(report) => { + self.sandbox_git.record_successful_push(); + (true, None, report.attempts) + } + Err(push_error) => { let exec_output_tail = - fabro_sandbox::default_redacted_output_tail(&err); + fabro_sandbox::default_redacted_output_tail( + &push_error.error, + ); tracing::warn!( branch = %branch, - error = %fabro_sandbox::display_for_log(&err), + attempts = push_error.report.attempts.len(), + error = %fabro_sandbox::display_for_log(&push_error.error), "git push from run lifecycle failed" ); self.emitter.notice_with_tail( RunNoticeLevel::Warn, RunNoticeCode::GitPushFailed, - format!("Failed to push run branch {branch}: {err}"), + format!( + "Failed to push run branch {branch}: {}", + push_error.error + ), exec_output_tail.clone(), ); - (false, exec_output_tail) + (false, exec_output_tail, push_error.report.attempts) } }; git_result.push_results.push(PushResult { branch: branch.clone(), success: push_ok, exec_output_tail, + attempts, }); } } @@ -452,7 +476,10 @@ impl GitLifecycle { message: &str, scope: Option<&StageScope>, ) -> Option { - if self.metadata_runtime.metadata_degraded() { + if self.metadata_runtime.metadata_suspended() + || (phase == MetadataSnapshotPhase::Checkpoint + && self.metadata_runtime.metadata_checkpoint_suspended()) + { return None; } let writer = self.metadata_writer.as_ref()?; @@ -477,8 +504,12 @@ impl GitLifecycle { self.emit_metadata_warning( RunNoticeCode::CheckpointMetadataPushFailed, message, + metadata_push_failure_is_transient(detail, snapshot.token.as_ref()), ); } else { + // One good snapshot ends the degradation; a later + // independent failure warns again. + self.metadata_runtime.clear_metadata_degraded(); self.emit_metadata_snapshot_completed( phase, meta_branch, @@ -503,7 +534,11 @@ impl GitLifecycle { None, scope, ); - self.emit_metadata_warning(RunNoticeCode::CheckpointMetadataWriteFailed, message); + self.emit_metadata_warning( + RunNoticeCode::CheckpointMetadataWriteFailed, + message, + false, + ); None } } @@ -588,8 +623,8 @@ impl GitLifecycle { } } - fn emit_metadata_warning(&self, code: RunNoticeCode, message: String) { - if self.metadata_runtime.mark_metadata_degraded() { + fn emit_metadata_warning(&self, code: RunNoticeCode, message: String, transient: bool) { + if self.metadata_runtime.mark_metadata_degraded(transient) { self.emitter.notice(RunNoticeLevel::Warn, code, message); } } @@ -750,6 +785,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -1202,7 +1238,7 @@ mod tests { let repo_dir = tempfile::tempdir().unwrap(); init_git_repo(repo_dir.path()); let runtime = Arc::new(RunMetadataRuntime::new()); - runtime.mark_metadata_degraded(); + runtime.mark_metadata_degraded(false); let emitter = Arc::new(Emitter::new(fixtures::RUN_1)); let events = record_events(&emitter); let lifecycle = git_lifecycle( @@ -1328,7 +1364,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/operations/archive.rs b/lib/components/fabro-workflow/src/operations/archive.rs index c44ff0006..92d9124cf 100644 --- a/lib/components/fabro-workflow/src/operations/archive.rs +++ b/lib/components/fabro-workflow/src/operations/archive.rs @@ -227,6 +227,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/operations/create.rs b/lib/components/fabro-workflow/src/operations/create.rs index 59f424e2d..7783a9b59 100644 --- a/lib/components/fabro-workflow/src/operations/create.rs +++ b/lib/components/fabro-workflow/src/operations/create.rs @@ -13,10 +13,11 @@ use std::sync::Arc; use fabro_config::Storage; use fabro_graphviz::graph::{AttrValue, Graph}; use fabro_model::{Catalog, ProviderId}; -use fabro_store::Database; +use fabro_store::{Database, RunDatabase}; use fabro_template::TemplateContext; use fabro_types::{ - AutomationRef, ForkSourceRef, GitContext, ManifestPath, RunId, RunProvenance, WorkflowSettings, + AutomationRef, BlobHash, ForkSourceRef, GitContext, ManifestPath, RunId, RunProvenance, + WorkflowSettings, }; use fabro_util::json::normalize_json_value; use tokio::task::spawn_blocking; @@ -470,6 +471,7 @@ pub async fn persist_create_run( provenance, manifest_blob: None, definition_blob: None, + spec_blob: None, git, fork_source_ref, }; @@ -516,18 +518,17 @@ async fn persist_created_run( .create_run(&record.run_id) .await .map_err(|err| Error::engine_with_source("failed to create run store", err))?; - let manifest_blob = match submitted_manifest_bytes { - Some(bytes) => Some(run_store.write_blob(bytes).await.map_err(store_error)?), - None => None, - }; - let definition_blob = match accepted_definition { - Some(definition) => { - let bytes = - serde_json::to_vec(definition).map_err(|err| Error::engine(err.to_string()))?; - Some(run_store.write_blob(&bytes).await.map_err(store_error)?) - } - None => None, - }; + let definition_bytes = accepted_definition + .map(serde_json::to_vec) + .transpose() + .map_err(|err| Error::engine_with_source("failed to serialize run definition", err))?; + let spec_bytes = serde_json::to_vec(record) + .map_err(|err| Error::engine_with_source("failed to serialize run spec", err))?; + let (manifest_blob, definition_blob, spec_blob) = tokio::try_join!( + write_optional_blob(&run_store, submitted_manifest_bytes), + write_optional_blob(&run_store, definition_bytes.as_deref()), + async { run_store.write_blob(&spec_bytes).await.map_err(store_error) }, + )?; let title = explicit_title.unwrap_or_else(|| fabro_types::infer_run_title(record.graph.goal())); let stored = to_run_event_at( @@ -554,6 +555,7 @@ async fn persist_created_run( automation: record.automation.clone(), provenance: record.provenance.clone(), manifest_blob, + spec_blob: Some(spec_blob), git: record.git.clone(), fork_source_ref: record.fork_source_ref.clone(), retried_from: None, @@ -580,8 +582,22 @@ async fn persist_created_run( .map_err(store_error) } -fn store_error(err: impl std::fmt::Display) -> Error { - Error::engine(err.to_string()) +async fn write_optional_blob( + run_store: &RunDatabase, + bytes: Option<&[u8]>, +) -> Result, Error> { + match bytes { + Some(bytes) => run_store + .write_blob(bytes) + .await + .map(Some) + .map_err(store_error), + None => Ok(None), + } +} + +fn store_error(err: impl Into) -> Error { + Error::engine_with_source("run store operation failed", err) } /// Parse, transform, and validate `dot_source`. diff --git a/lib/components/fabro-workflow/src/operations/fork.rs b/lib/components/fabro-workflow/src/operations/fork.rs index 7e5e65516..3f728211d 100644 --- a/lib/components/fabro-workflow/src/operations/fork.rs +++ b/lib/components/fabro-workflow/src/operations/fork.rs @@ -162,6 +162,9 @@ async fn persist_forked_run( automation: spec.automation.clone(), provenance: spec.provenance.clone(), manifest_blob: spec.manifest_blob, + // Content-addressed, so the forked run reads the source run's + // unredacted spec bytes through the same id. + spec_blob: spec.spec_blob, git: spec.git.clone(), fork_source_ref: spec.fork_source_ref.clone(), retried_from: None, @@ -381,6 +384,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: Some(fabro_types::GitContext { origin_url: "https://github.com/example/repo.git".to_string(), branch: "main".to_string(), diff --git a/lib/components/fabro-workflow/src/operations/retry.rs b/lib/components/fabro-workflow/src/operations/retry.rs index 8163efa55..010c08f3f 100644 --- a/lib/components/fabro-workflow/src/operations/retry.rs +++ b/lib/components/fabro-workflow/src/operations/retry.rs @@ -54,6 +54,7 @@ pub async fn retry_run( provenance: _, manifest_blob, definition_blob, + spec_blob, git, fork_source_ref, } = source.spec; @@ -78,6 +79,9 @@ pub async fn retry_run( automation, provenance: input.provenance.clone(), manifest_blob, + // Blobs are content-addressed, so the retried run reads the source + // run's unredacted spec bytes through the same id. + spec_blob, git, fork_source_ref, retried_from: Some(source_run_id), @@ -185,6 +189,7 @@ mod tests { automation: None, provenance: provenance("source-user"), manifest_blob, + spec_blob: None, git: Some(git_context()), fork_source_ref, retried_from: None, diff --git a/lib/components/fabro-workflow/src/operations/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/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index dcb352693..3ba470151 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, }; @@ -572,15 +572,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/operations/timeline.rs b/lib/components/fabro-workflow/src/operations/timeline.rs index b96d10aa4..83319745e 100644 --- a/lib/components/fabro-workflow/src/operations/timeline.rs +++ b/lib/components/fabro-workflow/src/operations/timeline.rs @@ -252,6 +252,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, diff --git a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs index 118767be8..7bc1d6dff 100644 --- a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs @@ -173,6 +173,7 @@ fn persisted_workflow(graph: Graph, source: String, run_dir: &Path, run_id: RunI provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }, ) @@ -218,6 +219,7 @@ async fn seed_created_and_starting( automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: run_options.pre_run_git.clone(), fork_source_ref: run_options.fork_source_ref.clone(), retried_from: None, @@ -751,11 +753,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/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index 9f43e86ff..9bdb18c39 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -14,7 +14,7 @@ use crate::error::{Error, run_failure_from_error, run_failure_from_outcome_failu use crate::event::{Event, RunNoticeCode, RunNoticeLevel}; use crate::outcome::{Outcome, StageOutcome}; use crate::records::{Checkpoint, Conclusion, StageSummary}; -use crate::run_metadata::MetadataSnapshot; +use crate::run_metadata::{MetadataSnapshot, metadata_push_failure_is_transient}; use crate::run_options::RunOptions; use crate::run_status::{FailureReason, RunStatus, SuccessReason}; use crate::runtime_store::RunStoreHandle; @@ -202,7 +202,7 @@ pub async fn write_finalize_commit( services: &RunServices, conclusion: &Conclusion, ) { - if services.metadata_runtime.metadata_degraded() { + if services.metadata_runtime.metadata_suspended() { return; } let Some(writer) = services.metadata_writer.as_ref() else { @@ -240,6 +240,7 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); return; } @@ -265,6 +266,7 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); return; } @@ -290,8 +292,10 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataPushFailed, message, + metadata_push_failure_is_transient(detail, snapshot.token.as_ref()), ); } else { + services.metadata_runtime.clear_metadata_degraded(); emit_metadata_snapshot_completed(services, phase, meta_branch, started, &snapshot); } } @@ -313,6 +317,7 @@ pub async fn write_finalize_commit( services, RunNoticeCode::CheckpointMetadataWriteFailed, message, + false, ); } } @@ -376,8 +381,13 @@ fn emit_metadata_snapshot_failed( }); } -fn emit_metadata_warning(services: &RunServices, code: RunNoticeCode, message: String) { - if services.metadata_runtime.mark_metadata_degraded() { +fn emit_metadata_warning( + services: &RunServices, + code: RunNoticeCode, + message: String, + transient: bool, +) { + if services.metadata_runtime.mark_metadata_degraded(transient) { services.emitter.notice(RunNoticeLevel::Warn, code, message); } } @@ -788,6 +798,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -906,6 +917,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, @@ -1257,7 +1269,7 @@ mod tests { let emitter = Arc::new(Emitter::new(test_run_id())); let events = record_events(&emitter); let runtime = Arc::new(RunMetadataRuntime::new()); - runtime.mark_metadata_degraded(); + runtime.mark_metadata_degraded(false); let services = test_services( RunStoreHandle::local(run_store), emitter, @@ -1461,7 +1473,18 @@ mod tests { ); let events = events.lock().unwrap(); let names = events.iter().map(RunEvent::event_name).collect::>(); + // Exactly one durable git.push event per high-level push — retries + // nest inside it as attempts, never as extra events. assert_eq!(names, vec!["git.push", "run.failed"]); + match &events.first().unwrap().body { + EventBody::GitPush(props) => { + assert!(!props.success); + // MockSandbox's default git_push_ref fails before any attempt + // runs, so the nested history is empty here. + assert!(props.attempts.is_empty()); + } + other => panic!("expected git.push, got {other:?}"), + } match &events.last().unwrap().body { EventBody::RunFailed(props) => { assert_eq!(props.failure.reason, FailureReason::PublishFailed); @@ -1823,7 +1846,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/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 5a7dd6069..1c4711b7f 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -7,6 +7,7 @@ use fabro_agent::{Sandbox, ToolSecrets}; use fabro_auth::{ CredentialSource, ExtraHeadersCredentialSource, VaultCredentialSource, auth_issue_message, }; +use fabro_github::token_source::InstallationTokenSource; use fabro_graphviz::graph; use fabro_hooks::{HookContext, HookDecision, HookEvent, HookExecutionContext, HookRunner}; use fabro_model::Catalog; @@ -23,7 +24,6 @@ use super::types::{InitOptions, Initialized, LlmSpec, Persisted, SandboxEnvSpec} use crate::error::Error; use crate::event::{Event, RunNoticeCode, RunNoticeLevel}; use crate::git::GitAuthor; -use crate::github_token_source::{AppIatMinter, GitHubTokenSource}; use crate::handler::llm::{AgentAcpBackend, AgentApiBackend, BackendRouter, routing}; use crate::handler::{HandlerRegistry, default_registry}; #[cfg(test)] @@ -37,7 +37,10 @@ use crate::services::{ use crate::stage_execution::{StageExecutionSeed, StageExecutionTracker}; use crate::steering_hub::SteeringHub; -type BuiltSandboxEnv = (HashMap, Option>); +type BuiltSandboxEnv = ( + HashMap, + Option>, +); async fn run_hooks( hook_runner: Option<&HookRunner>, @@ -99,12 +102,12 @@ fn build_sandbox_env( let source = match creds { fabro_github::GitHubCredentials::Pat(token) => { - Some(Arc::new(GitHubTokenSource::pat(token.clone()))) + Some(InstallationTokenSource::pat(token.clone())) } fabro_github::GitHubCredentials::Installation(token) => { - Some(Arc::new(GitHubTokenSource::static_iat(token.clone()))) + Some(InstallationTokenSource::installation(token.clone())) } - fabro_github::GitHubCredentials::App(app) => { + fabro_github::GitHubCredentials::App(_) => { let Some(origin_url) = spec.origin_url.as_deref() else { return Ok((env, None)); }; @@ -114,19 +117,11 @@ fn build_sandbox_env( let permissions = serde_json::to_value(permissions).map_err(|err| { Error::engine_with_source("Failed to serialize GitHub permissions", err) })?; - let http = fabro_http::http_client() - .map_err(|err| Error::engine_with_source("Failed to build HTTP client", err))?; - let install_url = app.installation_url(&owner); - let minter = AppIatMinter::new( - app.clone(), - http, - owner, - repo, - fabro_github::github_api_base_url(), - install_url, - permissions, - ); - Some(Arc::new(GitHubTokenSource::mintable(Arc::new(minter)))) + Some( + InstallationTokenSource::for_repository(creds, owner, repo, permissions).map_err( + |err| Error::engine_with_anyhow("Failed to build GitHub token source", err), + )?, + ) } }; @@ -458,7 +453,7 @@ pub async fn initialize( }); let github_token_refresh_managed = github_token .as_deref() - .is_some_and(GitHubTokenSource::is_refreshable); + .is_some_and(InstallationTokenSource::mints_installation_tokens); let (registry, effective_dry_run) = if let Some(registry) = options.registry_override.clone() { // A caller-supplied registry owns execution behavior for its handlers. (registry, options.dry_run) @@ -600,20 +595,21 @@ pub async fn initialize( }); } - let metadata_writer = match build_metadata_writer(&options.run_options) { - Ok(writer) => writer, - Err(err) => { - let message = format!("failed to initialize checkpoint metadata writer: {err}"); - if metadata_runtime.mark_metadata_degraded() { - options.emitter.notice( - RunNoticeLevel::Warn, - RunNoticeCode::CheckpointMetadataWriteFailed, - message, - ); + let metadata_writer = + match build_metadata_writer(&options.run_options, sandbox.push_token_source()) { + Ok(writer) => writer, + Err(err) => { + let message = format!("failed to initialize checkpoint metadata writer: {err}"); + if metadata_runtime.mark_metadata_degraded(false) { + options.emitter.notice( + RunNoticeLevel::Warn, + RunNoticeCode::CheckpointMetadataWriteFailed, + message, + ); + } + None } - None - } - }; + }; let run_services = RunServices::new( options.run_store.clone(), @@ -870,6 +866,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref, }, ) @@ -1053,6 +1050,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: run_options.fork_source_ref.clone(), retried_from: None, diff --git a/lib/components/fabro-workflow/src/pipeline/persist.rs b/lib/components/fabro-workflow/src/pipeline/persist.rs index cc12c3cba..303241ff5 100644 --- a/lib/components/fabro-workflow/src/pipeline/persist.rs +++ b/lib/components/fabro-workflow/src/pipeline/persist.rs @@ -2,6 +2,7 @@ use std::path::Path; use super::types::{PersistOptions, Persisted, Validated}; use crate::error::Error; +use crate::records::RunSpec; use crate::runtime_store::RunStoreHandle; /// PERSIST phase: create the run directory and return durable metadata for @@ -37,7 +38,7 @@ pub(crate) async fn load_from_store( .state() .await .map_err(|err| Error::engine(err.to_string()))?; - let run_spec = state.spec; + let run_spec = executable_run_spec(run_store, state.spec).await?; let graph = run_spec.graph.clone(); let source = run_spec.graph_source.clone().unwrap_or_default(); @@ -50,6 +51,41 @@ pub(crate) async fn load_from_store( )) } +/// Replace the event-folded spec content with the exact bytes from the spec +/// blob. Stored events pass through secret redaction, so the folded spec is +/// display data; the blob written at creation is what execution must see. +/// Runs created before the blob existed fall back to the folded spec. +async fn executable_run_spec( + run_store: &RunStoreHandle, + folded: RunSpec, +) -> Result { + let Some(blob_id) = folded.spec_blob else { + return Ok(folded); + }; + let bytes = run_store + .read_blob(&blob_id) + .await + .map_err(|err| Error::engine_with_anyhow("failed to read run spec blob", err))? + .ok_or_else(|| { + Error::engine(format!( + "run spec blob is missing from the run store: {blob_id}" + )) + })?; + let mut spec: RunSpec = serde_json::from_slice(&bytes) + .map_err(|err| Error::engine_with_source("run spec blob was not valid JSON", err))?; + // The event stream stays authoritative for run identity, provenance, and + // blob ids. Prefer the unredacted graph source from the blob, with the + // folded source as a compatibility fallback. + spec.run_id = folded.run_id; + spec.provenance = folded.provenance; + spec.manifest_blob = folded.manifest_blob; + spec.definition_blob = folded.definition_blob; + spec.spec_blob = folded.spec_blob; + spec.fork_source_ref = folded.fork_source_ref; + spec.graph_source = spec.graph_source.or(folded.graph_source); + Ok(spec) +} + #[cfg(test)] #[expect(clippy::disallowed_methods, reason = "tests stage pipeline fixtures")] mod tests { @@ -150,30 +186,49 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, } } async fn seeded_store(record: &RunSpec, source: Option<&str>) -> RunDatabase { + seeded_store_with(record, source, Some(record)).await + } + + async fn seeded_store_with( + record: &RunSpec, + source: Option<&str>, + blob_record: Option<&RunSpec>, + ) -> RunDatabase { let store = memory_store(); let run_store = store.create_run(&record.run_id).await.unwrap(); + let spec_blob = match blob_record { + Some(blob_record) => Some( + run_store + .write_blob(&serde_json::to_vec(blob_record).unwrap()) + .await + .unwrap(), + ), + None => None, + }; append_event(&run_store, &record.run_id, &Event::RunCreated { - run_id: record.run_id, - title: None, - settings: serde_json::to_value(&record.settings).unwrap(), - graph: serde_json::to_value(&record.graph).unwrap(), - workflow_source: source.map(ToOwned::to_owned), - labels: record.labels.clone().into_iter().collect(), + run_id: record.run_id, + title: None, + settings: serde_json::to_value(&record.settings).unwrap(), + graph: serde_json::to_value(&record.graph).unwrap(), + workflow_source: source.map(ToOwned::to_owned), + labels: record.labels.clone().into_iter().collect(), source_directory: record.source_directory.clone(), - workflow_slug: record.workflow_slug.clone(), - automation: record.automation.clone(), - provenance: record.provenance.clone(), - manifest_blob: None, - git: record.git.clone(), - fork_source_ref: record.fork_source_ref.clone(), - retried_from: None, - parent_id: None, - web_url: None, + workflow_slug: record.workflow_slug.clone(), + automation: record.automation.clone(), + provenance: record.provenance.clone(), + manifest_blob: None, + spec_blob, + git: record.git.clone(), + fork_source_ref: record.fork_source_ref.clone(), + retried_from: None, + parent_id: None, + web_url: None, }) .await .unwrap(); @@ -272,6 +327,96 @@ mod tests { assert!(loaded.diagnostics().is_empty()); } + #[tokio::test] + async fn load_from_store_preserves_high_entropy_dockerfile_content() { + // The spec the worker executes must survive the store byte-identical. + // Event redaction is a storage/display concern; when it reaches the + // spec that `load_from_store` rehydrates, the sandbox builds a + // corrupted Dockerfile: `ARG NAME=` pairs come back as + // `ARG REDACTED`, the build's `set -eu` step fails on the unset + // variable, and the environment's snapshot identity silently changes. + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + + // Two shapes that must both survive: the hex pins that triggered the + // production failure, and a token high-entropy enough that any + // detector will keep flagging it in stored events. The second keeps + // this test red until execution stops reading redacted content, + // independent of how the entropy heuristic evolves. + let dockerfile = "FROM buildpack-deps:noble\n\ + ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf\n\ + ARG DOCKER_INSTALL_SHA256=b991f2806186f7287bb9e53362060c382e906d154599b2fb0982f34246bacfd4\n\ + ENV CACHE_SALT=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p\n\ + RUN install-docker \"${DOCKER_INSTALL_COMMIT}\" \"${DOCKER_INSTALL_SHA256}\"\n"; + + let mut record = sample_record(different_graph()); + record.graph = graph; + record.settings.run.environment.image.dockerfile = Some( + fabro_types::settings::run::DockerfileSource::Inline(dockerfile.to_string()), + ); + + let run_store = seeded_store(&record, Some(&source)).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!( + loaded.run_spec().settings.run.environment.image.dockerfile, + Some(fabro_types::settings::run::DockerfileSource::Inline( + dockerfile.to_string() + )), + "the executable run spec must round-trip through the store unredacted" + ); + } + + #[tokio::test] + async fn load_from_store_falls_back_to_folded_spec_without_spec_blob() { + // Runs created before the spec blob existed carry no spec_blob on + // run.created; the folded spec is their only copy. + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + let mut record = sample_record(different_graph()); + record.graph = graph; + + let run_store = seeded_store_with(&record, Some(&source), None).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!(loaded.run_spec().settings, record.settings); + assert_eq!(loaded.run_spec().spec_blob, None); + } + + #[tokio::test] + async fn load_from_store_uses_fork_reference_from_event_fold() { + let temp = tempfile::tempdir().unwrap(); + let run_dir = temp.path().join("run"); + std::fs::create_dir_all(&run_dir).unwrap(); + let (graph, source) = graph_and_source(); + let source_record = sample_record(graph.clone()); + let mut fork_record = source_record.clone(); + fork_record.run_id = fixtures::RUN_7; + fork_record.fork_source_ref = Some(fabro_types::ForkSourceRef { + source_run_id: source_record.run_id, + checkpoint_sha: "checkpoint-sha".to_string(), + }); + + let run_store = seeded_store_with(&fork_record, Some(&source), Some(&source_record)).await; + let loaded = load_from_store(&run_store.clone().into(), &run_dir) + .await + .unwrap(); + + assert_eq!(loaded.run_spec().run_id, fork_record.run_id); + assert_eq!( + loaded.run_spec().fork_source_ref, + fork_record.fork_source_ref + ); + } + #[test] fn persist_returns_error_on_io_failure() { let temp = tempfile::tempdir().unwrap(); diff --git a/lib/components/fabro-workflow/src/pipeline/publish.rs b/lib/components/fabro-workflow/src/pipeline/publish.rs index 30fe5b055..42010e133 100644 --- a/lib/components/fabro-workflow/src/pipeline/publish.rs +++ b/lib/components/fabro-workflow/src/pipeline/publish.rs @@ -1,8 +1,11 @@ +use std::fmt::Write as _; use std::sync::Arc; +use fabro_types::ExecOutputTail; + use super::pull_request::{AutoMergeOptions, OpenPullRequestRequest, open_pull_request}; use super::types::{Concluded, PublishOptions, PublishOutcome, Published}; -use crate::error::Error; +use crate::error::{Error, FailureCategory, classify_failure_reason}; use crate::event::Event; use crate::lifecycle::git::push_run_branch; @@ -35,6 +38,74 @@ pub async fn publish(concluded: Concluded, options: &PublishOptions) -> Publishe } } +/// Build the terminal publish error from a failed push operation. +/// +/// Retries exhausted on transient classifications stay `TransientInfra`: a +/// mature-token 404 is not proof of permanent access loss — a service-side +/// failure presents the same surface — so `Deterministic` would need +/// independent evidence this path does not gather. Each attempt becomes one +/// bounded cause line in the failure detail; git output stays inside the +/// exec output tail. +fn publish_push_error( + run_branch: &str, + push_error: fabro_sandbox::Error, + exec_output_tail: Option, + attempts: &[fabro_sandbox::PushAttempt], + last_successful_push_at: Option>, +) -> Error { + let message = match last_successful_push_at { + Some(at) => format!( + "failed to push run branch '{run_branch}' (last successful push at {})", + at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true) + ), + None => format!("failed to push run branch '{run_branch}'"), + }; + let failure_class = match attempts.last().and_then(|attempt| attempt.retry_reason) { + Some(_) => FailureCategory::TransientInfra, + None => classify_failure_reason(&format!( + "{message}: {}", + fabro_sandbox::display_for_log(&push_error) + )), + }; + let causes = attempts.iter().map(push_attempt_cause).collect(); + Error::publish_with_source_and_class( + message, + push_error, + failure_class, + exec_output_tail, + causes, + ) +} + +/// One bounded line per push attempt for the failure detail. +fn push_attempt_cause(attempt: &fabro_sandbox::PushAttempt) -> String { + let outcome = if attempt.success { + "succeeded".to_string() + } else { + attempt + .retry_reason + .map_or_else(|| "unclassified".to_string(), |reason| reason.to_string()) + }; + let mut line = format!( + "push attempt {} at {}: {outcome}", + attempt.attempt, + attempt + .started_at + .to_rfc3339_opts(chrono::SecondsFormat::Millis, true) + ); + if let Some(age_ms) = attempt + .token + .and_then(|token| token.age_at(attempt.started_at)) + .map(|age| u64::try_from(age.as_millis()).unwrap_or(u64::MAX)) + { + let _ = write!(line, " (token age {age_ms}ms)"); + } + if let Some(refresh_error) = attempt.refresh_error { + let _ = write!(line, ", refresh error: {refresh_error}"); + } + line +} + impl Concluded { /// Run the publish steps, recording each one into `outcome` as it lands. /// @@ -159,26 +230,36 @@ impl Concluded { } async fn push_final_commit(&self, run_branch: &str) -> Result<(), Error> { - match push_run_branch(self.services.sandbox.as_ref(), run_branch).await { - Ok(()) => { + // The terminal push guards the whole run's value, so it gets a real + // retry budget; attempts are nearly free at this point. + let plan = fabro_sandbox::RetryPlan::publish_push(); + match push_run_branch(self.services.sandbox.as_ref(), run_branch, &plan).await { + Ok(report) => { + self.services.sandbox_git.record_successful_push(); self.services.emitter.emit(&Event::GitPush { branch: run_branch.to_string(), success: true, exec_output_tail: None, + attempts: report.attempts, }); Ok(()) } - Err(error) => { + Err(push_error) => { + let fabro_sandbox::PushError { report, error } = push_error; let exec_output_tail = fabro_sandbox::default_redacted_output_tail(&error); + let attempts = report.attempts; self.services.emitter.emit(&Event::GitPush { branch: run_branch.to_string(), success: false, exec_output_tail: exec_output_tail.clone(), + attempts: attempts.clone(), }); - Err(Error::publish_with_source_and_exec_output_tail( - format!("failed to push run branch '{run_branch}'"), + Err(publish_push_error( + run_branch, error, exec_output_tail, + &attempts, + self.services.sandbox_git.last_successful_push_at(), )) } } @@ -192,3 +273,145 @@ impl Concluded { Error::publish(message) } } + +#[cfg(test)] +mod tests { + use chrono::Utc; + + use super::*; + use crate::error::FailureCategory; + + fn push_attempt( + attempt: u32, + retry_reason: Option, + token_age_ms: Option, + refresh_error: Option, + ) -> fabro_sandbox::PushAttempt { + let started_at = Utc::now(); + fabro_sandbox::PushAttempt { + attempt, + started_at, + success: false, + retry_reason, + exec_output_tail: None, + token: token_age_ms.map(|age_ms| fabro_sandbox::TokenSnapshot { + generation: 14, + provenance: fabro_sandbox::TokenProvenance::Minted { + minted_at: started_at + - chrono::Duration::milliseconds(i64::try_from(age_ms).unwrap()), + expires_at: started_at + chrono::Duration::hours(1), + }, + }), + credential_action: Some(fabro_sandbox::RemoteCredentialAction::Unchanged), + refresh_error, + } + } + + fn push_attempts_with_reasons( + reasons: &[Option], + ) -> Vec { + reasons + .iter() + .enumerate() + .map(|(index, reason)| fabro_sandbox::PushAttempt { + attempt: u32::try_from(index).unwrap() + 1, + started_at: Utc::now(), + success: false, + retry_reason: *reason, + exec_output_tail: None, + token: None, + credential_action: None, + refresh_error: None, + }) + .collect() + } + + fn push_source_error() -> fabro_sandbox::Error { + fabro_sandbox::Error::message("remote: Repository not found.") + } + + /// Exhausted retries on a retryable classification are transient + /// infrastructure, not deterministic: the same push succeeded manually an + /// hour after run 01M0DH033P2XSTHAGVBHG6922F failed, with no + /// configuration change. + #[test] + fn exhausted_transient_retries_classify_as_transient_infra() { + let attempts = push_attempts_with_reasons(&[ + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(fabro_sandbox::GitRetryReason::TokenReplication), + ]); + let error = + publish_push_error("fabro/run/test", push_source_error(), None, &attempts, None); + assert_eq!(error.failure_category(), FailureCategory::TransientInfra); + } + + #[test] + fn permanently_classified_push_falls_back_to_message_sniffing() { + let attempts = push_attempts_with_reasons(&[None]); + let error = + publish_push_error("fabro/run/test", push_source_error(), None, &attempts, None); + // "Repository not found." carries no transient hint for the + // heuristic, so the fallback stays deterministic. + assert_eq!(error.failure_category(), FailureCategory::Deterministic); + } + + #[test] + fn failure_detail_renders_one_cause_line_per_attempt() { + let attempts = vec![ + push_attempt( + 1, + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(180), + None, + ), + push_attempt( + 2, + Some(fabro_sandbox::GitRetryReason::TokenReplication), + Some(3320), + Some(fabro_sandbox::RefreshErrorKind::SetUrl), + ), + ]; + let last_push = Utc::now() - chrono::Duration::seconds(67); + let error = publish_push_error( + "fabro/run/test", + push_source_error(), + None, + &attempts, + Some(last_push), + ); + + let detail = error.to_failure_detail(); + assert!( + detail.message.contains("last successful push at"), + "{}", + detail.message + ); + let attempt_lines: Vec<&String> = detail + .causes + .iter() + .filter(|cause| cause.starts_with("push attempt")) + .collect(); + assert_eq!(attempt_lines.len(), 2); + assert!( + attempt_lines[0].contains("token_replication"), + "{attempt_lines:?}" + ); + assert!( + attempt_lines[0].contains("(token age 180ms)"), + "{attempt_lines:?}" + ); + assert!( + attempt_lines[1].contains("refresh error: set_url"), + "{attempt_lines:?}" + ); + assert_eq!( + detail + .causes + .iter() + .filter(|cause| cause.as_str() == "remote: Repository not found.") + .count(), + 1, + "the source chain must not repeat the inner push error" + ); + } +} diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index 1ab76a24b..f3f7e4a78 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -830,6 +830,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }, @@ -1112,6 +1113,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1126,6 +1128,7 @@ mod tests { automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git: run_spec.git.clone(), fork_source_ref: None, retried_from: None, @@ -1179,6 +1182,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1193,6 +1197,7 @@ mod tests { automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git: run_spec.git.clone(), fork_source_ref: None, retried_from: None, @@ -1596,6 +1601,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1610,6 +1616,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -1813,6 +1820,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }; append_event(&run_store, &fixtures::RUN_1, &Event::RunCreated { @@ -1827,6 +1835,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/components/fabro-workflow/src/run_lookup.rs b/lib/components/fabro-workflow/src/run_lookup.rs index 99e9825fe..8a66dbf91 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() } } @@ -512,6 +501,7 @@ mod tests { automation: None, provenance: run_spec.provenance.clone(), manifest_blob: None, + spec_blob: None, git: run_spec.git.clone(), fork_source_ref: run_spec.fork_source_ref.clone(), retried_from: None, diff --git a/lib/components/fabro-workflow/src/run_metadata.rs b/lib/components/fabro-workflow/src/run_metadata.rs index 74be989df..942b662f1 100644 --- a/lib/components/fabro-workflow/src/run_metadata.rs +++ b/lib/components/fabro-workflow/src/run_metadata.rs @@ -1,32 +1,33 @@ -use std::collections::HashMap; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, LazyLock, Mutex}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; -use anyhow::Context as _; use async_trait::async_trait; use fabro_checkpoint::git::{FileMode, Store, TreeEntries}; use fabro_dump::RunDump; +use fabro_github::token_source::{InstallationTokenSource, ResolvedToken, TokenSnapshot}; use git2::{ Cred, Direction, ErrorClass, ErrorCode, FetchOptions, Oid, PushOptions, RemoteCallbacks, Repository, Signature, }; use tokio::task::{self, JoinError}; +use tokio::time; use crate::git::{GitAuthor, META_BRANCH_PREFIX}; use crate::run_options::RunOptions; -static METADATA_PERMISSIONS: LazyLock> = LazyLock::new(|| { - [("contents", "write")] - .into_iter() - .map(|(key, value)| (key.to_string(), value.to_string())) - .collect() -}); - pub(crate) fn metadata_branch_name(run_id: &str) -> String { format!("{META_BRANCH_PREFIX}{run_id}") } +pub(crate) fn metadata_push_failure_is_transient( + detail: &str, + token: Option<&TokenSnapshot>, +) -> bool { + let credentials = fabro_sandbox::CredentialContext::from_snapshot(token); + fabro_sandbox::classify_failure(detail, credentials).is_some() +} + #[derive(Debug, thiserror::Error)] pub(crate) enum RunMetadataError { #[error("metadata writer initialization failed: {0}")] @@ -55,28 +56,86 @@ pub(crate) struct MetadataSnapshot { pub push_error: Option, pub entry_count: usize, pub bytes: u64, + /// The token the push authenticated with, for classifying a push + /// failure against the credential context. + pub token: Option, +} + +const METADATA_REPROBE_COOLDOWN: Duration = Duration::from_secs(30); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MetadataRuntimeState { + Healthy, + TransientDegraded { next_probe_at: time::Instant }, + PermanentlyDegraded, } pub(crate) struct RunMetadataRuntime { - degraded: AtomicBool, - warning_emitted: AtomicBool, + state: Mutex, } impl RunMetadataRuntime { pub(crate) fn new() -> Self { Self { - degraded: AtomicBool::new(false), - warning_emitted: AtomicBool::new(false), + state: Mutex::new(MetadataRuntimeState::Healthy), } } - pub(crate) fn mark_metadata_degraded(&self) -> bool { - self.degraded.store(true, Ordering::SeqCst); - !self.warning_emitted.swap(true, Ordering::SeqCst) + /// Record a metadata failure. `transient` failures (push failures with + /// retryable classifications) leave the writer eligible for re-probing; + /// initialization, discovery, serialization, and permanent-authentication + /// failures latch it off — repeating known-failing work at every + /// checkpoint is noise, not resilience. A permanent latch is never + /// upgraded by a later transient failure. Returns whether the caller + /// should emit the degradation warning (once per degradation). + pub(crate) fn mark_metadata_degraded(&self, transient: bool) -> bool { + let mut state = self.state.lock().expect("metadata runtime mutex poisoned"); + let should_warn = matches!(*state, MetadataRuntimeState::Healthy); + *state = match (*state, transient) { + (MetadataRuntimeState::PermanentlyDegraded, _) | (_, false) => { + MetadataRuntimeState::PermanentlyDegraded + } + (_, true) => MetadataRuntimeState::TransientDegraded { + next_probe_at: time::Instant::now() + METADATA_REPROBE_COOLDOWN, + }, + }; + should_warn + } + + /// A successful snapshot clears the degraded state and re-arms the + /// warning, so a later independent failure warns again instead of + /// failing silently. + pub(crate) fn clear_metadata_degraded(&self) { + let mut state = self.state.lock().expect("metadata runtime mutex poisoned"); + if !matches!(*state, MetadataRuntimeState::Healthy) { + *state = MetadataRuntimeState::Healthy; + } + } + + /// Whether all later snapshot writes must be skipped. + pub(crate) fn metadata_suspended(&self) -> bool { + matches!( + *self.state.lock().expect("metadata runtime mutex poisoned"), + MetadataRuntimeState::PermanentlyDegraded + ) + } + + /// Whether a checkpoint should defer its transient-failure re-probe. + pub(crate) fn metadata_checkpoint_suspended(&self) -> bool { + match *self.state.lock().expect("metadata runtime mutex poisoned") { + MetadataRuntimeState::Healthy => false, + MetadataRuntimeState::TransientDegraded { next_probe_at } => { + time::Instant::now() < next_probe_at + } + MetadataRuntimeState::PermanentlyDegraded => true, + } } pub(crate) fn metadata_degraded(&self) -> bool { - self.degraded.load(Ordering::SeqCst) + !matches!( + *self.state.lock().expect("metadata runtime mutex poisoned"), + MetadataRuntimeState::Healthy + ) } } @@ -88,24 +147,24 @@ impl Default for RunMetadataRuntime { #[async_trait] pub(crate) trait AuthProvider: Send + Sync { - async fn token(&self) -> Result, RunMetadataError>; + async fn token(&self) -> Result, RunMetadataError>; } struct GitHubAuthProvider { - creds: fabro_github::GitHubCredentials, - origin_url: String, + source: Arc, } impl GitHubAuthProvider { - fn new(creds: fabro_github::GitHubCredentials, origin_url: String) -> Self { - Self { creds, origin_url } + fn new(source: Arc) -> Self { + Self { source } } } #[async_trait] impl AuthProvider for GitHubAuthProvider { - async fn token(&self) -> Result, RunMetadataError> { - mint_token(&self.creds, &self.origin_url, &METADATA_PERMISSIONS) + async fn token(&self) -> Result, RunMetadataError> { + self.source + .resolve() .await .map(Some) .map_err(RunMetadataError::TokenMint) @@ -118,7 +177,7 @@ struct NoAuth; #[cfg(test)] #[async_trait] impl AuthProvider for NoAuth { - async fn token(&self) -> Result, RunMetadataError> { + async fn token(&self) -> Result, RunMetadataError> { Ok(None) } } @@ -184,12 +243,14 @@ impl RunMetadataWriterHandle { .unwrap() } + #[tracing::instrument(name = "git_op", skip_all, fields(op = "metadata-push"))] pub(crate) async fn write_snapshot( &self, dump: &RunDump, message: &str, ) -> Result { let token = self.auth.token().await?; + let token_snapshot = token.as_ref().map(|token| token.snapshot); let entries = dump .git_entries() .map_err(RunMetadataError::DumpSerialize)?; @@ -198,15 +259,24 @@ impl RunMetadataWriterHandle { task::spawn_blocking(move || { let mut guard = writer.lock().expect("metadata writer mutex poisoned"); - guard.write_snapshot_blocking(&entries, &message, token.as_deref()) + guard.write_snapshot_blocking( + &entries, + &message, + token.as_ref().map(|token| token.token.expose()), + ) }) .await .map_err(RunMetadataError::Join)? + .map(|mut snapshot| { + snapshot.token = token_snapshot; + snapshot + }) } } pub(crate) fn build_metadata_writer( run_options: &RunOptions, + token_source: Option>, ) -> Result, RunMetadataError> { if !run_options.settings.run.meta_branch.enabled { return Ok(None); @@ -229,14 +299,21 @@ pub(crate) fn build_metadata_writer( if !normalized_url.starts_with("https://") { return Ok(None); } - if fabro_github::parse_github_owner_repo(&normalized_url).is_err() { + let Ok((owner, repo)) = fabro_github::parse_github_owner_repo(&normalized_url) else { return Ok(None); - } + }; - let auth = Arc::new(GitHubAuthProvider::new( - creds.clone(), - normalized_url.clone(), - )); + let source = match token_source { + Some(source) => source, + None => InstallationTokenSource::for_repository( + creds, + owner, + repo, + serde_json::json!({ "contents": "write" }), + ) + .map_err(RunMetadataError::TokenMint)?, + }; + let auth = Arc::new(GitHubAuthProvider::new(source)); let writer = RunMetadataWriter::new( normalized_url, meta_branch.clone(), @@ -247,28 +324,6 @@ pub(crate) fn build_metadata_writer( Ok(Some(RunMetadataWriterHandle::new(writer, auth))) } -pub(crate) async fn mint_token( - creds: &fabro_github::GitHubCredentials, - origin_url: &str, - permissions: &HashMap, -) -> anyhow::Result { - let normalized_url = fabro_github::normalize_repo_origin_url(origin_url); - let (owner, repo) = - fabro_github::parse_github_owner_repo(&normalized_url).context("parsing GitHub origin")?; - let client = fabro_http::http_client().map_err(anyhow::Error::new)?; - let permissions = - serde_json::to_value(permissions).context("serializing GitHub permissions")?; - creds - .resolve_bearer_token( - &client, - &owner, - &repo, - &fabro_github::github_api_base_url(), - permissions, - ) - .await -} - pub(crate) struct RunMetadataWriter { store: Store, tempdir: tempfile::TempDir, @@ -358,6 +413,7 @@ impl RunMetadataWriter { push_error, entry_count, bytes, + token: None, }) } @@ -641,6 +697,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, fork_source_ref: None, }, chrono::Utc::now(), @@ -1010,16 +1067,19 @@ mod tests { for (origin, expected) in cases { let options = run_options_for_origin(origin); - let handle = build_metadata_writer(&options).unwrap().unwrap(); + let handle = build_metadata_writer(&options, None).unwrap().unwrap(); assert_eq!(handle.remote_url_for_test(), expected); assert!(!handle.remote_url_for_test().contains("ghs_aaaaaa")); assert!(!handle.remote_url_for_test().contains('@')); } assert!( - build_metadata_writer(&run_options_for_origin("https://gitlab.com/owner/repo.git")) - .unwrap() - .is_none() + build_metadata_writer( + &run_options_for_origin("https://gitlab.com/owner/repo.git"), + None + ) + .unwrap() + .is_none() ); } @@ -1028,6 +1088,57 @@ mod tests { let mut options = run_options_for_origin("https://github.com/owner/repo.git"); options.settings.run.meta_branch.enabled = false; - assert!(build_metadata_writer(&options).unwrap().is_none()); + assert!(build_metadata_writer(&options, None).unwrap().is_none()); + } + #[tokio::test(start_paused = true)] + async fn transient_degradation_reprobes_after_a_cooldown() { + let runtime = RunMetadataRuntime::new(); + assert!(runtime.mark_metadata_degraded(true), "first failure warns"); + assert!(runtime.metadata_degraded()); + assert!( + !runtime.metadata_suspended(), + "a final snapshot can still re-probe" + ); + assert!(runtime.metadata_checkpoint_suspended()); + time::advance(METADATA_REPROBE_COOLDOWN).await; + assert!(!runtime.metadata_checkpoint_suspended()); + assert!( + !runtime.mark_metadata_degraded(true), + "repeat failures do not warn again" + ); + } + + #[test] + fn permanent_degradation_latches_snapshots_off() { + let runtime = RunMetadataRuntime::new(); + runtime.mark_metadata_degraded(false); + assert!(runtime.metadata_suspended()); + + // A later transient failure never upgrades a permanent latch. + runtime.mark_metadata_degraded(true); + assert!(runtime.metadata_suspended()); + } + + #[test] + fn permanent_failure_latches_a_transiently_degraded_writer() { + let runtime = RunMetadataRuntime::new(); + runtime.mark_metadata_degraded(true); + assert!(!runtime.metadata_suspended()); + runtime.mark_metadata_degraded(false); + assert!(runtime.metadata_suspended()); + } + + #[test] + fn successful_snapshot_clears_degradation_and_rearms_the_warning() { + let runtime = RunMetadataRuntime::new(); + assert!(runtime.mark_metadata_degraded(true)); + runtime.clear_metadata_degraded(); + + assert!(!runtime.metadata_degraded()); + assert!(!runtime.metadata_suspended()); + assert!( + runtime.mark_metadata_degraded(false), + "a later independent failure warns again instead of failing silently" + ); } } diff --git a/lib/components/fabro-workflow/src/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/components/fabro-workflow/src/runtime_store.rs b/lib/components/fabro-workflow/src/runtime_store.rs index 45252d5d3..2e506400c 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) } @@ -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() } } @@ -169,6 +157,7 @@ mod tests { automation: None, provenance: test_support::test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, @@ -217,8 +206,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/src/sandbox_git.rs b/lib/components/fabro-workflow/src/sandbox_git.rs index 914153955..c6084a977 100644 --- a/lib/components/fabro-workflow/src/sandbox_git.rs +++ b/lib/components/fabro-workflow/src/sandbox_git.rs @@ -158,6 +158,7 @@ pub async fn git_checkpoint( clippy::too_many_arguments, reason = "Checkpointing needs explicit run metadata, checkpoint settings, and author inputs." )] +#[tracing::instrument(name = "git_op", skip_all, fields(op = "checkpoint-commit"))] pub(crate) async fn checked_git_checkpoint( runtime: &SandboxGitRuntime, sandbox: &dyn Sandbox, diff --git a/lib/components/fabro-workflow/src/sandbox_git_runtime.rs b/lib/components/fabro-workflow/src/sandbox_git_runtime.rs index 2bd246e13..1178d5c69 100644 --- a/lib/components/fabro-workflow/src/sandbox_git_runtime.rs +++ b/lib/components/fabro-workflow/src/sandbox_git_runtime.rs @@ -6,16 +6,35 @@ use tokio::sync::OnceCell; use crate::sandbox_git::{GIT_REMOTE, exec_err}; pub(crate) struct SandboxGitRuntime { - probe: OnceCell>, + probe: OnceCell>, + /// When the run last pushed its branch successfully (checkpoint or + /// publish). Read by the publish failure report so "last success 67s + /// before the failure" is visible from the run conclusion. + last_successful_push_at: std::sync::Mutex>>, } impl SandboxGitRuntime { pub(crate) fn new() -> Self { Self { - probe: OnceCell::new(), + probe: OnceCell::new(), + last_successful_push_at: std::sync::Mutex::new(None), } } + pub(crate) fn record_successful_push(&self) { + *self + .last_successful_push_at + .lock() + .expect("last push timestamp mutex poisoned") = Some(chrono::Utc::now()); + } + + pub(crate) fn last_successful_push_at(&self) -> Option> { + *self + .last_successful_push_at + .lock() + .expect("last push timestamp mutex poisoned") + } + pub(crate) async fn ensure_git_available( &self, sandbox: &dyn Sandbox, diff --git a/lib/components/fabro-workflow/src/services.rs b/lib/components/fabro-workflow/src/services.rs index 2af76a9c7..64facc23e 100644 --- a/lib/components/fabro-workflow/src/services.rs +++ b/lib/components/fabro-workflow/src/services.rs @@ -8,6 +8,7 @@ use fabro_agent::{Sandbox, ToolEnvProvider}; use fabro_auth::CredentialSource; #[cfg(test)] use fabro_auth::ResolvedCredentials; +use fabro_github::token_source::InstallationTokenSource; use fabro_hooks::{HookContext, HookDecision, HookExecutionContext, HookRunner}; use fabro_interview::Interviewer; use fabro_model::{Catalog, ProviderId}; @@ -15,7 +16,6 @@ use fabro_types::{ManifestPath, RunId}; use tokio_util::sync::CancellationToken; use crate::event::Emitter; -use crate::github_token_source::GitHubTokenSource; use crate::handler::HandlerRegistry; use crate::interview_runtime::RunInterviewBlocker; use crate::run_metadata::{RunMetadataRuntime, RunMetadataWriterHandle}; @@ -238,7 +238,7 @@ pub struct EngineServices { /// Environment variables from `[sandbox.env]` config. pub base_env: HashMap, /// GitHub token source used to inject `GITHUB_TOKEN` at the point of use. - pub github_token: Option>, + pub github_token: Option>, /// Typed values from `[run.inputs]`, available to prompt templates. pub inputs: HashMap, /// When true, handlers should skip real execution and return simulated @@ -342,7 +342,7 @@ impl EngineServices { pub struct WorkflowToolEnvProvider { pub base_env: HashMap, - pub github_token: Option>, + pub github_token: Option>, } #[async_trait::async_trait] @@ -354,11 +354,15 @@ impl ToolEnvProvider for WorkflowToolEnvProvider { async fn resolve_workflow_env( base_env: &HashMap, - github_token: Option<&Arc>, + github_token: Option<&Arc>, ) -> anyhow::Result> { let mut env = base_env.clone(); if let Some(source) = github_token { - env.insert("GITHUB_TOKEN".to_string(), source.current_token().await?); + let resolved = source.resolve().await?; + env.insert( + "GITHUB_TOKEN".to_string(), + resolved.token.expose().to_owned(), + ); } Ok(env) } @@ -371,9 +375,10 @@ mod tests { use anyhow::anyhow; use fabro_agent::ToolEnvProvider as _; use fabro_github::InstallationToken; + use fabro_github::test_support::{InstallationTokenMinter, installation_token_source}; + use fabro_github::token_source::InstallationTokenSource; use super::{EngineServices, WorkflowToolEnvProvider}; - use crate::github_token_source::{GitHubTokenSource, IatMinter}; #[tokio::test] async fn test_default_uses_stub_credential_source() { @@ -406,7 +411,7 @@ mod tests { async fn workflow_tool_env_provider_merges_current_github_token() { let provider = WorkflowToolEnvProvider { base_env: HashMap::from([("FOO".to_string(), "bar".to_string())]), - github_token: Some(Arc::new(GitHubTokenSource::pat("ghp_pat".to_string()))), + github_token: Some(InstallationTokenSource::pat("ghp_pat".to_string())), }; let env = provider.resolve().await.unwrap(); @@ -418,7 +423,7 @@ mod tests { struct FailingMinter; #[async_trait::async_trait] - impl IatMinter for FailingMinter { + impl InstallationTokenMinter for FailingMinter { async fn mint(&self) -> anyhow::Result { Err(anyhow!("GITHUB_TOKEN refresh failed")) } @@ -428,9 +433,10 @@ mod tests { async fn workflow_tool_env_provider_propagates_token_refresh_errors() { let provider = WorkflowToolEnvProvider { base_env: HashMap::new(), - github_token: Some(Arc::new(GitHubTokenSource::mintable(Arc::new( - FailingMinter, - )))), + github_token: Some(installation_token_source( + "owner/repo", + Arc::new(FailingMinter), + )), }; let err = format!("{:#}", provider.resolve().await.unwrap_err()); diff --git a/lib/components/fabro-workflow/src/stage_execution.rs b/lib/components/fabro-workflow/src/stage_execution.rs index c05a3547e..ec755127f 100644 --- a/lib/components/fabro-workflow/src/stage_execution.rs +++ b/lib/components/fabro-workflow/src/stage_execution.rs @@ -209,6 +209,7 @@ mod tests { provenance: test_support::test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: None, fork_source_ref: None, }; diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 19390762d..c7407b7b9 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -204,6 +204,7 @@ async fn initialized( }, }, manifest_blob: None, + spec_blob: None, git: run_options.pre_run_git.clone(), fork_source_ref: run_options.fork_source_ref.clone(), retried_from: None, diff --git a/lib/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/components/fabro-workflow/tests/it/daytona_integration.rs b/lib/components/fabro-workflow/tests/it/daytona_integration.rs index 2a1b6b5ae..fda3aabf7 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 @@ -427,7 +447,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(); @@ -552,15 +574,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_id = 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), + 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 0dafedd3f..8104f7802 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_id) = 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_id))? - .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_id = 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), + 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_id = 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), + 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!( 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..e67818068 --- /dev/null +++ b/lib/foundation/fabro-api/tests/blob_hash_round_trip.rs @@ -0,0 +1,61 @@ +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_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] +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::() + ); +} + +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/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-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-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index 4ece0a9de..9547dddce 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -1839,20 +1839,20 @@ impl Client { .await }) .await?; - response - .into_inner() - .id - .parse() - .context("write_run_blob returned invalid blob id") + Ok(response.into_inner().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) .send() .await; match response { 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-core/src/executor.rs b/lib/foundation/fabro-core/src/executor.rs index 3a94eb332..6701e30be 100644 --- a/lib/foundation/fabro-core/src/executor.rs +++ b/lib/foundation/fabro-core/src/executor.rs @@ -179,8 +179,11 @@ impl Executor { } } - // Check visit limits (>= matches fabro-workflow semantics) - let visits = state.increment_visits(node.id()); + // Check visit limits before entry: a node with a limit of N may + // execute N times, matching the documented contract. The count + // covers previously admitted entries, so the refused visit is + // not reported as one. + let visits = state.visits(node.id()); if let Some(max) = node.max_visits() { if visits >= max { return Err(Error::VisitLimitExceeded { @@ -201,6 +204,7 @@ impl Executor { }); } } + state.increment_visits(node.id()); // before_node lifecycle let node_result = match self.lifecycle.before_node(&node, &state).await? { @@ -807,7 +811,8 @@ mod tests { #[tokio::test] async fn executor_visit_limit_per_node() { - // Node with max_visits=2, loops back — fails on 2nd visit (>= semantics) + // Node with max_visits=2, loops back — executes exactly twice, then + // the third entry is refused. The error reports completed visits. let g = TestGraph::new( vec![ TestNode::new("loop_node").with_max_visits(2), @@ -821,11 +826,20 @@ mod tests { "loop_node", ); let state = ExecutionState::new(&g).unwrap(); + let handler = Arc::new(CountingHandler::new(vec![])); let executor = - ExecutorBuilder::new(Arc::new(AlwaysSucceedHandler) as Arc>) - .build(); + ExecutorBuilder::new(Arc::clone(&handler) as Arc>).build(); let result = executor.run(&g, state).await; - assert!(matches!(result, Err(Error::VisitLimitExceeded { .. }))); + match result { + Err(Error::VisitLimitExceeded { visits, limit, .. }) => { + assert_eq!(visits, 2); + assert_eq!(limit, 2); + } + Err(other) => panic!("expected VisitLimitExceeded, got {other:?}"), + Ok(_) => panic!("expected VisitLimitExceeded, got success"), + } + // Two full loop_node -> other iterations ran before the refusal. + assert_eq!(handler.calls(), 4); } #[tokio::test] diff --git a/lib/foundation/fabro-core/src/state.rs b/lib/foundation/fabro-core/src/state.rs index ca3b8e56b..c44eeeb7d 100644 --- a/lib/foundation/fabro-core/src/state.rs +++ b/lib/foundation/fabro-core/src/state.rs @@ -79,6 +79,10 @@ impl ExecutionState { graph.get_node(&self.current_node_id) } + pub fn visits(&self, node_id: &str) -> usize { + self.node_visits.get(node_id).copied().unwrap_or(0) + } + pub fn increment_visits(&mut self, node_id: &str) -> usize { let count = self.node_visits.entry(node_id.to_string()).or_insert(0); *count += 1; 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()?; 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, diff --git a/lib/foundation/fabro-model/src/catalog/providers/venice.toml b/lib/foundation/fabro-model/src/catalog/providers/venice.toml index dc97c4ab4..9791591e0 100644 --- a/lib/foundation/fabro-model/src/catalog/providers/venice.toml +++ b/lib/foundation/fabro-model/src/catalog/providers/venice.toml @@ -1,46 +1,212 @@ +# Model IDs, capabilities, contexts, and prices are from Venice's published +# model catalog, verified 2026-08-21: +# https://github.com/veniceai/api-docs/blob/59a300b1d036c0c0acc0e5f75c0ab0dd07c40c1c/data/static-models.json + [providers.venice] display_name = "Venice" adapter = "openai_compatible" base_url = "https://api.venice.ai/api/v1" priority = 35 aliases = ["venice-ai"] +billing_policy = "openai" [providers.venice.auth] credentials = ["env:VENICE_API_KEY", "vault:VENICE_API_KEY"] -[providers.venice.models."venice-uncensored-1-2"] -display_name = "Venice Uncensored 1.2" -family = "venice-uncensored" +[providers.venice.models."kimi-k3"] +display_name = "Kimi K3" +family = "kimi-k3" +agent_profile = "kimi" +aliases = ["kimi"] + +[providers.venice.models."kimi-k3".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."kimi-k3".features] +tools = true +vision = true +reasoning = true +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."kimi-k3".costs] +input_cost_per_mtok = 3.75 +output_cost_per_mtok = 18.75 +cache_input_cost_per_mtok = 0.375 + +[providers.venice.models."kimi-k3-fast"] +api_id = "kimi-k3-fast-api" +display_name = "Kimi K3 Fast" +family = "kimi-k3" +agent_profile = "kimi" +aliases = ["kimi-fast"] + +[providers.venice.models."kimi-k3-fast".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."kimi-k3-fast".features] +tools = true +vision = true +reasoning = true +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."kimi-k3-fast".costs] +input_cost_per_mtok = 4.5 +output_cost_per_mtok = 22.5 +cache_input_cost_per_mtok = 0.45 + +[providers.venice.models."grok-4.6"] +api_id = "grok-4-6" +display_name = "Grok 4.6" +family = "grok-4" +aliases = ["grok", "grok46", "grok-46"] + +[providers.venice.models."grok-4.6".limits] +context_window = 500000 +max_output = 32000 + +[providers.venice.models."grok-4.6".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true +prompt_cache = true + +[providers.venice.models."grok-4.6".controls] +reasoning_effort = ["low", "medium", "high", "xhigh"] + +[providers.venice.models."grok-4.6".costs] +input_cost_per_mtok = 2.27 +output_cost_per_mtok = 6.8 +cache_input_cost_per_mtok = 0.57 + +[providers.venice.models."glm-5.3"] +api_id = "z-ai-glm-5-3" +display_name = "GLM 5.3" +family = "glm-5" +aliases = ["glm", "glm5", "glm53", "glm5.3", "glm-5-3"] + +[providers.venice.models."glm-5.3".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."glm-5.3".features] +tools = true +vision = false +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true +prompt_cache = true + +[providers.venice.models."glm-5.3".controls] +reasoning_effort = ["low", "high", "max"] + +[providers.venice.models."glm-5.3".costs] +input_cost_per_mtok = 1.75 +output_cost_per_mtok = 5.5 +cache_input_cost_per_mtok = 0.325 + +[providers.venice.models."deepseek-v4-flash"] +api_id = "deepseek-v4-flash-0731" +display_name = "DeepSeek V4 Flash" +family = "deepseek-v4" +agent_profile = "openai" default = true -aliases = ["venice-uncensored", "vu"] +aliases = ["deepseek-v4", "deepseek", "deepseek-flash"] -[providers.venice.models."venice-uncensored-1-2".limits] -context_window = 128000 -max_output = 8192 +[providers.venice.models."deepseek-v4-flash".limits] +context_window = 1000000 +max_output = 32768 -[providers.venice.models."venice-uncensored-1-2".features] +[providers.venice.models."deepseek-v4-flash".features] +tools = true +vision = false +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."deepseek-v4-flash".controls] +reasoning_effort = ["low", "high", "max"] + +[providers.venice.models."deepseek-v4-flash".costs] +input_cost_per_mtok = 0.175 +output_cost_per_mtok = 0.35 +cache_input_cost_per_mtok = 0.035 + +[providers.venice.models."deepseek-v4-pro"] +api_id = "deepseek-v4-pro-0813" +display_name = "DeepSeek V4 Pro" +family = "deepseek-v4" +agent_profile = "openai" +aliases = ["deepseek-pro"] + +[providers.venice.models."deepseek-v4-pro".limits] +context_window = 1000000 +max_output = 32768 + +[providers.venice.models."deepseek-v4-pro".features] +tools = true +vision = false +reasoning = true +reasoning_by_default = true +prompt_cache = true +sampling_params = false + +[providers.venice.models."deepseek-v4-pro".costs] +input_cost_per_mtok = 1.65 +output_cost_per_mtok = 4.95 +cache_input_cost_per_mtok = 0.165 + +[providers.venice.models."qwen3.8-max"] +api_id = "qwen-3-8-max" +display_name = "Qwen 3.8 Max" +family = "qwen3" +aliases = ["qwen", "qwen-max", "qwen3.8", "qwen-3.8", "qwen38", "qwen-3.8-max", "qwen38-max"] + +[providers.venice.models."qwen3.8-max".limits] +context_window = 1000000 +max_output = 131072 + +[providers.venice.models."qwen3.8-max".features] tools = true vision = true -reasoning = false +reasoning = true +reasoning_by_default = true +prompt_cache = true -[providers.venice.models."venice-uncensored-1-2".costs] -input_cost_per_mtok = 0.2 -output_cost_per_mtok = 0.9 +[providers.venice.models."qwen3.8-max".costs] +input_cost_per_mtok = 2.5 +output_cost_per_mtok = 7.5 +cache_input_cost_per_mtok = 0.3125 -[providers.venice.models."venice-uncensored-role-play"] -display_name = "Venice Uncensored Role Play" -family = "venice-uncensored" -aliases = ["venice-roleplay", "vrp"] +[providers.venice.models."qwen3.8-27b"] +api_id = "qwen-3-8-27b" +display_name = "Qwen 3.8 27B" +family = "qwen3.8" +aliases = ["qwen-27b", "qwen-3.8-27b", "qwen38-27b"] -[providers.venice.models."venice-uncensored-role-play".limits] -context_window = 128000 -max_output = 4096 +[providers.venice.models."qwen3.8-27b".limits] +context_window = 262144 +max_output = 131072 -[providers.venice.models."venice-uncensored-role-play".features] +[providers.venice.models."qwen3.8-27b".features] tools = true vision = true -reasoning = false +reasoning = true +reasoning_effort = "levels" +reasoning_by_default = true -[providers.venice.models."venice-uncensored-role-play".costs] -input_cost_per_mtok = 0.5 -output_cost_per_mtok = 2.0 +[providers.venice.models."qwen3.8-27b".controls] +reasoning_effort = ["low", "medium", "xhigh"] + +[providers.venice.models."qwen3.8-27b".costs] +input_cost_per_mtok = 0.45 +output_cost_per_mtok = 3.2 diff --git a/lib/foundation/fabro-redact/src/entropy.rs b/lib/foundation/fabro-redact/src/entropy.rs index dc744cf39..ec6029c3c 100644 --- a/lib/foundation/fabro-redact/src/entropy.rs +++ b/lib/foundation/fabro-redact/src/entropy.rs @@ -36,6 +36,12 @@ pub(super) fn shannon_entropy(s: &str) -> f64 { /// Returns regions where tokens match `[A-Za-z0-9+_=-]{10,}` and have /// Shannon entropy above the threshold (4.5 bits). Protects against /// consuming characters from JSON escape sequences. +/// +/// An assignment token (`NAME=value`) is measured and redacted by its +/// value alone. Measuring the pair merges the name's charset into the +/// value's and pushes innocuous values (a pure-hex git SHA can never +/// exceed 4.0 bits by itself) over the threshold, and redacting the +/// pair destroys the name that says what was redacted. pub(super) fn find_entropy_regions(s: &str) -> Vec { let mut regions = Vec::new(); for m in SECRET_PATTERN.find_iter(s) { @@ -58,6 +64,10 @@ pub(super) fn find_entropy_regions(s: &str) -> Vec { } } + if let Some(offset) = assignment_value_offset(&s[start..end]) { + start += offset; + } + if shannon_entropy(&s[start..end]) > ENTROPY_THRESHOLD { regions.push(Region { start, end }); } @@ -65,6 +75,28 @@ pub(super) fn find_entropy_regions(s: &str) -> Vec { regions } +/// For an assignment token (`NAME=value` with an identifier-shaped name), +/// return the byte offset where the value begins. Entropy above the 4.5-bit +/// threshold needs at least 23 distinct characters, so a value too short to +/// qualify simply measures under the threshold; no length guard is needed. +fn assignment_value_offset(token: &str) -> Option { + let eq = token.find('=')?; + let value = &token[eq + 1..]; + if value.is_empty() || value.starts_with('=') { + return None; + } + let name = &token[..eq]; + let mut chars = name.chars(); + let first = chars.next()?; + if !(first.is_ascii_alphabetic() || first == '_') { + return None; + } + if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') { + return None; + } + Some(eq + 1) +} + #[cfg(test)] mod tests { use super::*; @@ -98,14 +130,28 @@ mod tests { #[test] fn regions_finds_high_entropy_token() { - // `=` is in the regex pattern, so "key=xK9..." matches as one token + // "key=xK9..." matches as one token, but only the value is + // measured and flagged; the name survives redaction. let input = "key=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p"; let regions = find_entropy_regions(input); assert_eq!(regions.len(), 1); - assert_eq!(regions[0].start, 0); + assert_eq!(regions[0].start, "key=".len()); assert_eq!(regions[0].end, input.len()); } + #[test] + fn regions_find_padded_base64_tokens() { + for input in [ + "WxFhjC5EAnh30M0JIe0Wa58Xb1BYf8kedTTdKUbbd9Y=", + "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789ABCDEF==", + ] { + assert_eq!(find_entropy_regions(input), vec![Region { + start: 0, + end: input.len(), + }]); + } + } + #[test] fn regions_empty_for_json_escape_sequence() { // "controller.go\nmodel.go" — the regex could match across the \n boundary diff --git a/lib/foundation/fabro-redact/src/jsonl.rs b/lib/foundation/fabro-redact/src/jsonl.rs index e466f7002..f0f50f6fa 100644 --- a/lib/foundation/fabro-redact/src/jsonl.rs +++ b/lib/foundation/fabro-redact/src/jsonl.rs @@ -209,7 +209,7 @@ mod tests { let redacted = redact_json_value(input); assert_eq!(redacted["name"], "fabro-01KQR3V9D4VPFFWMNTVH09J48G"); - assert_eq!(redacted["content"], "REDACTED"); + assert_eq!(redacted["content"], "token=REDACTED"); } #[test] @@ -262,7 +262,7 @@ mod tests { let redacted = redact_json_value(input); - assert_eq!(redacted["content"], "REDACTED"); + assert_eq!(redacted["content"], "key=REDACTED"); assert_eq!(redacted["session_id"], HIGH_ENTROPY_SECRET); } diff --git a/lib/foundation/fabro-redact/src/lib.rs b/lib/foundation/fabro-redact/src/lib.rs index 8ed562e53..4a7efa45b 100644 --- a/lib/foundation/fabro-redact/src/lib.rs +++ b/lib/foundation/fabro-redact/src/lib.rs @@ -111,6 +111,27 @@ mod tests { assert_eq!(result, "key=REDACTED"); } + #[test] + fn redact_string_keeps_assignment_with_low_entropy_value() { + // A pinned git SHA is pure hex, so the value alone can never exceed + // 4.0 bits of entropy. Only the merged NAME=value token crosses the + // 4.5-bit threshold, because the uppercase name widens the charset. + // Measuring the name together with the value redacts innocuous + // pins; the pair must survive. + let input = "ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf"; + assert_eq!(redact_string(input), input); + } + + #[test] + fn redact_string_keeps_assignment_key_for_high_entropy_value() { + // The value alone is above the entropy threshold, so it is + // redacted either way — but the name says which setting was + // redacted and must survive, as the gitleaks layer already + // does for `key=REDACTED`. + let result = redact_string("BUILD_STAMP=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p"); + assert_eq!(result, "BUILD_STAMP=REDACTED"); + } + #[test] fn redact_string_overlapping_detections_produce_single_redacted() { // A high-entropy string that also matches a gitleaks pattern diff --git a/lib/foundation/fabro-template/src/dependency.rs b/lib/foundation/fabro-template/src/dependency.rs index fa9629108..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, @@ -80,38 +101,55 @@ 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() { - 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(), - } - })?; - 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(); 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-test/src/lib.rs b/lib/foundation/fabro-test/src/lib.rs index 74a5f6727..5d442b411 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_ID]""#.to_string(), - )); - filters.push(( - r#""definition_blob":\s*"[0-9a-f]{64}""#.to_string(), - r#""definition_blob": "[BLOB_ID]""#.to_string(), - )); + for field in ["manifest_blob", "definition_blob", "spec_blob"] { + filters.push(( + format!(r#""{field}":\s*"[0-9a-f]{{64}}""#), + format!(r#""{field}": "[BLOB_HASH]""#), + )); + } filters.push(( r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(), r#""run_dir": "[RUN_DIR]""#.to_string(), @@ -2562,8 +2560,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/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/foundation/fabro-types/src/graph.rs b/lib/foundation/fabro-types/src/graph.rs index b9f74b79f..ded69954d 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")] @@ -612,6 +611,31 @@ 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, run goal files) 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. @@ -620,19 +644,19 @@ 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 +1192,7 @@ mod tests { "output_schema", "@schemas/result.schema.json", ), - Some(ReferenceKind::FileInline), + Some(GraphReferenceKind::FileInline), ); } diff --git a/lib/foundation/fabro-types/src/run.rs b/lib/foundation/fabro-types/src/run.rs index 287f5b85f..86f1910d7 100644 --- a/lib/foundation/fabro-types/src/run.rs +++ b/lib/foundation/fabro-types/src/run.rs @@ -76,6 +76,11 @@ pub struct RunSpec { pub manifest_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub definition_blob: Option, + /// Unredacted copy of this spec in the blob store. Stored events pass + /// through secret redaction, so the spec folded from them is display + /// data; execution must load the spec from this blob. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub spec_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/foundation/fabro-types/src/run_event/misc.rs b/lib/foundation/fabro-types/src/run_event/misc.rs index cde08f65f..9e5600287 100644 --- a/lib/foundation/fabro-types/src/run_event/misc.rs +++ b/lib/foundation/fabro-types/src/run_event/misc.rs @@ -112,12 +112,104 @@ pub struct GitCommitProps { pub sha: String, } +/// Why a failed git push attempt is safe to retry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitPushRetryReason { + /// A recently minted token may not have reached every GitHub git endpoint. + TokenReplication, + /// The failure came from transient network or service infrastructure. + TransientInfra, +} + +/// Non-secret origin of the token used by a git push attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitTokenProvenance { + /// The token source minted the token for this resolve. + Minted, + /// The token source reused an earlier mint. + Reused, + /// The credential cannot be refreshed by Fabro. + Static, +} + +/// What credential preparation changed before a git push attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitCredentialAction { + /// Fabro wrote a token generation into the remote URL. + Embedded, + /// The remote already tracked the selected token generation. + Unchanged, + /// No managed credential was available. + None, +} + +/// Which credential preparation step failed before a git push attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum GitCredentialRefreshError { + /// Token resolution or minting failed. + Mint, + /// Rewriting the remote URL failed. + SetUrl, +} + +/// One attempt of a retried git push, nested inside [`GitPushProps`]. +/// +/// The durable projection of the sandbox layer's runtime attempt record. +/// Token identity is flattened into the three `token_*` fields — a nested +/// provenance enum never appears in stored events. `classified_reason` is the +/// retry classifier's verdict for a failed attempt (the terminal attempt +/// carries its classification too); whether an attempt was actually retried +/// is positional — every entry except the last. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GitPushAttemptProps { + /// 1-based attempt number within this push operation. + pub attempt: u32, + pub started_at: chrono::DateTime, + pub success: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub classified_reason: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exec_output_tail: Option, + /// Generation of the token embedded during this attempt (0 for static + /// credentials). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_generation: Option, + /// `minted`, `reused`, or `static`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_provenance: Option, + /// Token age at the attempt; absent for static credentials. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_age_ms: Option, + /// What the credential refresh did to the remote this attempt: + /// `embedded`, `unchanged`, or `none`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credential_action: Option, + /// A credential `mint` or `set_url` failure this attempt pushed through. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub refresh_error: Option, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GitPushProps { pub branch: String, + /// Final outcome of the whole push operation — one `git.push` event per + /// high-level push, so finality is unambiguous. pub success: bool, + /// The final attempt's output tail, unchanged for existing consumers. #[serde(default, skip_serializing_if = "Option::is_none")] pub exec_output_tail: Option, + /// Per-attempt history. Absent on events stored before attempts were + /// recorded. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub attempts: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/foundation/fabro-types/src/run_event/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index 99c9c4aa5..6323e5071 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -1954,6 +1954,7 @@ mod tests { branch: "refs/heads/run:refs/heads/run".to_string(), success: false, exec_output_tail: Some(tail.clone()), + attempts: Vec::new(), }), ] { let value = serde_json::to_value(&body).unwrap(); @@ -1985,6 +1986,7 @@ mod tests { branch: "refs/heads/run:refs/heads/run".to_string(), success: false, exec_output_tail: None, + attempts: Vec::new(), }), ] { let value = serde_json::to_value(&body).unwrap(); diff --git a/lib/foundation/fabro-types/src/run_event/run.rs b/lib/foundation/fabro-types/src/run_event/run.rs index 68a994dc9..c7a17d990 100644 --- a/lib/foundation/fabro-types/src/run_event/run.rs +++ b/lib/foundation/fabro-types/src/run_event/run.rs @@ -28,6 +28,11 @@ pub struct RunCreatedProps { pub provenance: RunProvenance, #[serde(default, skip_serializing_if = "Option::is_none")] pub manifest_blob: Option, + /// Unredacted copy of the run spec in the blob store. The settings and + /// graph on this event are redacted at the sink; execution loads the + /// spec from this blob instead. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub spec_blob: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub git: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index 3a6be70d4..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..41af963f1 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,36 @@ 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: +/// +/// ``` +/// # use fabro_types::{RunSpec, test_support}; +/// let spec = RunSpec { +/// workflow_slug: Some("release-flow".to_string()), +/// ..test_support::test_run_spec() +/// }; +/// # assert_eq!(spec.workflow_slug.as_deref(), Some("release-flow")); +/// ``` +#[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, + spec_blob: None, + git: None, + fork_source_ref: None, + } +} 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/foundation/fabro-types/tests/run_event_serde.rs b/lib/foundation/fabro-types/tests/run_event_serde.rs index 633abab04..f287a6acc 100644 --- a/lib/foundation/fabro-types/tests/run_event_serde.rs +++ b/lib/foundation/fabro-types/tests/run_event_serde.rs @@ -32,6 +32,7 @@ fn run_created_props_round_trip_templated_settings() { }), provenance: test_run_provenance(), manifest_blob: None, + spec_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), @@ -93,6 +94,7 @@ fn run_created_props_omits_web_url_when_absent() { automation: None, provenance: test_run_provenance(), manifest_blob: None, + spec_blob: None, git: None, fork_source_ref: None, retried_from: None, diff --git a/lib/foundation/fabro-types/tests/run_spec_methods.rs b/lib/foundation/fabro-types/tests/run_spec_methods.rs index f6f76fecf..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() } } diff --git a/lib/foundation/fabro-types/tests/run_spec_serde.rs b/lib/foundation/fabro-types/tests/run_spec_serde.rs index 97529bc93..ec7a7ef40 100644 --- a/lib/foundation/fabro-types/tests/run_spec_serde.rs +++ b/lib/foundation/fabro-types/tests/run_spec_serde.rs @@ -31,6 +31,7 @@ fn run_spec_round_trips_templated_settings() { provenance: test_run_provenance(), manifest_blob: None, definition_blob: None, + spec_blob: None, git: Some(GitContext { origin_url: "https://github.com/fabro-sh/fabro.git".to_string(), branch: "main".to_string(), 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 403e2eb05..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). @@ -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/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..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 @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -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/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..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 @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -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/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..e83be609b 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). @@ -44,6 +44,7 @@ export interface RunSpec { 'provenance': RunProvenance; 'manifest_blob'?: string | null; 'definition_blob'?: string | null; + 'spec_blob'?: string | null; 'git'?: GitContext | null; 'fork_source_ref'?: ForkSourceRef | null; } 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 295ff9874..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 @@ -4,7 +4,7 @@ * Fabro Run API * HTTP API for managing Fabro workflow run executions. * - * The version of the OpenAPI document: 0.1.0 + * The version of the OpenAPI document: 0.2.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -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 SHA-256 hash of a stored blob. Hex input is case-insensitive; Fabro emits the canonical lowercase form. */ - 'id': string; + 'hash': string; }