From 2460ffc37a717cc3d69eb680e12cfb07c7e788ec Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 27 Apr 2026 14:59:17 -0700 Subject: [PATCH] feat(install): add sandbox provider step to web install wizard Operators choose Docker (default, zero-config) or Daytona (validated via Daytona SDK) during browser install. Selection is captured in settings.toml under [run.sandbox] -- explicitly even for Docker, so the choice is locked in. Daytona keys land in the vault as DAYTONA_API_KEY (Environment secret). Step always runs after object_store and before the LLM step. Server adds POST /install/sandbox/test (validates Daytona key via client.list) and PUT /install/sandbox; both reuse the install-token auth and InstallSecret redaction patterns established by object-store. A resolve_install_sandbox_state helper preserves a saved Daytona key when the operator revisits the step without re-entering it. The in-memory api_key is dropped from PendingInstall after finish, matching the manual_credentials cleanup for S3 access keys. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 1 + apps/fabro-web/app/install-api.test.ts | 46 ++ apps/fabro-web/app/install-api.ts | 28 ++ apps/fabro-web/app/install-app.test.tsx | 294 ++++++++++++- apps/fabro-web/app/install-app.tsx | 175 +++++++- docs/public/api-reference/fabro-api.yaml | 100 +++++ lib/crates/fabro-install/src/lib.rs | 62 ++- lib/crates/fabro-sandbox/src/daytona/mod.rs | 10 + lib/crates/fabro-server/Cargo.toml | 1 + lib/crates/fabro-server/src/install.rs | 165 ++++++- .../fabro-server/tests/it/api/install.rs | 404 ++++++++++++++++++ .../src/.openapi-generator/FILES | 3 + .../fabro-api-client/src/api/install-api.ts | 142 ++++++ .../fabro-api-client/src/api/runs-api.ts | 107 ++--- .../fabro-api-client/src/models/diff-stats.ts | 5 +- .../src/models/fork-request.ts | 3 +- .../src/models/fork-response.ts | 3 +- .../fabro-api-client/src/models/index.ts | 3 + .../src/models/install-object-store-input.ts | 4 +- .../models/install-object-store-summary.ts | 4 +- .../src/models/install-prefill.ts | 3 +- .../src/models/install-sandbox-input.ts | 32 ++ .../src/models/install-sandbox-summary.ts | 32 ++ .../install-sandbox-validation-response.ts | 23 + .../src/models/install-session-response.ts | 4 + .../src/models/log-destination.ts | 5 +- .../src/models/manifest-args.ts | 3 +- .../src/models/rewind-request.ts | 3 +- .../src/models/rewind-response.ts | 3 +- .../src/models/run-summary.ts | 5 +- .../src/models/run-superseded-by-props.ts | 3 +- .../src/models/server-logging-settings.ts | 5 +- .../src/models/timeline-entry-response.ts | 3 +- 33 files changed, 1605 insertions(+), 79 deletions(-) create mode 100644 lib/packages/fabro-api-client/src/models/install-sandbox-input.ts create mode 100644 lib/packages/fabro-api-client/src/models/install-sandbox-summary.ts create mode 100644 lib/packages/fabro-api-client/src/models/install-sandbox-validation-response.ts diff --git a/Cargo.lock b/Cargo.lock index 0988ddbba..1c5879578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2111,6 +2111,7 @@ dependencies = [ "fabro-install", "fabro-interview", "fabro-llm", + "fabro-macros", "fabro-model", "fabro-proc", "fabro-redact", diff --git a/apps/fabro-web/app/install-api.test.ts b/apps/fabro-web/app/install-api.test.ts index 1e20ff19d..558af9f29 100644 --- a/apps/fabro-web/app/install-api.test.ts +++ b/apps/fabro-web/app/install-api.test.ts @@ -2,8 +2,10 @@ import { describe, expect, test } from "bun:test"; import { putInstallObjectStore, + putInstallSandbox, readInstallError, testInstallObjectStore, + testInstallSandbox, } from "./install-api"; describe("readInstallError", () => { @@ -92,3 +94,47 @@ describe("install object-store requests", () => { ).rejects.toThrow("Bucket is required."); }); }); + +describe("install sandbox requests", () => { + test("testInstallSandbox posts the install payload to the validation endpoint", async () => { + const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []; + globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { + calls.push({ input, init }); + return Promise.resolve(new Response(JSON.stringify({ ok: true }), { status: 200 })); + }) as typeof fetch; + + await testInstallSandbox("test-install-token", { + provider: "daytona", + api_key: "dtn_test", + }); + + expect(calls).toHaveLength(1); + expect(String(calls[0]!.input)).toBe("/install/sandbox/test"); + expect(calls[0]!.init?.method).toBe("POST"); + expect(calls[0]!.init?.body).toBe( + JSON.stringify({ provider: "daytona", api_key: "dtn_test" }), + ); + }); + + test("putInstallSandbox surfaces structured API errors", async () => { + globalThis.fetch = (() => + Promise.resolve( + new Response( + JSON.stringify({ + errors: [ + { + status: "422", + title: "Unprocessable Entity", + detail: "api_key is required for daytona", + }, + ], + }), + { status: 422, headers: { "Content-Type": "application/json" } }, + ), + )) as typeof fetch; + + await expect( + putInstallSandbox("test-install-token", { provider: "daytona" }), + ).rejects.toThrow("api_key is required for daytona"); + }); +}); diff --git a/apps/fabro-web/app/install-api.ts b/apps/fabro-web/app/install-api.ts index 7c528ea1d..4ef22820e 100644 --- a/apps/fabro-web/app/install-api.ts +++ b/apps/fabro-web/app/install-api.ts @@ -6,6 +6,8 @@ import type { InstallLlmProviderInput, InstallObjectStoreInput, InstallObjectStoreSummary, + InstallSandboxInput, + InstallSandboxSummary, InstallSessionResponse, } from "@qltysh/fabro-api-client"; @@ -17,6 +19,8 @@ export type { InstallLlmProviderInput, InstallObjectStoreInput, InstallObjectStoreSummary, + InstallSandboxInput, + InstallSandboxSummary, InstallSessionResponse, }; @@ -157,6 +161,30 @@ export async function putInstallObjectStore( }); } +export async function testInstallSandbox( + token: string, + input: InstallSandboxInput, +): Promise { + await installRequest(token, { + path: "/install/sandbox/test", + method: "POST", + body: input, + errorFallback: "install sandbox validation failed", + }); +} + +export async function putInstallSandbox( + token: string, + input: InstallSandboxInput, +): Promise { + await installRequest(token, { + path: "/install/sandbox", + method: "PUT", + body: input, + errorFallback: "install sandbox request failed", + }); +} + export async function testInstallGithubToken( token: string, githubToken: string, diff --git a/apps/fabro-web/app/install-app.test.tsx b/apps/fabro-web/app/install-app.test.tsx index 6d1d7d3c4..3b050b72e 100644 --- a/apps/fabro-web/app/install-app.test.tsx +++ b/apps/fabro-web/app/install-app.test.tsx @@ -102,6 +102,25 @@ function renderTreeText( return (node.children ?? []).map(renderTreeText).join(""); } +function findOptionButton( + renderer: TestRenderer.ReactTestRenderer, + title: string, +): TestRenderer.ReactTestInstance { + const button = renderer.root.findAll((node) => { + if (node.type !== "button" || node.props["aria-pressed"] === undefined) { + return false; + } + const titleSpan = node.findAll( + (child) => child.type === "span" && child.children[0] === title, + ); + return titleSpan.length > 0; + })[0]; + if (!button) { + throw new Error(`option button "${title}" not found`); + } + return button; +} + async function waitFor(assertion: () => void, timeoutMs = 1000): Promise { const deadline = Date.now() + timeoutMs; let lastError: unknown; @@ -249,7 +268,7 @@ describe("InstallApp", () => { } }); - test("saves local disk object-store settings and advances to the LLM step", async () => { + test("saves local disk object-store settings and advances to the sandbox step", async () => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const originalConsoleError = console.error; console.error = ((...args: unknown[]) => { @@ -335,7 +354,7 @@ describe("InstallApp", () => { }); await waitFor(() => { - expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials"); + expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime"); }); const backLink = renderer!.root.findAll( (node) => @@ -511,6 +530,277 @@ describe("InstallApp", () => { } }); + test("shows the sandbox provider on the review step", async () => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + const originalConsoleError = console.error; + console.error = ((...args: unknown[]) => { + if ( + typeof args[0] === "string" && + args[0].startsWith("react-test-renderer is deprecated") + ) { + return; + } + originalConsoleError(...args); + }) as typeof console.error; + try { + const fetchMock = mock(() => + Promise.resolve( + new Response( + JSON.stringify({ + completed_steps: ["server", "object_store", "sandbox", "llm", "github"], + llm: { providers: [{ provider: "anthropic" }] }, + server: { canonical_url: "https://fabro.example.com" }, + object_store: { provider: "local" }, + sandbox: { provider: "daytona", api_key_saved: true }, + github: { strategy: "token", username: "octocat" }, + prefill: INSTALL_PREFILL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ), + ); + globalThis.fetch = fetchMock as typeof fetch; + + const testWindow = createTestWindow("https://fabro.example.com/install/review"); + testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token"); + (globalThis as { window?: unknown }).window = testWindow; + + let renderer: TestRenderer.ReactTestRenderer | null = null; + await act(async () => { + renderer = TestRenderer.create( + + + } /> + + , + ); + }); + + await waitFor(() => { + const text = renderTreeText(renderer!.toJSON()); + expect(text).toContain("Daytona"); + expect(text).toContain("Saved"); + }); + + await act(async () => { + renderer?.unmount(); + }); + } finally { + console.error = originalConsoleError; + } + }); + + test("validates Daytona key, saves sandbox, and advances to the LLM step", async () => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + const originalConsoleError = console.error; + console.error = ((...args: unknown[]) => { + if ( + typeof args[0] === "string" && + args[0].startsWith("react-test-renderer is deprecated") + ) { + return; + } + originalConsoleError(...args); + }) as typeof console.error; + try { + const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []; + const fetchMock = mock((input: RequestInfo | URL, init?: RequestInit) => { + fetchCalls.push({ input, init }); + if (String(input) === "/install/session" && fetchCalls.length === 1) { + return Promise.resolve( + new Response( + JSON.stringify({ + completed_steps: ["server", "object_store"], + llm: null, + server: { canonical_url: "https://fabro.example.com" }, + object_store: { provider: "local" }, + sandbox: null, + github: null, + prefill: INSTALL_PREFILL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + } + if ( + String(input) === "/install/sandbox/test" + || String(input) === "/install/sandbox" + ) { + return Promise.resolve( + new Response(JSON.stringify({ ok: true }), { + status: String(input).endsWith("/test") ? 200 : 204, + }), + ); + } + if (String(input) === "/install/session") { + return Promise.resolve( + new Response( + JSON.stringify({ + completed_steps: ["server", "object_store", "sandbox"], + llm: null, + server: { canonical_url: "https://fabro.example.com" }, + object_store: { provider: "local" }, + sandbox: { provider: "daytona", api_key_saved: true }, + github: null, + prefill: INSTALL_PREFILL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + } + throw new Error(`unexpected fetch: ${String(input)}`); + }); + globalThis.fetch = fetchMock as typeof fetch; + + const testWindow = createTestWindow("https://fabro.example.com/install/sandbox"); + testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token"); + (globalThis as { window?: unknown }).window = testWindow; + + let renderer: TestRenderer.ReactTestRenderer | null = null; + await act(async () => { + renderer = TestRenderer.create( + + + } /> + + , + ); + }); + + await waitFor(() => { + expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime"); + }); + + const daytonaButton = findOptionButton(renderer!, "Daytona"); + await act(async () => { + daytonaButton.props.onClick(); + }); + + const apiKeyInput = renderer!.root.findByProps({ name: "sandbox_api_key" }); + await act(async () => { + apiKeyInput.props.onChange({ target: { value: "dtn_secret" } }); + }); + + const form = renderer!.root.findByType("form"); + await act(async () => { + form.props.onSubmit({ preventDefault() {} }); + }); + + await waitFor(() => { + expect(renderTreeText(renderer!.toJSON())).toContain("Add your LLM credentials"); + }); + const calls = fetchCalls.map((call) => String(call.input)); + const testIdx = calls.indexOf("/install/sandbox/test"); + const putIdx = calls.indexOf("/install/sandbox"); + expect(testIdx).toBeGreaterThanOrEqual(0); + expect(putIdx).toBeGreaterThan(testIdx); + const sandboxTestCall = fetchCalls[testIdx]; + expect(sandboxTestCall?.init?.body).toBe( + JSON.stringify({ provider: "daytona", api_key: "dtn_secret" }), + ); + + await act(async () => { + renderer?.unmount(); + }); + } finally { + console.error = originalConsoleError; + } + }); + + test("blocks Daytona save when the API key is missing", async () => { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + const originalConsoleError = console.error; + console.error = ((...args: unknown[]) => { + if ( + typeof args[0] === "string" && + args[0].startsWith("react-test-renderer is deprecated") + ) { + return; + } + originalConsoleError(...args); + }) as typeof console.error; + try { + const fetchCalls: Array<{ input: RequestInfo | URL }> = []; + const fetchMock = mock((input: RequestInfo | URL) => { + fetchCalls.push({ input }); + if (String(input) === "/install/session") { + return Promise.resolve( + new Response( + JSON.stringify({ + completed_steps: ["server", "object_store"], + llm: null, + server: { canonical_url: "https://fabro.example.com" }, + object_store: { provider: "local" }, + sandbox: null, + github: null, + prefill: INSTALL_PREFILL, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + } + throw new Error(`unexpected fetch: ${String(input)}`); + }); + globalThis.fetch = fetchMock as typeof fetch; + + const testWindow = createTestWindow("https://fabro.example.com/install/sandbox"); + testWindow.sessionStorage.setItem("fabro-install-token", "test-install-token"); + (globalThis as { window?: unknown }).window = testWindow; + + let renderer: TestRenderer.ReactTestRenderer | null = null; + await act(async () => { + renderer = TestRenderer.create( + + + } /> + + , + ); + }); + + await waitFor(() => { + expect(renderTreeText(renderer!.toJSON())).toContain("Choose the sandbox runtime"); + }); + + const daytonaButton = findOptionButton(renderer!, "Daytona"); + await act(async () => { + daytonaButton.props.onClick(); + }); + + const form = renderer!.root.findByType("form"); + await act(async () => { + form.props.onSubmit({ preventDefault() {} }); + }); + + await waitFor(() => { + expect(renderTreeText(renderer!.toJSON())).toContain( + "Enter the Daytona API key before continuing.", + ); + }); + expect(fetchCalls.map((call) => String(call.input))).toEqual([ + "/install/session", + ]); + + await act(async () => { + renderer?.unmount(); + }); + } finally { + console.error = originalConsoleError; + } + }); + test("shows the GitHub App callback URL on the review step", async () => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const originalConsoleError = console.error; diff --git a/apps/fabro-web/app/install-app.tsx b/apps/fabro-web/app/install-app.tsx index 4053de577..aeaa57ef2 100644 --- a/apps/fabro-web/app/install-app.tsx +++ b/apps/fabro-web/app/install-app.tsx @@ -19,6 +19,7 @@ import { type InstallGithubAppOwner, type InstallLlmProviderInput, type InstallObjectStoreInput, + type InstallSandboxInput, type InstallSessionResponse, createInstallGithubAppManifest, finishInstall, @@ -27,11 +28,13 @@ import { putInstallGithubToken, putInstallLlm, putInstallObjectStore, + putInstallSandbox, putInstallServer, readStoredInstallToken, testInstallGithubToken, testInstallLlm, testInstallObjectStore, + testInstallSandbox, } from "./install-api"; import { INSTALL_PROVIDERS } from "./install-config"; import { shouldRedirectAfterHealthPoll } from "./install-flow"; @@ -53,6 +56,7 @@ const INSTALL_STEPS = [ { id: "welcome", label: "Welcome", href: "/install/welcome" }, { id: "server", label: "Server", href: "/install/server" }, { id: "object_store", label: "Object store", href: "/install/object-store" }, + { id: "sandbox", label: "Sandbox", href: "/install/sandbox" }, { id: "llm", label: "LLMs", href: "/install/llm" }, { id: "github", label: "GitHub", href: "/install/github" }, { id: "review", label: "Review", href: "/install/review" }, @@ -92,6 +96,12 @@ type ObjectStoreForm = { secretAccessKey: string; manualCredentialsSaved: boolean; }; +type SandboxProvider = NonNullable; +type SandboxForm = { + provider: SandboxProvider; + apiKey: string; + apiKeySaved: boolean; +}; export default function InstallApp() { const navigate = useNavigate(); @@ -108,6 +118,9 @@ export default function InstallApp() { const [objectStoreForm, setObjectStoreForm] = useState(() => defaultObjectStoreForm(), ); + const [sandboxForm, setSandboxForm] = useState(() => + defaultSandboxForm(), + ); const [canonicalUrl, setCanonicalUrl] = useState(""); const [githubStrategy, setGithubStrategy] = useState("token"); const [tokenForm, setTokenForm] = useState({ token: "", username: "" }); @@ -126,6 +139,7 @@ export default function InstallApp() { const regionInputRef = useRef(null); const accessKeyIdInputRef = useRef(null); const secretAccessKeyInputRef = useRef(null); + const sandboxApiKeyInputRef = useRef(null); useEffect(() => { const { token, sanitizedUrl } = consumeInstallTokenFromUrl(window.location.href); @@ -164,6 +178,7 @@ export default function InstallApp() { current || nextSession.server?.canonical_url || nextSession.prefill.canonical_url, ); setObjectStoreForm(hydrateObjectStoreForm(nextSession)); + setSandboxForm((current) => hydrateSandboxForm(current, nextSession)); setLlmSelection((current) => hydrateProviderSelection(current, nextSession), ); @@ -296,6 +311,7 @@ export default function InstallApp() { if (args.next) { const nextSession = await getInstallSession(installToken); setSessionState({ status: "ready", data: nextSession }); + setSandboxForm((current) => hydrateSandboxForm(current, nextSession)); navigate(args.next); } } catch (error) { @@ -346,7 +362,7 @@ export default function InstallApp() { description="Each key you enter is validated before it's saved. Skip a provider by leaving it blank." error={saveError} submitting={submitting} - backHref="/install/object-store" + backHref="/install/sandbox" onSubmit={async () => { const providers = INSTALL_PROVIDERS.map(({ id }) => { const current = llmSelection[id] ?? { apiKey: "" }; @@ -469,7 +485,7 @@ export default function InstallApp() { await putInstallObjectStore(installToken, payload); }, fallback: "Failed to save object-store settings.", - next: "/install/llm", + next: "/install/sandbox", }); }} > @@ -611,6 +627,88 @@ export default function InstallApp() { )} + ) : location.pathname === "/install/sandbox" ? ( + { + if (sandboxForm.provider === "daytona") { + const apiKey = sandboxForm.apiKey.trim(); + const keepStoredKey = sandboxForm.apiKeySaved && !apiKey; + if (!keepStoredKey && !apiKey) { + setSaveError("Enter the Daytona API key before continuing."); + focusInput(sandboxApiKeyInputRef); + return; + } + } + + const payload = buildSandboxPayload(sandboxForm); + await runStepSubmit({ + action: async () => { + if (sandboxForm.provider === "daytona") { + await testInstallSandbox(installToken, payload); + } + await putInstallSandbox(installToken, payload); + }, + fallback: "Failed to save sandbox settings.", + next: "/install/llm", + }); + }} + > + { + setSandboxForm((current) => ({ ...current, provider })); + if (provider === "daytona") { + focusInput(sandboxApiKeyInputRef); + } + }} + /> + {sandboxForm.provider === "daytona" ? ( +
+ + + setSandboxForm((current) => ({ + ...current, + apiKey: event.target.value, + })) + } + className={`${INPUT_CLASS} font-mono`} + placeholder={ + sandboxForm.apiKeySaved ? "•••• (saved)" : "dtn_..." + } + autoComplete="off" + spellCheck={false} + /> + +
+ ) : ( +

+ Fabro will use the host Docker daemon. Make sure the server has + access to /var/run/docker.sock. +

+ )} +
) : location.pathname === "/install/github/done" ? ( ) : location.pathname === "/install/github" ? ( @@ -1018,8 +1116,8 @@ function WelcomeScreen() {

A short walkthrough to confirm the public server URL, choose the shared - object store, validate your LLM credentials, and connect GitHub. When - you finish, Fabro restarts into normal mode. + object store and sandbox runtime, validate your LLM credentials, and + connect GitHub. When you finish, Fabro restarts into normal mode.

    {[ @@ -1028,6 +1126,7 @@ function WelcomeScreen() { "Object store", "Choose local disk or AWS S3 for SlateDB and artifacts.", ], + ["Sandbox", "Choose Docker or Daytona for workflow execution."], ["LLMs", "Validate API keys for Anthropic, OpenAI, or Gemini."], ["GitHub", "Choose a personal access token or a GitHub App."], ["Review", "Double-check the plan, then write the files."], @@ -1165,6 +1264,7 @@ function ReviewScreen({ action={} /> {renderObjectStoreSummaryRows(session?.object_store)} + {renderSandboxSummaryRows(session?.sandbox)} {renderGithubSummaryRows(session?.github, serverUrl)} @@ -1361,6 +1461,19 @@ const OBJECT_STORE_CREDENTIAL_MODE_OPTIONS: ReadonlyArray< }, ]; +const SANDBOX_PROVIDER_OPTIONS: ReadonlyArray> = [ + { + id: "docker", + title: "Docker", + body: "Default. Uses the host Docker daemon to run sandbox containers.", + }, + { + id: "daytona", + title: "Daytona", + body: "Each run gets a managed Daytona cloud sandbox. Requires an API key.", + }, +]; + const GITHUB_OWNER_OPTIONS: ReadonlyArray> = [ { id: "personal", @@ -1716,6 +1829,40 @@ function buildObjectStorePayload(form: ObjectStoreForm): InstallObjectStoreInput return payload; } +function defaultSandboxForm(): SandboxForm { + return { provider: "docker", apiKey: "", apiKeySaved: false }; +} + +function hydrateSandboxForm( + current: SandboxForm, + session: InstallSessionResponse, +): SandboxForm { + const summary = session.sandbox; + if (!summary) { + return current.apiKey ? { ...current, apiKeySaved: false } : defaultSandboxForm(); + } + if (current.apiKey) { + return { ...current, apiKeySaved: Boolean(summary.api_key_saved) }; + } + return { + provider: summary.provider === "daytona" ? "daytona" : "docker", + apiKey: "", + apiKeySaved: Boolean(summary.api_key_saved), + }; +} + +function buildSandboxPayload(form: SandboxForm): InstallSandboxInput { + if (form.provider === "docker") { + return { provider: "docker" }; + } + const apiKey = form.apiKey.trim(); + const payload: InstallSandboxInput = { provider: "daytona" }; + if (apiKey) { + payload.api_key = apiKey; + } + return payload; +} + function focusInput(ref: { current: HTMLInputElement | null }): void { window.setTimeout(() => ref.current?.focus(), 0); } @@ -1798,6 +1945,26 @@ function renderObjectStoreSummaryRows( ); } +function renderSandboxSummaryRows( + sandbox: InstallSessionResponse["sandbox"], +): ReactNode { + if (!sandbox) { + return ; + } + if (sandbox.provider === "daytona") { + return ( + <> + + + + ); + } + return ; +} + function describeGithubAppOwner( owner: InstallGithubAppOwner | undefined, ): string { diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 621493ced..01e1cf8c8 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -247,6 +247,68 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + /install/sandbox/test: + post: + operationId: testInstallSandbox + tags: [Install] + summary: Validate install sandbox configuration + description: Validates the browser-install sandbox-provider selection without persisting it. For Daytona, performs a cheap authenticated call against the Daytona SDK to verify the API key. For Docker, returns ok without further checks. Requires the one-time install token. + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InstallSandboxInput" + responses: + "200": + description: Sandbox configuration validated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/InstallSandboxValidationResponse" + "401": + description: Invalid or missing install token + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "422": + description: Sandbox validation failed + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /install/sandbox: + put: + operationId: putInstallSandbox + tags: [Install] + summary: Save install sandbox configuration + description: Records the sandbox provider selected during browser install. Requires the one-time install token. + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InstallSandboxInput" + responses: + "204": + description: Sandbox configuration recorded + "401": + description: Invalid or missing install token + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "422": + description: Invalid install input + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /install/github/token/test: post: operationId: testInstallGithubToken @@ -2566,6 +2628,10 @@ components: oneOf: - $ref: "#/components/schemas/InstallObjectStoreSummary" - type: "null" + sandbox: + oneOf: + - $ref: "#/components/schemas/InstallSandboxSummary" + - type: "null" github: oneOf: - $ref: "#/components/schemas/InstallGithubSummary" @@ -2715,6 +2781,40 @@ components: manual_credentials_saved: type: boolean + InstallSandboxValidationResponse: + description: Successful response from install-time sandbox validation. + type: object + required: + - ok + properties: + ok: + type: boolean + example: true + + InstallSandboxInput: + description: Sandbox provider selected during browser install. `api_key` is required for Daytona and ignored for Docker. + type: object + required: + - provider + properties: + provider: + type: string + enum: [docker, daytona] + api_key: + type: string + + InstallSandboxSummary: + description: Redacted summary of the sandbox provider selected during browser install. + type: object + required: + - provider + properties: + provider: + type: string + enum: [docker, daytona] + api_key_saved: + type: boolean + InstallGithubTokenTestInput: description: Input for install-time GitHub token validation. type: object diff --git a/lib/crates/fabro-install/src/lib.rs b/lib/crates/fabro-install/src/lib.rs index 2dc1cde21..97463f7e9 100644 --- a/lib/crates/fabro-install/src/lib.rs +++ b/lib/crates/fabro-install/src/lib.rs @@ -54,6 +54,12 @@ pub enum InstallObjectStoreSelection { }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstallSandboxSelection { + Docker, + Daytona, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct InstallObjectStoreEnvPlan { pub writes: Vec, @@ -379,6 +385,24 @@ pub fn write_object_store_settings( } } +pub fn write_sandbox_settings( + doc: &mut toml::Value, + selection: InstallSandboxSelection, +) -> Result<()> { + let provider = match selection { + InstallSandboxSelection::Docker => "docker", + InstallSandboxSelection::Daytona => "daytona", + }; + let root = root_table_mut(doc)?; + let run = ensure_table(root, "run")?; + let sandbox = ensure_table(run, "sandbox")?; + sandbox.insert( + "provider".to_string(), + toml::Value::String(provider.to_string()), + ); + Ok(()) +} + fn restore_optional_file(path: &Path, previous_contents: Option<&str>) -> Result<()> { match previous_contents { Some(contents) => { @@ -505,10 +529,10 @@ mod tests { use super::{ InstallListenConfig, InstallObjectStoreCredentialMode, InstallObjectStoreSelection, - OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_MANAGED_COMMENT, + InstallSandboxSelection, OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_MANAGED_COMMENT, OBJECT_STORE_SECRET_ACCESS_KEY_ENV, PendingSettingsWrite, VaultSecretWrite, default_web_url, merge_server_settings, persist_install_outputs_direct, - write_github_app_settings, write_object_store_settings, + write_github_app_settings, write_object_store_settings, write_sandbox_settings, }; fn format_config_toml() -> String { @@ -855,6 +879,40 @@ name = "custom" ); } + #[test] + fn write_sandbox_settings_records_docker_provider() { + let mut doc = toml::Value::Table(toml::Table::default()); + write_sandbox_settings(&mut doc, InstallSandboxSelection::Docker) + .expect("docker sandbox selection should succeed"); + + assert_eq!( + doc.get("run") + .and_then(toml::Value::as_table) + .and_then(|run| run.get("sandbox")) + .and_then(toml::Value::as_table) + .and_then(|sandbox| sandbox.get("provider")) + .and_then(toml::Value::as_str), + Some("docker") + ); + } + + #[test] + fn write_sandbox_settings_records_daytona_provider() { + let mut doc = toml::Value::Table(toml::Table::default()); + write_sandbox_settings(&mut doc, InstallSandboxSelection::Daytona) + .expect("daytona sandbox selection should succeed"); + + assert_eq!( + doc.get("run") + .and_then(toml::Value::as_table) + .and_then(|run| run.get("sandbox")) + .and_then(toml::Value::as_table) + .and_then(|sandbox| sandbox.get("provider")) + .and_then(toml::Value::as_str), + Some("daytona") + ); + } + #[test] fn persist_install_outputs_direct_only_removes_marked_object_store_keys() { let dir = tempfile::tempdir().unwrap(); diff --git a/lib/crates/fabro-sandbox/src/daytona/mod.rs b/lib/crates/fabro-sandbox/src/daytona/mod.rs index 6fbef8471..1ae11a520 100644 --- a/lib/crates/fabro-sandbox/src/daytona/mod.rs +++ b/lib/crates/fabro-sandbox/src/daytona/mod.rs @@ -40,6 +40,16 @@ async fn build_daytona_client( daytona_sdk::Client::new_with_config(sdk_config).await } +/// Validate a Daytona API key by performing a single authenticated call. Used +/// at install time to confirm the operator-provided credential is accepted by +/// the Daytona control plane before persisting it. The call requests a single +/// sandbox listing entry to keep latency and quota impact minimal. +pub async fn validate_daytona_api_key(api_key: String) -> Result<(), daytona_sdk::DaytonaError> { + let client = build_daytona_client(Some(api_key)).await?; + client.list(None, Some(1), Some(1)).await?; + Ok(()) +} + fn elapsed_ms(start: &Instant) -> u64 { u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX) } diff --git a/lib/crates/fabro-server/Cargo.toml b/lib/crates/fabro-server/Cargo.toml index 10ebdfafb..45ad80746 100644 --- a/lib/crates/fabro-server/Cargo.toml +++ b/lib/crates/fabro-server/Cargo.toml @@ -97,6 +97,7 @@ serde_yaml = "0.9" tracing-subscriber.workspace = true async-trait.workspace = true tokio-util.workspace = true +fabro-macros = { path = "../fabro-macros" } fabro-sandbox = { path = "../fabro-sandbox", features = ["test-support"] } fabro-test = { workspace = true } fabro-types = { path = "../fabro-types", features = ["test-support"] } diff --git a/lib/crates/fabro-server/src/install.rs b/lib/crates/fabro-server/src/install.rs index bbdb47577..e1f634ba0 100644 --- a/lib/crates/fabro-server/src/install.rs +++ b/lib/crates/fabro-server/src/install.rs @@ -17,11 +17,13 @@ use fabro_config::Storage; use fabro_config::bind::{Bind, BindRequest}; use fabro_config::envfile::EnvFileUpdate; use fabro_install::{ - InstallListenConfig, OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_SECRET_ACCESS_KEY_ENV, - PendingSettingsWrite, VaultSecretWrite, merge_server_settings, persist_install_outputs_direct, - write_github_app_settings, write_object_store_settings, write_token_settings, + InstallListenConfig, InstallSandboxSelection, OBJECT_STORE_ACCESS_KEY_ID_ENV, + OBJECT_STORE_SECRET_ACCESS_KEY_ENV, PendingSettingsWrite, VaultSecretWrite, + merge_server_settings, persist_install_outputs_direct, write_github_app_settings, + write_object_store_settings, write_sandbox_settings, write_token_settings, }; use fabro_model::Provider; +use fabro_sandbox::daytona; use fabro_static::EnvVars; use fabro_store::ArtifactStore; use fabro_types::ServerSettings; @@ -219,6 +221,7 @@ struct PendingInstall { llm: Option, server: Option, object_store: Option, + sandbox: Option, github: Option, pending_github_app: Option, } @@ -394,6 +397,45 @@ impl InstallObjectStoreState { } } +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, strum::IntoStaticStr)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +enum InstallSandboxProvider { + Docker, + Daytona, +} + +#[derive(Clone, Debug, Deserialize)] +struct InstallSandboxInput { + provider: InstallSandboxProvider, + api_key: Option, +} + +#[derive(Clone, Debug)] +enum InstallSandboxState { + Docker, + Daytona { api_key: InstallSecret }, +} + +impl InstallSandboxState { + fn as_session_value(&self) -> serde_json::Value { + match self { + Self::Docker => serde_json::json!({ "provider": "docker" }), + Self::Daytona { .. } => serde_json::json!({ + "provider": "daytona", + "api_key_saved": true, + }), + } + } + + fn to_persistence_selection(&self) -> InstallSandboxSelection { + match self { + Self::Docker => InstallSandboxSelection::Docker, + Self::Daytona { .. } => InstallSandboxSelection::Daytona, + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] struct GithubTokenInput { token: String, @@ -545,6 +587,11 @@ pub fn build_install_router(state: InstallAppState) -> Router { "/install/object-store", get(render_install_shell).put(put_install_object_store), ) + .route("/install/sandbox/test", post(post_install_sandbox_test)) + .route( + "/install/sandbox", + get(render_install_shell).put(put_install_sandbox), + ) .route( "/install/github/token/test", post(post_install_github_token_test), @@ -699,6 +746,7 @@ async fn get_install_session( "llm": redacted_llm(&pending_install), "server": pending_install.server, "object_store": redacted_object_store(&pending_install), + "sandbox": redacted_sandbox(&pending_install), "github": redacted_github(&pending_install), "prefill": { "canonical_url": detect_canonical_url(&headers), @@ -867,6 +915,62 @@ async fn put_install_object_store( StatusCode::NO_CONTENT.into_response() } +async fn post_install_sandbox_test( + State(state): State, + headers: HeaderMap, + Query(query): Query, + Json(input): Json, +) -> Response { + if let Some(response) = require_valid_token(&state, &headers, query.token.as_deref()) { + return response; + } + observe_operator(&state, &headers); + + let api_key = { + let pending_install = lock_unpoisoned(&state.pending_install, "install session"); + match resolve_install_sandbox_state(pending_install.sandbox.as_ref(), input) { + Ok(InstallSandboxState::Docker) => { + return Json(serde_json::json!({ "ok": true })).into_response(); + } + Ok(InstallSandboxState::Daytona { api_key }) => api_key.expose_secret().to_string(), + Err(err) => return install_error_response(StatusCode::UNPROCESSABLE_ENTITY, err), + } + }; + + match daytona::validate_daytona_api_key(api_key).await { + Ok(()) => Json(serde_json::json!({ "ok": true })).into_response(), + Err(err) => { + warn!(error = %err, "install sandbox validation failed"); + install_error_response( + StatusCode::UNPROCESSABLE_ENTITY, + format!("daytona credential validation failed: {err}"), + ) + } + } +} + +async fn put_install_sandbox( + State(state): State, + headers: HeaderMap, + Query(query): Query, + Json(input): Json, +) -> Response { + if let Some(response) = require_valid_token(&state, &headers, query.token.as_deref()) { + return response; + } + observe_operator(&state, &headers); + + let mut pending_install = lock_unpoisoned(&state.pending_install, "install session"); + let selection = match resolve_install_sandbox_state(pending_install.sandbox.as_ref(), input) { + Ok(selection) => selection, + Err(err) => return install_error_response(StatusCode::UNPROCESSABLE_ENTITY, err), + }; + + pending_install.sandbox = Some(selection); + info!(step = "sandbox", "install step completed"); + StatusCode::NO_CONTENT.into_response() +} + fn trim_install_field(value: Option) -> Option { value .map(|value| value.trim().to_string()) @@ -882,6 +986,27 @@ fn default_local_object_store_root(state: &InstallAppState) -> String { .to_string() } +fn resolve_install_sandbox_state( + current: Option<&InstallSandboxState>, + input: InstallSandboxInput, +) -> Result { + match input.provider { + InstallSandboxProvider::Docker => Ok(InstallSandboxState::Docker), + InstallSandboxProvider::Daytona => { + let api_key = match trim_install_field(input.api_key) { + Some(value) => InstallSecret::new(value), + None => match current { + Some(InstallSandboxState::Daytona { api_key }) => { + InstallSecret::new(api_key.expose_secret()) + } + _ => return Err("api_key is required for daytona".to_string()), + }, + }; + Ok(InstallSandboxState::Daytona { api_key }) + } + } +} + fn resolve_install_object_store_state( current: Option<&InstallObjectStoreState>, input: InstallObjectStoreInput, @@ -1337,6 +1462,9 @@ async fn post_install_finish( let Some(object_store) = pending_install.object_store else { return missing_step_response("object_store"); }; + let Some(sandbox) = pending_install.sandbox else { + return missing_step_response("sandbox"); + }; let Some(llm) = pending_install.llm else { return missing_step_response("llm"); }; @@ -1360,7 +1488,19 @@ async fn post_install_finish( return install_error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()); } }; + if let Err(err) = write_sandbox_settings(&mut settings_doc, sandbox.to_persistence_selection()) + { + return install_error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()); + } let mut vault_secrets = Vec::new(); + if let InstallSandboxState::Daytona { api_key } = &sandbox { + vault_secrets.push(VaultSecretWrite { + name: EnvVars::DAYTONA_API_KEY.to_string(), + value: api_key.expose_secret().to_string(), + secret_type: VaultSecretType::Environment, + description: None, + }); + } for provider in llm.providers { let credential = AuthCredential { provider: provider.provider, @@ -1522,6 +1662,15 @@ async fn post_install_finish( { *manual_credentials = None; } + { + let mut pending_install = lock_unpoisoned(&state.pending_install, "install session"); + if matches!( + pending_install.sandbox.as_ref(), + Some(InstallSandboxState::Daytona { .. }) + ) { + pending_install.sandbox = Some(InstallSandboxState::Docker); + } + } if let Some(finish_hook) = state.finish_hook.clone() { let info = InstallFinishInfo { @@ -1698,6 +1847,9 @@ fn completed_steps(pending_install: &PendingInstall) -> Vec<&'static str> { if pending_install.object_store.is_some() { steps.push("object_store"); } + if pending_install.sandbox.is_some() { + steps.push("sandbox"); + } if pending_install.llm.is_some() { steps.push("llm"); } @@ -1747,6 +1899,13 @@ fn redacted_object_store(pending_install: &PendingInstall) -> serde_json::Value ) } +fn redacted_sandbox(pending_install: &PendingInstall) -> serde_json::Value { + pending_install.sandbox.as_ref().map_or_else( + || serde_json::Value::Null, + InstallSandboxState::as_session_value, + ) +} + fn missing_step_response(step: &str) -> Response { ApiError::new( StatusCode::UNPROCESSABLE_ENTITY, diff --git a/lib/crates/fabro-server/tests/it/api/install.rs b/lib/crates/fabro-server/tests/it/api/install.rs index cd11ef4d8..22943520e 100644 --- a/lib/crates/fabro-server/tests/it/api/install.rs +++ b/lib/crates/fabro-server/tests/it/api/install.rs @@ -168,9 +168,31 @@ async fn put_install_object_store_local(app: &axum::Router, token: &str) { put_install_object_store(app, token, r#"{"provider":"local"}"#).await; } +async fn put_install_sandbox(app: &axum::Router, token: &str, body: &str) { + let response = app + .clone() + .oneshot( + Request::builder() + .method("PUT") + .uri("/install/sandbox") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from(body.to_string())) + .expect("sandbox install request should build"), + ) + .await + .unwrap(); + response_status(response, StatusCode::NO_CONTENT, "PUT /install/sandbox").await; +} + +async fn put_install_sandbox_docker(app: &axum::Router, token: &str) { + put_install_sandbox(app, token, r#"{"provider":"docker"}"#).await; +} + async fn configure_token_install(app: &axum::Router, token: &str) { put_install_server(app, token, "https://fabro.example.com").await; put_install_object_store_local(app, token).await; + put_install_sandbox_docker(app, token).await; put_install_llm(app, token).await; put_install_github_token(app, token, "brynary").await; } @@ -320,6 +342,12 @@ async fn install_endpoints_reject_missing_and_wrong_tokens() { "/install/object-store", Some(r#"{"provider":"local"}"#), ), + ( + "POST", + "/install/sandbox/test", + Some(r#"{"provider":"docker"}"#), + ), + ("PUT", "/install/sandbox", Some(r#"{"provider":"docker"}"#)), ( "POST", "/install/github/token/test", @@ -574,6 +602,7 @@ async fn manual_object_store_session_is_redacted_and_blank_resubmit_preserves_cr ) .await; put_install_server(&app, "test-install-token", "https://fabro.example.com").await; + put_install_sandbox_docker(&app, "test-install-token").await; put_install_llm(&app, "test-install-token").await; put_install_github_token(&app, "test-install-token", "brynary").await; @@ -650,6 +679,7 @@ async fn switching_object_store_from_manual_to_runtime_clears_saved_manual_crede assert!(!rendered_session.contains("switch-secret-value")); put_install_server(&app, "test-install-token", "https://fabro.example.com").await; + put_install_sandbox_docker(&app, "test-install-token").await; put_install_llm(&app, "test-install-token").await; put_install_github_token(&app, "test-install-token", "brynary").await; @@ -704,6 +734,7 @@ async fn runtime_object_store_finish_removes_managed_aws_keys_but_keeps_unmarked r#"{"provider":"s3","bucket":"fabro-data","region":"us-east-1","credential_mode":"runtime"}"#, ) .await; + put_install_sandbox_docker(&app, "test-install-token").await; put_install_llm(&app, "test-install-token").await; put_install_github_token(&app, "test-install-token", "brynary").await; @@ -770,6 +801,14 @@ async fn token_install_finish_persists_settings_env_and_vault() { let settings = std::fs::read_to_string(&config_path).unwrap(); assert!(settings.contains("https://fabro.example.com")); assert!(settings.contains("strategy = \"token\"")); + assert!( + settings.contains("[run.sandbox]"), + "settings.toml should contain [run.sandbox]" + ); + assert!( + settings.contains("provider = \"docker\""), + "settings.toml should record explicit docker sandbox provider" + ); let resolved = ServerSettingsBuilder::from_toml(&settings) .expect("settings should resolve") .server; @@ -918,6 +957,7 @@ async fn app_install_finish_omits_dev_token_and_does_not_write_it() { .await; put_install_object_store_local(&app, "test-install-token").await; + put_install_sandbox_docker(&app, "test-install-token").await; let manifest_response = app .clone() @@ -1900,6 +1940,7 @@ async fn install_finish_failure_with_manual_credentials_does_not_leak_values() { ), ) .await; + put_install_sandbox_docker(&app, "test-install-token").await; put_install_llm(&app, "test-install-token").await; put_install_github_token(&app, "test-install-token", "brynary").await; @@ -1974,6 +2015,7 @@ async fn install_finish_failure_reports_only_env_keys_actually_removed() { r#"{"provider":"s3","bucket":"fabro-data","region":"us-east-1","credential_mode":"runtime"}"#, ) .await; + put_install_sandbox_docker(&app, "test-install-token").await; put_install_llm(&app, "test-install-token").await; put_install_github_token(&app, "test-install-token", "brynary").await; @@ -2058,3 +2100,365 @@ async fn install_finish_failure_does_not_create_home_dev_token() { let server_env = std::fs::read_to_string(storage.runtime_directory().env_path()).unwrap(); assert!(server_env.contains(&format!("FABRO_DEV_TOKEN={storage_dev_token}"))); } + +#[tokio::test] +async fn sandbox_docker_save_records_explicit_provider_in_session() { + let app = build_install_router(InstallAppState::for_test("test-install-token")); + + let validation_response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/install/sandbox/test") + .header("authorization", "Bearer test-install-token") + .header("content-type", "application/json") + .body(Body::from(r#"{"provider":"docker"}"#)) + .unwrap(), + ) + .await + .unwrap(); + let validation_body = response_json( + validation_response, + StatusCode::OK, + "POST /install/sandbox/test", + ) + .await; + assert_eq!(validation_body["ok"], true); + + put_install_sandbox_docker(&app, "test-install-token").await; + + let session_response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/install/session") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let session_body = + response_json(session_response, StatusCode::OK, "GET /install/session").await; + assert_eq!(session_body["sandbox"]["provider"], "docker"); + assert!( + session_body["completed_steps"] + .as_array() + .unwrap() + .iter() + .any(|value| value == "sandbox") + ); +} + +#[tokio::test] +async fn sandbox_daytona_without_api_key_is_rejected() { + let app = build_install_router(InstallAppState::for_test("test-install-token")); + + let response = app + .oneshot( + Request::builder() + .method("PUT") + .uri("/install/sandbox") + .header("authorization", "Bearer test-install-token") + .header("content-type", "application/json") + .body(Body::from(r#"{"provider":"daytona"}"#)) + .unwrap(), + ) + .await + .unwrap(); + let body = response_json( + response, + StatusCode::UNPROCESSABLE_ENTITY, + "PUT /install/sandbox without api_key", + ) + .await; + assert_eq!( + body["errors"][0]["detail"], + "api_key is required for daytona" + ); +} + +#[tokio::test] +async fn sandbox_daytona_test_endpoint_without_api_key_is_rejected() { + let app = build_install_router(InstallAppState::for_test("test-install-token")); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/install/sandbox/test") + .header("authorization", "Bearer test-install-token") + .header("content-type", "application/json") + .body(Body::from(r#"{"provider":"daytona"}"#)) + .unwrap(), + ) + .await + .unwrap(); + let body = response_json( + response, + StatusCode::UNPROCESSABLE_ENTITY, + "POST /install/sandbox/test without api_key", + ) + .await; + assert_eq!( + body["errors"][0]["detail"], + "api_key is required for daytona" + ); +} + +#[tokio::test] +async fn sandbox_daytona_resave_without_api_key_preserves_saved_key() { + let temp_dir = tempfile::tempdir().unwrap(); + let config_path = temp_dir.path().join("settings.toml"); + let app = build_install_router(InstallAppState::for_test_with_paths( + "test-install-token", + temp_dir.path(), + &config_path, + )); + + let api_key = "dtn_keep_me"; + put_install_sandbox( + &app, + "test-install-token", + &format!(r#"{{"provider":"daytona","api_key":"{api_key}"}}"#), + ) + .await; + put_install_sandbox(&app, "test-install-token", r#"{"provider":"daytona"}"#).await; + + let session_response = app + .clone() + .oneshot( + Request::builder() + .method("GET") + .uri("/install/session") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let session_body = + response_json(session_response, StatusCode::OK, "GET /install/session").await; + assert_eq!(session_body["sandbox"]["provider"], "daytona"); + assert_eq!(session_body["sandbox"]["api_key_saved"], true); + + put_install_server(&app, "test-install-token", "https://fabro.example.com").await; + put_install_object_store_local(&app, "test-install-token").await; + put_install_llm(&app, "test-install-token").await; + put_install_github_token(&app, "test-install-token", "brynary").await; + + let finish_response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/install/finish") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + response_status( + finish_response, + StatusCode::ACCEPTED, + "POST /install/finish", + ) + .await; + + let vault = Vault::load(Storage::new(temp_dir.path()).secrets_path()).unwrap(); + assert_eq!(vault.get("DAYTONA_API_KEY"), Some(api_key)); +} + +#[tokio::test] +async fn sandbox_switching_from_daytona_to_docker_drops_saved_key() { + let temp_dir = tempfile::tempdir().unwrap(); + let config_path = temp_dir.path().join("settings.toml"); + let app = build_install_router(InstallAppState::for_test_with_paths( + "test-install-token", + temp_dir.path(), + &config_path, + )); + + put_install_sandbox( + &app, + "test-install-token", + r#"{"provider":"daytona","api_key":"dtn_will_be_dropped"}"#, + ) + .await; + put_install_sandbox_docker(&app, "test-install-token").await; + + put_install_server(&app, "test-install-token", "https://fabro.example.com").await; + put_install_object_store_local(&app, "test-install-token").await; + put_install_llm(&app, "test-install-token").await; + put_install_github_token(&app, "test-install-token", "brynary").await; + + let finish_response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/install/finish") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + response_status( + finish_response, + StatusCode::ACCEPTED, + "POST /install/finish", + ) + .await; + + let settings = std::fs::read_to_string(&config_path).unwrap(); + assert!(settings.contains("provider = \"docker\"")); + let vault = Vault::load(Storage::new(temp_dir.path()).secrets_path()).unwrap(); + assert_eq!(vault.get("DAYTONA_API_KEY"), None); +} + +#[tokio::test] +async fn sandbox_daytona_save_redacts_api_key_in_session() { + let app = build_install_router(InstallAppState::for_test("test-install-token")); + let api_key = "dtn_should_not_leak"; + + put_install_sandbox( + &app, + "test-install-token", + &format!(r#"{{"provider":"daytona","api_key":"{api_key}"}}"#), + ) + .await; + + let session_response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/install/session") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let session_body = + response_json(session_response, StatusCode::OK, "GET /install/session").await; + assert_eq!(session_body["sandbox"]["provider"], "daytona"); + assert_eq!(session_body["sandbox"]["api_key_saved"], true); + let rendered = session_body.to_string(); + assert!(!rendered.contains(api_key)); +} + +#[tokio::test] +async fn install_finish_requires_sandbox_step() { + let app = build_install_router(InstallAppState::for_test("test-install-token")); + + put_install_server(&app, "test-install-token", "https://fabro.example.com").await; + put_install_object_store_local(&app, "test-install-token").await; + put_install_llm(&app, "test-install-token").await; + put_install_github_token(&app, "test-install-token", "brynary").await; + + let finish_response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/install/finish") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let finish_body = response_json( + finish_response, + StatusCode::UNPROCESSABLE_ENTITY, + "POST /install/finish", + ) + .await; + assert_eq!( + finish_body["errors"][0]["detail"], + "install step 'sandbox' is incomplete" + ); +} + +#[tokio::test] +async fn daytona_install_finish_writes_settings_and_vault_secret() { + let temp_dir = tempfile::tempdir().unwrap(); + let config_path = temp_dir.path().join("settings.toml"); + let app = build_install_router(InstallAppState::for_test_with_paths( + "test-install-token", + temp_dir.path(), + &config_path, + )); + + let api_key = "dtn_test_secret"; + put_install_server(&app, "test-install-token", "https://fabro.example.com").await; + put_install_object_store_local(&app, "test-install-token").await; + put_install_sandbox( + &app, + "test-install-token", + &format!(r#"{{"provider":"daytona","api_key":"{api_key}"}}"#), + ) + .await; + put_install_llm(&app, "test-install-token").await; + put_install_github_token(&app, "test-install-token", "brynary").await; + + let finish_response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/install/finish") + .header("authorization", "Bearer test-install-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + response_status( + finish_response, + StatusCode::ACCEPTED, + "POST /install/finish", + ) + .await; + + let settings = std::fs::read_to_string(&config_path).unwrap(); + assert!( + settings.contains("[run.sandbox]"), + "settings.toml should contain [run.sandbox]" + ); + assert!( + settings.contains("provider = \"daytona\""), + "settings.toml should record daytona sandbox provider" + ); + + let vault = Vault::load(Storage::new(temp_dir.path()).secrets_path()).unwrap(); + assert_eq!(vault.get("DAYTONA_API_KEY"), Some(api_key)); +} + +#[fabro_macros::e2e_test(live("DAYTONA_API_KEY"))] +async fn sandbox_daytona_test_endpoint_validates_real_api_key() { + let api_key = std::env::var(fabro_static::EnvVars::DAYTONA_API_KEY) + .expect("DAYTONA_API_KEY must be set for live test"); + let app = build_install_router(InstallAppState::for_test("test-install-token")); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/install/sandbox/test") + .header("authorization", "Bearer test-install-token") + .header("content-type", "application/json") + .body(Body::from(format!( + r#"{{"provider":"daytona","api_key":"{api_key}"}}"# + ))) + .unwrap(), + ) + .await + .unwrap(); + let body = response_json( + response, + StatusCode::OK, + "POST /install/sandbox/test (live daytona)", + ) + .await; + assert_eq!(body["ok"], true); +} diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 7177f0bbf..eddc1bd89 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -97,6 +97,9 @@ models/install-object-store-input.ts models/install-object-store-summary.ts models/install-object-store-validation-response.ts models/install-prefill.ts +models/install-sandbox-input.ts +models/install-sandbox-summary.ts +models/install-sandbox-validation-response.ts models/install-server-config-input.ts models/install-session-response.ts models/integration-webhooks-settings.ts 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 5a7b5a5cd..820a5dc3a 100644 --- a/lib/packages/fabro-api-client/src/api/install-api.ts +++ b/lib/packages/fabro-api-client/src/api/install-api.ts @@ -46,6 +46,10 @@ import type { InstallObjectStoreInput } from '../models'; // @ts-ignore import type { InstallObjectStoreValidationResponse } from '../models'; // @ts-ignore +import type { InstallSandboxInput } from '../models'; +// @ts-ignore +import type { InstallSandboxValidationResponse } from '../models'; +// @ts-ignore import type { InstallServerConfigInput } from '../models'; // @ts-ignore import type { InstallSessionResponse } from '../models'; @@ -298,6 +302,41 @@ export const InstallApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * Records the sandbox provider selected during browser install. Requires the one-time install token. + * @summary Save install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + putInstallSandbox: async (installSandboxInput: InstallSandboxInput, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'installSandboxInput' is not null or undefined + assertParamExists('putInstallSandbox', 'installSandboxInput', installSandboxInput) + const localVarPath = `/install/sandbox`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(installSandboxInput, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Records the canonical server URL confirmed by the operator. Requires the one-time install token. * @summary Save install server configuration @@ -433,6 +472,41 @@ export const InstallApiAxiosParamCreator = function (configuration?: Configurati localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(installObjectStoreInput, localVarRequestOptions, configuration) + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Validates the browser-install sandbox-provider selection without persisting it. For Daytona, performs a cheap authenticated call against the Daytona SDK to verify the API key. For Docker, returns ok without further checks. Requires the one-time install token. + * @summary Validate install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + testInstallSandbox: async (installSandboxInput: InstallSandboxInput, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'installSandboxInput' is not null or undefined + assertParamExists('testInstallSandbox', 'installSandboxInput', installSandboxInput) + const localVarPath = `/install/sandbox/test`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Content-Type'] = 'application/json'; + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(installSandboxInput, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -537,6 +611,19 @@ export const InstallApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['InstallApi.putInstallObjectStore']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Records the sandbox provider selected during browser install. Requires the one-time install token. + * @summary Save install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async putInstallSandbox(installSandboxInput: InstallSandboxInput, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.putInstallSandbox(installSandboxInput, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['InstallApi.putInstallSandbox']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Records the canonical server URL confirmed by the operator. Requires the one-time install token. * @summary Save install server configuration @@ -589,6 +676,19 @@ export const InstallApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['InstallApi.testInstallObjectStore']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Validates the browser-install sandbox-provider selection without persisting it. For Daytona, performs a cheap authenticated call against the Daytona SDK to verify the API key. For Docker, returns ok without further checks. Requires the one-time install token. + * @summary Validate install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async testInstallSandbox(installSandboxInput: InstallSandboxInput, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.testInstallSandbox(installSandboxInput, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['InstallApi.testInstallSandbox']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, } }; @@ -667,6 +767,16 @@ export const InstallApiFactory = function (configuration?: Configuration, basePa putInstallObjectStore(installObjectStoreInput: InstallObjectStoreInput, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.putInstallObjectStore(installObjectStoreInput, options).then((request) => request(axios, basePath)); }, + /** + * Records the sandbox provider selected during browser install. Requires the one-time install token. + * @summary Save install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + putInstallSandbox(installSandboxInput: InstallSandboxInput, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.putInstallSandbox(installSandboxInput, options).then((request) => request(axios, basePath)); + }, /** * Records the canonical server URL confirmed by the operator. Requires the one-time install token. * @summary Save install server configuration @@ -707,6 +817,16 @@ export const InstallApiFactory = function (configuration?: Configuration, basePa testInstallObjectStore(installObjectStoreInput: InstallObjectStoreInput, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.testInstallObjectStore(installObjectStoreInput, options).then((request) => request(axios, basePath)); }, + /** + * Validates the browser-install sandbox-provider selection without persisting it. For Daytona, performs a cheap authenticated call against the Daytona SDK to verify the API key. For Docker, returns ok without further checks. Requires the one-time install token. + * @summary Validate install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + testInstallSandbox(installSandboxInput: InstallSandboxInput, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.testInstallSandbox(installSandboxInput, options).then((request) => request(axios, basePath)); + }, }; }; @@ -790,6 +910,17 @@ export class InstallApi extends BaseAPI { return InstallApiFp(this.configuration).putInstallObjectStore(installObjectStoreInput, options).then((request) => request(this.axios, this.basePath)); } + /** + * Records the sandbox provider selected during browser install. Requires the one-time install token. + * @summary Save install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public putInstallSandbox(installSandboxInput: InstallSandboxInput, options?: RawAxiosRequestConfig) { + return InstallApiFp(this.configuration).putInstallSandbox(installSandboxInput, options).then((request) => request(this.axios, this.basePath)); + } + /** * Records the canonical server URL confirmed by the operator. Requires the one-time install token. * @summary Save install server configuration @@ -833,5 +964,16 @@ export class InstallApi extends BaseAPI { public testInstallObjectStore(installObjectStoreInput: InstallObjectStoreInput, options?: RawAxiosRequestConfig) { return InstallApiFp(this.configuration).testInstallObjectStore(installObjectStoreInput, options).then((request) => request(this.axios, this.basePath)); } + + /** + * Validates the browser-install sandbox-provider selection without persisting it. For Daytona, performs a cheap authenticated call against the Daytona SDK to verify the API key. For Docker, returns ok without further checks. Requires the one-time install token. + * @summary Validate install sandbox configuration + * @param {InstallSandboxInput} installSandboxInput + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public testInstallSandbox(installSandboxInput: InstallSandboxInput, options?: RawAxiosRequestConfig) { + return InstallApiFp(this.configuration).testInstallSandbox(installSandboxInput, options).then((request) => request(this.axios, this.basePath)); + } } 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 17c4d55db..07767ebf9 100644 --- a/lib/packages/fabro-api-client/src/api/runs-api.ts +++ b/lib/packages/fabro-api-client/src/api/runs-api.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -67,7 +67,7 @@ import type { TimelineEntryResponse } from '../models'; export const RunsApiAxiosParamCreator = function (configuration?: Configuration) { return { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -189,7 +189,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -231,7 +231,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -318,10 +318,10 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -401,7 +401,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -541,7 +541,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -625,7 +625,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -787,10 +787,10 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -832,7 +832,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -874,7 +874,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -914,7 +914,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration) }; }, /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1003,7 +1003,7 @@ export const RunsApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = RunsApiAxiosParamCreator(configuration) return { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1044,7 +1044,7 @@ export const RunsApiFp = function(configuration?: Configuration) { /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1058,7 +1058,7 @@ export const RunsApiFp = function(configuration?: Configuration) { * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1083,10 +1083,10 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1110,7 +1110,7 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1155,7 +1155,7 @@ export const RunsApiFp = function(configuration?: Configuration) { * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1181,7 +1181,7 @@ export const RunsApiFp = function(configuration?: Configuration) { /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1231,10 +1231,10 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1247,7 +1247,7 @@ export const RunsApiFp = function(configuration?: Configuration) { /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1261,7 +1261,7 @@ export const RunsApiFp = function(configuration?: Configuration) { * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1272,7 +1272,7 @@ export const RunsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1307,7 +1307,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? const localVarFp = RunsApiFp(configuration) return { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1339,7 +1339,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1350,7 +1350,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1369,10 +1369,10 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.deleteRun(id, force, options).then((request) => request(axios, basePath)); }, /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1390,7 +1390,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.getRunPullRequest(id, options).then((request) => request(axios, basePath)); }, /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1426,7 +1426,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1446,7 +1446,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1484,10 +1484,10 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.retrieveRunGraph(id, options).then((request) => request(axios, basePath)); }, /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1497,7 +1497,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1508,7 +1508,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1516,7 +1516,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? return localVarFp.startRun(id, startRunRequest, options).then((request) => request(axios, basePath)); }, /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1543,7 +1543,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath? */ export class RunsApi extends BaseAPI { /** - * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. + * Marks a terminal run (`succeeded`, `failed`, or `dead`) as `archived`. Archived runs are hidden from default listings and are read-only until unarchived. Idempotent on already-archived runs. Returns 409 if the run is not terminal. * @summary Archive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1578,7 +1578,7 @@ export class RunsApi extends BaseAPI { /** * Creates a new workflow run in `submitted` status from a self-contained manifest. * @summary Create Run - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1590,7 +1590,7 @@ export class RunsApi extends BaseAPI { * Creates a pull request for a completed run on GitHub and persists the record on the server. * @summary Create Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {CreateRunPullRequestRequest} createRunPullRequestRequest + * @param {CreateRunPullRequestRequest} createRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1611,10 +1611,10 @@ export class RunsApi extends BaseAPI { } /** - * Creates a new run from a checkpoint of the source run. The source run is left untouched. + * Creates a new run from a checkpoint of the source run. The source run is left untouched. * @summary Fork Run * @param {string} id Unique run identifier (ULID). - * @param {ForkRequest} [forkRequest] + * @param {ForkRequest} [forkRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1634,7 +1634,7 @@ export class RunsApi extends BaseAPI { } /** - * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. + * Returns checkpoint timeline entries read from the run metadata branch. This endpoint does not rebuild missing metadata branches. * @summary Get Run Timeline * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1673,7 +1673,7 @@ export class RunsApi extends BaseAPI { * Merges the stored pull request for a run on GitHub. * @summary Merge Run Pull Request * @param {string} id Unique run identifier (ULID). - * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest + * @param {MergeRunPullRequestRequest} mergeRunPullRequestRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1695,7 +1695,7 @@ export class RunsApi extends BaseAPI { /** * Validates and renders a workflow manifest as SVG without creating a run. * @summary Render Workflow Graph - * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest + * @param {RenderWorkflowGraphRequest} renderWorkflowGraphRequest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1737,10 +1737,10 @@ export class RunsApi extends BaseAPI { } /** - * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. + * Creates a new run from an earlier checkpoint of a terminal source run, archives the source run, and records `run.superseded_by` on the source after archive succeeds. Returns 207 when the new run was created but the source archive step failed. * @summary Rewind Run * @param {string} id Unique run identifier (ULID). - * @param {RewindRequest} [rewindRequest] + * @param {RewindRequest} [rewindRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1751,7 +1751,7 @@ export class RunsApi extends BaseAPI { /** * Validates a workflow manifest without creating a run. * @summary Validate Workflow Manifest - * @param {RunManifest} runManifest + * @param {RunManifest} runManifest * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1763,7 +1763,7 @@ export class RunsApi extends BaseAPI { * Starts a submitted run, queuing it for execution. Provide `resume=true` to resume an interrupted run from checkpoint. Returns 409 if the run is not startable. * @summary Start Run * @param {string} id Unique run identifier (ULID). - * @param {StartRunRequest} [startRunRequest] + * @param {StartRunRequest} [startRunRequest] * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -1772,7 +1772,7 @@ export class RunsApi extends BaseAPI { } /** - * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. + * Restores an archived run to its prior terminal status. Idempotent on runs that are terminal but not archived (returns the current status without emitting an event). Returns 409 if the run is active. * @summary Unarchive Run * @param {string} id Unique run identifier (ULID). * @param {*} [options] Override http request option. @@ -1793,3 +1793,4 @@ export class RunsApi extends BaseAPI { return RunsApiFp(this.configuration).unpauseRun(id, options).then((request) => request(this.axios, this.basePath)); } } + 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 f34f91289..394ac9aa4 100644 --- a/lib/packages/fabro-api-client/src/models/diff-stats.ts +++ b/lib/packages/fabro-api-client/src/models/diff-stats.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -15,7 +15,7 @@ /** - * Aggregate `+/-` line counts across all files in a diff (or the unified patch in degraded mode). Binary, sensitive, symlink, and submodule files contribute 0/0 since they have no line-level diff. Both fields are 0 for empty / pre-start envelopes. + * Aggregate `+/-` line counts across all files in a diff (or the unified patch in degraded mode). Binary, sensitive, symlink, and submodule files contribute 0/0 since they have no line-level diff. Both fields are 0 for empty / pre-start envelopes. */ export interface DiffStats { /** @@ -27,3 +27,4 @@ export interface DiffStats { */ 'deletions': number; } + 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 0b5b0b59f..5da18402a 100644 --- a/lib/packages/fabro-api-client/src/models/fork-request.ts +++ b/lib/packages/fabro-api-client/src/models/fork-request.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -27,3 +27,4 @@ export interface ForkRequest { */ 'push'?: boolean | null; } + 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..0b03a87dd 100644 --- a/lib/packages/fabro-api-client/src/models/fork-response.ts +++ b/lib/packages/fabro-api-client/src/models/fork-response.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -22,3 +22,4 @@ export interface ForkResponse { 'new_run_id': string; 'target': string; } + diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index a3a930f86..4d3fc82ac 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -76,6 +76,9 @@ export * from './install-object-store-input'; export * from './install-object-store-summary'; export * from './install-object-store-validation-response'; export * from './install-prefill'; +export * from './install-sandbox-input'; +export * from './install-sandbox-summary'; +export * from './install-sandbox-validation-response'; export * from './install-server-config-input'; export * from './install-session-response'; export * from './integration-webhooks-settings'; 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..1e6a940b0 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 @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -39,3 +39,5 @@ export const InstallObjectStoreInputCredentialModeEnum = { } as const; export type InstallObjectStoreInputCredentialModeEnum = typeof InstallObjectStoreInputCredentialModeEnum[keyof typeof InstallObjectStoreInputCredentialModeEnum]; + + 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..7d473a448 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 @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -38,3 +38,5 @@ export const InstallObjectStoreSummaryCredentialModeEnum = { } as const; export type InstallObjectStoreSummaryCredentialModeEnum = typeof InstallObjectStoreSummaryCredentialModeEnum[keyof typeof InstallObjectStoreSummaryCredentialModeEnum]; + + 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..7bdc45ef8 100644 --- a/lib/packages/fabro-api-client/src/models/install-prefill.ts +++ b/lib/packages/fabro-api-client/src/models/install-prefill.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -21,3 +21,4 @@ export interface InstallPrefill { 'canonical_url': string; 'object_store_local_root': string; } + 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 new file mode 100644 index 000000000..b5b9f93e1 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/install-sandbox-input.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Sandbox provider selected during browser install. `api_key` is required for Daytona and ignored for Docker. + */ +export interface InstallSandboxInput { + 'provider': InstallSandboxInputProviderEnum; + 'api_key'?: string; +} + +export const InstallSandboxInputProviderEnum = { + DOCKER: 'docker', + DAYTONA: 'daytona' +} as const; + +export type InstallSandboxInputProviderEnum = typeof InstallSandboxInputProviderEnum[keyof typeof InstallSandboxInputProviderEnum]; + + 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 new file mode 100644 index 000000000..41760d310 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/install-sandbox-summary.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Redacted summary of the sandbox provider selected during browser install. + */ +export interface InstallSandboxSummary { + 'provider': InstallSandboxSummaryProviderEnum; + 'api_key_saved'?: boolean; +} + +export const InstallSandboxSummaryProviderEnum = { + DOCKER: 'docker', + DAYTONA: 'daytona' +} as const; + +export type InstallSandboxSummaryProviderEnum = typeof InstallSandboxSummaryProviderEnum[keyof typeof InstallSandboxSummaryProviderEnum]; + + 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 new file mode 100644 index 000000000..9da872961 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/install-sandbox-validation-response.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Successful response from install-time sandbox validation. + */ +export interface InstallSandboxValidationResponse { + 'ok': boolean; +} + 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 5f0f4f677..865c629f8 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 @@ -27,6 +27,9 @@ import type { InstallObjectStoreSummary } from './install-object-store-summary'; import type { InstallPrefill } from './install-prefill'; // May contain unused imports in some cases // @ts-ignore +import type { InstallSandboxSummary } from './install-sandbox-summary'; +// May contain unused imports in some cases +// @ts-ignore import type { InstallServerConfigInput } from './install-server-config-input'; /** @@ -37,6 +40,7 @@ export interface InstallSessionResponse { 'llm'?: InstallLlmSummary | null; 'server'?: InstallServerConfigInput | null; 'object_store'?: InstallObjectStoreSummary | null; + 'sandbox'?: InstallSandboxSummary | null; 'github'?: InstallGithubSummary | null; 'prefill': InstallPrefill; } 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..7ae1fb192 100644 --- a/lib/packages/fabro-api-client/src/models/log-destination.ts +++ b/lib/packages/fabro-api-client/src/models/log-destination.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -21,3 +21,6 @@ export const LogDestination = { } as const; export type LogDestination = typeof LogDestination[keyof typeof LogDestination]; + + + 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 e4652f996..15e9322fd 100644 --- a/lib/packages/fabro-api-client/src/models/manifest-args.ts +++ b/lib/packages/fabro-api-client/src/models/manifest-args.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -32,3 +32,4 @@ export interface ManifestArgs { 'preserve_sandbox'?: boolean; 'label'?: Array; } + 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 afcd3398d..438fd8b84 100644 --- a/lib/packages/fabro-api-client/src/models/rewind-request.ts +++ b/lib/packages/fabro-api-client/src/models/rewind-request.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -27,3 +27,4 @@ export interface RewindRequest { */ 'push'?: boolean | null; } + 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..a926b9b4c 100644 --- a/lib/packages/fabro-api-client/src/models/rewind-response.ts +++ b/lib/packages/fabro-api-client/src/models/rewind-response.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -24,3 +24,4 @@ export interface RewindResponse { 'archived': boolean; 'archive_error'?: string | null; } + diff --git a/lib/packages/fabro-api-client/src/models/run-summary.ts b/lib/packages/fabro-api-client/src/models/run-summary.ts index 4ad2cb303..57eb9750d 100644 --- a/lib/packages/fabro-api-client/src/models/run-summary.ts +++ b/lib/packages/fabro-api-client/src/models/run-summary.ts @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -44,3 +44,6 @@ export interface RunSummary { 'total_usd_micros'?: number | null; 'superseded_by'?: string | null; } + + + 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..4c9c64fbc 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 @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -23,3 +23,4 @@ export interface RunSupersededByProps { 'target_node_id': string; 'target_visit': number; } + 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..0a1476d7f 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 @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -21,3 +21,6 @@ export interface ServerLoggingSettings { 'level': string | null; 'destination': LogDestination; } + + + 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 f0f233ac1..e0d90c6c9 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 @@ -5,7 +5,7 @@ * HTTP API for managing Fabro workflow run executions. * * The version of the OpenAPI document: 0.1.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -23,3 +23,4 @@ export interface TimelineEntryResponse { 'visit': number; 'run_commit_sha'?: string | null; } +