From 4893d75947990efe173b7b76583a48cd53f6e8cb Mon Sep 17 00:00:00 2001 From: Fabro Date: Sun, 24 May 2026 13:58:54 -0400 Subject: [PATCH] =?UTF-8?q?checkpoint=20=E2=9A=92=EF=B8=8F=20Generated=20w?= =?UTF-8?q?ith=20[Fabro](https://fabro.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.json | 309 +++++++- stages/004-preflight_lint@1/output.log | 1 + .../004-preflight_lint@1/script_timing.json | 8 + stages/004-preflight_lint@1/status.json | 6 + stages/005-implement@1/prompt.md | 700 ++++++++++++++++++ stages/005-implement@1/provider_used.json | 6 + stages/005-implement@1/response.md | 31 + 7 files changed, 1052 insertions(+), 9 deletions(-) create mode 100644 stages/004-preflight_lint@1/output.log create mode 100644 stages/004-preflight_lint@1/script_timing.json create mode 100644 stages/004-preflight_lint@1/status.json create mode 100644 stages/005-implement@1/prompt.md create mode 100644 stages/005-implement@1/provider_used.json create mode 100644 stages/005-implement@1/response.md diff --git a/run.json b/run.json index 8c48b27d9..70a530f0b 100644 --- a/run.json +++ b/run.json @@ -492,7 +492,7 @@ "kind": "running" }, "status_updated_at": "2026-05-24T17:26:19.008722Z", - "last_event_at": "2026-05-24T17:28:34.852443Z", + "last_event_at": "2026-05-24T17:58:54.327166Z", "pending_control": null, "checkpoints": [ { @@ -659,9 +659,9 @@ } }, { - "seq": 0, + "seq": 49, "checkpoint": { - "timestamp": "2026-05-24T17:30:55.888909Z", + "timestamp": "2026-05-24T17:30:59.320241Z", "current_node": "preflight_lint", "completed_nodes": [ "start", @@ -670,24 +670,111 @@ "preflight_lint" ], "node_retries": {}, + "context_values": { + "internal.thread_id": "preflight_compile", + "thread.preflight_compile.current_node": "preflight_lint", + "internal.run_id": "01KSDGBEV87WMKH9NQ7301WCKA", + "internal.retry_count.preflight_lint": 0, + "internal.retry_count.preflight_compile": 0, + "internal.fidelity": "compact", + "failure_class": "", + "failure_signature": "", + "internal.retry_count.start": 0, + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "graph.goal": "# Server Sandbox Provider Enablement Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Add server-owned sandbox provider enablement policy at `[server.sandbox.providers.]`, enforce it for launched runs, and make the installer write explicit provider policy entries.\n\n**Architecture:** Model sandbox provider policy as resolved server settings, separate from run environments. Missing config remains backward-compatible by resolving all providers to enabled, while explicit false values block the corresponding effective sandbox provider at server admission, preflight, and launch.\n\n**Tech Stack:** Rust, `serde`, existing `fabro-config` layer/resolve patterns, OpenAPI/progenitor, generated TypeScript API client, Axum server handlers, `cargo nextest`.\n\n---\n\n## File Structure\n\n- Modify `lib/crates/fabro-config/src/layers/server.rs`\n - Add sparse `[server.sandbox]` layer structs with closed `providers.local`, `providers.docker`, and `providers.daytona` tables.\n- Modify `lib/crates/fabro-config/src/resolve/server.rs`\n - Resolve missing sandbox provider policy to all providers enabled.\n- Modify `lib/crates/fabro-types/src/settings/server.rs`\n - Add resolved `ServerSandboxSettings`, `ServerSandboxProvidersSettings`, and `ServerSandboxProviderSettings` types under `ServerNamespace`.\n- Modify `lib/crates/fabro-install/src/lib.rs`\n - Make `write_sandbox_settings` write all three provider enablement tables with `enabled = true`.\n- Modify `lib/crates/fabro-server/src/run_manifest.rs` and `lib/crates/fabro-server/src/server/handler/runs.rs`\n - Add policy checks for run creation and preflight.\n- Modify `lib/crates/fabro-server/src/server.rs`\n - Add a launch-time recheck before sandbox setup.\n- Modify `docs/public/api-reference/fabro-api.yaml`\n - Include `server.sandbox` in the `ServerSettings` API shape.\n- Regenerate `lib/packages/fabro-api-client/src/models/*`\n - Include TypeScript client models for the new settings shape.\n- Modify docs:\n - `docs/public/administration/server-configuration.mdx`\n - `docs/public/administration/sandboxing.mdx`\n\n## Contract\n\nSupported TOML shape:\n\n```toml\n[server.sandbox.providers.local]\nenabled = true\n\n[server.sandbox.providers.docker]\nenabled = true\n\n[server.sandbox.providers.daytona]\nenabled = true\n```\n\nResolution rules:\n\n- Missing `[server.sandbox]` means all providers are enabled.\n- Missing `[server.sandbox.providers]` means all providers are enabled.\n- Missing individual provider tables mean that provider is enabled.\n- Missing individual `enabled` values mean that provider is enabled.\n- Unknown keys under `[server.sandbox]`, `[server.sandbox.providers]`, or provider tables are schema errors.\n\nPolicy rule:\n\n- The server checks the **effective** provider, after existing dry-run coercion from Docker/Daytona to Local.\n- Disabled-provider failures use this message:\n\n```text\nsandbox provider \"\" is disabled by server.sandbox.providers..enabled\n```\n\nInstaller rule:\n\n- Browser install and CLI/shared install persistence keep using the chosen sandbox provider as the default run environment.\n- Installer-generated `settings.toml` always writes all three sandbox provider entries with `enabled = true`.\n\n## Task 1: Add Server Config Types and Resolution\n\n**Files:**\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Test: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n- [ ] **Step 1: Write failing config tests**\n\nAdd tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`:\n\n```rust\n#[test]\nfn server_sandbox_defaults_all_providers_enabled() {\n let settings = super::server_settings_from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\"#,\n );\n\n let sandbox = settings.server.sandbox;\n assert!(sandbox.providers.local.enabled);\n assert!(sandbox.providers.docker.enabled);\n assert!(sandbox.providers.daytona.enabled);\n}\n\n#[test]\nfn server_sandbox_allows_partial_provider_overrides() {\n let settings = super::server_settings_from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\n[server.sandbox.providers.daytona]\nenabled = false\n\"#,\n );\n\n let sandbox = settings.server.sandbox;\n assert!(sandbox.providers.local.enabled);\n assert!(sandbox.providers.docker.enabled);\n assert!(!sandbox.providers.daytona.enabled);\n}\n\n#[test]\nfn parsing_rejects_unknown_server_sandbox_provider() {\n let err = fabro_config::ServerSettingsBuilder::from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\n[server.sandbox.providers.exe]\nenabled = true\n\"#,\n )\n .expect_err(\"unknown sandbox provider should be rejected\");\n\n assert!(\n err.to_string().contains(\"unknown field `exe`\"),\n \"unexpected error: {err}\"\n );\n}\n```\n\nRun:\n\n```bash\ncargo test -p fabro-config server_sandbox --quiet\ncargo test -p fabro-config parsing_rejects_unknown_server_sandbox_provider --quiet\n```\n\nExpected: tests fail because `server.sandbox` does not exist yet.\n\n- [ ] **Step 2: Add sparse config layer types**\n\nIn `lib/crates/fabro-config/src/layers/server.rs`, add `sandbox` to `ServerLayer`:\n\n```rust\n#[serde(default, skip_serializing_if = \"Option::is_none\")]\npub sandbox: Option,\n```\n\nAdd the layer structs near the other server subdomain structs:\n\n```rust\n/// `[server.sandbox]` — server-owned sandbox provider policy.\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n#[serde(deny_unknown_fields)]\npub struct ServerSandboxLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub providers: Option,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n#[serde(deny_unknown_fields)]\npub struct ServerSandboxProvidersLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub local: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub docker: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub daytona: Option,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n#[serde(deny_unknown_fields)]\npub struct ServerSandboxProviderLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub enabled: Option,\n}\n```\n\n- [ ] **Step 3: Add resolved server settings types**\n\nIn `lib/crates/fabro-types/src/settings/server.rs`, add `sandbox` to `ServerNamespace` after `ip_allowlist` or before `storage`:\n\n```rust\npub sandbox: ServerSandboxSettings,\n```\n\nUpdate `ServerNamespace::test_default()` to initialize it:\n\n```rust\nsandbox: ServerSandboxSettings::default(),\n```\n\nAdd resolved structs:\n\n```rust\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ServerSandboxSettings {\n pub providers: ServerSandboxProvidersSettings,\n}\n\nimpl Default for ServerSandboxSettings {\n fn default() -> Self {\n Self {\n providers: ServerSandboxProvidersSettings::default(),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ServerSandboxProvidersSettings {\n pub local: ServerSandboxProviderSettings,\n pub docker: ServerSandboxProviderSettings,\n pub daytona: ServerSandboxProviderSettings,\n}\n\nimpl Default for ServerSandboxProvidersSettings {\n fn default() -> Self {\n Self {\n local: ServerSandboxProviderSettings::default(),\n docker: ServerSandboxProviderSettings::default(),\n daytona: ServerSandboxProviderSettings::default(),\n }\n }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub struct ServerSandboxProviderSettings {\n pub enabled: bool,\n}\n\nimpl Default for ServerSandboxProviderSettings {\n fn default() -> Self {\n Self { enabled: true }\n }\n}\n```\n\n- [ ] **Step 4: Resolve the new settings**\n\nIn `lib/crates/fabro-config/src/resolve/server.rs`, import the new layer and resolved types, then add `sandbox` to `ServerNamespace` construction:\n\n```rust\nsandbox: resolve_sandbox(layer.sandbox.as_ref()),\n```\n\nAdd resolver helpers:\n\n```rust\nfn resolve_sandbox(layer: Option<&ServerSandboxLayer>) -> ServerSandboxSettings {\n let providers = layer.and_then(|sandbox| sandbox.providers.as_ref());\n ServerSandboxSettings {\n providers: ServerSandboxProvidersSettings {\n local: resolve_sandbox_provider(providers.and_then(|providers| providers.local.as_ref())),\n docker: resolve_sandbox_provider(providers.and_then(|providers| providers.docker.as_ref())),\n daytona: resolve_sandbox_provider(providers.and_then(|providers| providers.daytona.as_ref())),\n },\n }\n}\n\nfn resolve_sandbox_provider(\n layer: Option<&ServerSandboxProviderLayer>,\n) -> ServerSandboxProviderSettings {\n ServerSandboxProviderSettings {\n enabled: layer.and_then(|provider| provider.enabled).unwrap_or(true),\n }\n}\n```\n\n- [ ] **Step 5: Run config tests**\n\nRun:\n\n```bash\ncargo test -p fabro-config server_sandbox --quiet\ncargo test -p fabro-config parsing_rejects_unknown_server_sandbox_provider --quiet\n```\n\nExpected: all tests pass.\n\n## Task 2: Enforce Policy in Server Run Paths\n\n**Files:**\n- Modify: `lib/crates/fabro-server/src/run_manifest.rs`\n- Modify: `lib/crates/fabro-server/src/server/handler/runs.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Test: `lib/crates/fabro-server/src/server/tests.rs`\n- Test: `lib/crates/fabro-server/tests/it/api/runs.rs`\n\n- [ ] **Step 1: Add the shared policy helper**\n\nIn `lib/crates/fabro-server/src/run_manifest.rs`, add this helper near `resolve_sandbox_provider`:\n\n```rust\npub(crate) fn sandbox_provider_policy_error(\n server_settings: &fabro_types::ServerSettings,\n provider: SandboxProvider,\n) -> Option {\n let enabled = match provider {\n SandboxProvider::Local => server_settings.server.sandbox.providers.local.enabled,\n SandboxProvider::Docker => server_settings.server.sandbox.providers.docker.enabled,\n SandboxProvider::Daytona => server_settings.server.sandbox.providers.daytona.enabled,\n };\n\n (!enabled).then(|| {\n format!(\n \"sandbox provider \\\"{provider}\\\" is disabled by server.sandbox.providers.{provider}.enabled\"\n )\n })\n}\n\npub(crate) fn effective_sandbox_provider(settings: &RunNamespace) -> SandboxProvider {\n let provider = resolve_sandbox_provider(settings);\n if settings.execution.mode == RunMode::DryRun && !provider.is_local() {\n SandboxProvider::Local\n } else {\n provider\n }\n}\n```\n\nReplace local duplicate dry-run effective-provider logic in `build_preflight_report` with `effective_sandbox_provider(&resolved_run)`.\n\n- [ ] **Step 2: Add preflight policy failure**\n\nIn `build_preflight_report`, after `sandbox_provider` is computed and before runtime sandbox checks:\n\n```rust\nif let Some(error) = sandbox_provider_policy_error(&server_settings, sandbox_provider) {\n checks.push(CheckResult {\n name: \"Sandbox Provider Policy\".into(),\n status: CheckStatus::Error,\n summary: error,\n details: Vec::new(),\n remediation: None,\n });\n return Ok((\n CheckReport {\n title: \"Run Preflight\".into(),\n sections: vec![CheckSection {\n title: String::new(),\n checks,\n }],\n },\n false,\n ));\n}\n```\n\n- [ ] **Step 3: Reject disabled providers at run creation**\n\nIn `lib/crates/fabro-server/src/server/handler/runs.rs`, after `prepared` is created and before parent validation:\n\n```rust\nlet provider = run_manifest::effective_sandbox_provider(&prepared.settings.run);\nif let Some(error) = run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider)\n{\n return ApiError::bad_request(error).into_response();\n}\n```\n\nThis deliberately uses the resolved run settings already produced by `prepare_manifest_with_environment_defaults`; sandbox provider selection is not graph-dependent.\n\n- [ ] **Step 4: Recheck policy at launch**\n\nIn `lib/crates/fabro-server/src/server.rs`, after loading `persisted` and before resolving GitHub credentials:\n\n```rust\nlet effective_provider = run_manifest::effective_sandbox_provider(&persisted.run_spec().settings.run);\nif let Some(error) = run_manifest::sandbox_provider_policy_error(&server_settings, effective_provider)\n{\n tracing::error!(run_id = %run_id, error = %error, \"Sandbox provider disabled by server policy\");\n fail_run_before_execution(&state, run_id, FailureReason::LaunchFailed, error).await;\n return;\n}\n```\n\n- [ ] **Step 5: Test server behavior**\n\nAdd tests covering:\n\n```rust\n#[test]\nfn sandbox_provider_policy_error_reports_disabled_provider() {\n let settings = server_settings_from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\n[server.sandbox.providers.daytona]\nenabled = false\n\"#,\n );\n\n assert_eq!(\n crate::run_manifest::sandbox_provider_policy_error(\n &settings,\n fabro_sandbox::SandboxProvider::Daytona,\n )\n .as_deref(),\n Some(\n \"sandbox provider \\\"daytona\\\" is disabled by server.sandbox.providers.daytona.enabled\"\n )\n );\n}\n```\n\nAdd an API integration test in `lib/crates/fabro-server/tests/it/api/runs.rs` that creates a test app with Daytona disabled and a manifest selecting a Daytona environment. Assert `POST /api/v1/runs` returns `400` and the policy message.\n\nAdd a preflight test that sends the same manifest to `/api/v1/runs/preflight` and asserts `ok = false` plus a `Sandbox Provider Policy` error check.\n\nRun:\n\n```bash\ncargo nextest run -p fabro-server sandbox_provider_policy\ncargo nextest run -p fabro-server --test it runs::create_run_rejects_disabled_sandbox_provider\n```\n\nExpected: all new tests pass.\n\n## Task 3: Update Installer Persistence\n\n**Files:**\n- Modify: `lib/crates/fabro-install/src/lib.rs`\n- Test: `lib/crates/fabro-install/src/lib.rs`\n- Test: `lib/crates/fabro-server/tests/it/api/install.rs`\n\n- [ ] **Step 1: Add installer unit assertions**\n\nExtend `write_sandbox_settings_records_docker_provider` and `write_sandbox_settings_records_daytona_provider` to assert all three provider policies:\n\n```rust\nfn sandbox_provider_enabled(doc: &toml::Value, provider: &str) -> Option {\n doc.get(\"server\")\n .and_then(toml::Value::as_table)\n .and_then(|server| server.get(\"sandbox\"))\n .and_then(toml::Value::as_table)\n .and_then(|sandbox| sandbox.get(\"providers\"))\n .and_then(toml::Value::as_table)\n .and_then(|providers| providers.get(provider))\n .and_then(toml::Value::as_table)\n .and_then(|provider| provider.get(\"enabled\"))\n .and_then(toml::Value::as_bool)\n}\n\nassert_eq!(sandbox_provider_enabled(&doc, \"local\"), Some(true));\nassert_eq!(sandbox_provider_enabled(&doc, \"docker\"), Some(true));\nassert_eq!(sandbox_provider_enabled(&doc, \"daytona\"), Some(true));\n```\n\nRun:\n\n```bash\ncargo test -p fabro-install write_sandbox_settings_records --quiet\n```\n\nExpected: tests fail because policy entries are not written yet.\n\n- [ ] **Step 2: Write all provider policy entries**\n\nAdd helper functions in `lib/crates/fabro-install/src/lib.rs`:\n\n```rust\nfn write_sandbox_provider_enabled(\n providers: &mut toml::Table,\n provider: &str,\n enabled: bool,\n) -> Result<()> {\n let table = ensure_table(providers, provider)?;\n table.insert(\"enabled\".to_string(), toml::Value::Boolean(enabled));\n Ok(())\n}\n\nfn write_sandbox_provider_policy(server: &mut toml::Table) -> Result<()> {\n let sandbox = ensure_table(server, \"sandbox\")?;\n let providers = ensure_table(sandbox, \"providers\")?;\n write_sandbox_provider_enabled(providers, \"local\", true)?;\n write_sandbox_provider_enabled(providers, \"docker\", true)?;\n write_sandbox_provider_enabled(providers, \"daytona\", true)?;\n Ok(())\n}\n```\n\nIn `write_sandbox_settings`, after obtaining the root table and before returning:\n\n```rust\nlet server = ensure_table(root, \"server\")?;\nwrite_sandbox_provider_policy(server)?;\n```\n\n- [ ] **Step 3: Update browser install finish tests**\n\nIn `lib/crates/fabro-server/tests/it/api/install.rs`, update Docker and Daytona install finish tests to assert:\n\n```rust\nassert!(settings.contains(\"[server.sandbox.providers.local]\"));\nassert!(settings.contains(\"[server.sandbox.providers.docker]\"));\nassert!(settings.contains(\"[server.sandbox.providers.daytona]\"));\nassert!(settings.contains(\"enabled = true\"));\n```\n\nAlso parse the generated settings with `ServerSettingsBuilder::from_toml` and assert all three resolved providers are enabled.\n\n- [ ] **Step 4: Run installer tests**\n\nRun:\n\n```bash\ncargo test -p fabro-install write_sandbox_settings_records --quiet\ncargo nextest run -p fabro-server --test it install::token_install_finish_persists_settings_env_and_vault\ncargo nextest run -p fabro-server --test it install::daytona_install_finish_writes_settings_and_vault_secret\n```\n\nExpected: all tests pass.\n\n## Task 4: Update API Schema, Generated Clients, and Docs\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Regenerate: `lib/packages/fabro-api-client/src/models/*`\n- Modify: `docs/public/administration/server-configuration.mdx`\n- Modify: `docs/public/administration/sandboxing.mdx`\n\n- [ ] **Step 1: Update OpenAPI server settings schema**\n\nIn `docs/public/api-reference/fabro-api.yaml`, add `sandbox` as required on `ServerNamespace` and define:\n\n```yaml\n ServerSandboxSettings:\n type: object\n required: [providers]\n properties:\n providers:\n $ref: \"#/components/schemas/ServerSandboxProvidersSettings\"\n\n ServerSandboxProvidersSettings:\n type: object\n required: [local, docker, daytona]\n properties:\n local:\n $ref: \"#/components/schemas/ServerSandboxProviderSettings\"\n docker:\n $ref: \"#/components/schemas/ServerSandboxProviderSettings\"\n daytona:\n $ref: \"#/components/schemas/ServerSandboxProviderSettings\"\n\n ServerSandboxProviderSettings:\n type: object\n required: [enabled]\n properties:\n enabled:\n type: boolean\n```\n\n- [ ] **Step 2: Update API round-trip test**\n\nIn `lib/crates/fabro-api/tests/server_settings_round_trip.rs`, add TOML to the sample:\n\n```toml\n[server.sandbox.providers.daytona]\nenabled = false\n```\n\nAdd JSON assertions:\n\n```rust\nassert_eq!(json[\"server\"][\"sandbox\"][\"providers\"][\"local\"][\"enabled\"], true);\nassert_eq!(json[\"server\"][\"sandbox\"][\"providers\"][\"docker\"][\"enabled\"], true);\nassert_eq!(json[\"server\"][\"sandbox\"][\"providers\"][\"daytona\"][\"enabled\"], false);\n```\n\n- [ ] **Step 3: Regenerate API artifacts**\n\nRun:\n\n```bash\ncargo build -p fabro-api\ncd lib/packages/fabro-api-client && bun run generate\n```\n\nExpected: generated Rust/API and TypeScript client types include the new sandbox settings models.\n\n- [ ] **Step 4: Update docs**\n\nIn `docs/public/administration/server-configuration.mdx`, add `[server.sandbox]` to the server-owned sections table and full reference:\n\n```toml\n[server.sandbox.providers.local]\nenabled = true\n\n[server.sandbox.providers.docker]\nenabled = true\n\n[server.sandbox.providers.daytona]\nenabled = true\n```\n\nAdd a short section:\n\n```md\n### `[server.sandbox.providers]` section\n\nControls which sandbox providers the server may launch. Missing provider entries default to `enabled = true` for backward compatibility. Disabling a provider rejects new runs whose effective provider is disabled; dry-run Docker/Daytona runs use the local provider and are governed by `server.sandbox.providers.local.enabled`.\n```\n\nIn `docs/public/administration/sandboxing.mdx`, add one paragraph pointing operators to `[server.sandbox.providers.]` for enable/disable policy.\n\n- [ ] **Step 5: Run API/docs tests**\n\nRun:\n\n```bash\ncargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: tests and typecheck pass.\n\n## Task 5: Final Verification\n\n**Files:**\n- No new files.\n\n- [ ] **Step 1: Run focused Rust tests**\n\nRun:\n\n```bash\ncargo test -p fabro-config server_sandbox --quiet\ncargo test -p fabro-install write_sandbox_settings_records --quiet\ncargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet\ncargo nextest run -p fabro-server sandbox_provider_policy\n```\n\nExpected: all focused tests pass.\n\n- [ ] **Step 2: Run formatting and lint checks**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 fmt --check --all\ncargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings\n```\n\nExpected: both pass.\n\n- [ ] **Step 3: Run frontend typecheck if generated TypeScript changed**\n\nRun:\n\n```bash\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: typecheck passes.\n\n- [ ] **Step 4: Commit**\n\nRun:\n\n```bash\ngit add lib/crates/fabro-config lib/crates/fabro-types lib/crates/fabro-install lib/crates/fabro-server docs/public lib/crates/fabro-api lib/packages/fabro-api-client apps/fabro-web\ngit commit -m \"feat: add server sandbox provider policy\"\n```\n\nExpected: commit succeeds with only intended files staged.\n", + "thread.toolchain.current_node": "preflight_compile", + "internal.work_dir": "/home/daytona/workspace/fabro", + "internal.node_visit_count": 1, + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "internal.retry_count.toolchain": 0, + "outcome": "succeeded", + "thread.start.current_node": "toolchain", + "graph.rankdir": "LR", + "current_node": "preflight_lint" + }, + "node_outcomes": { + "preflight_compile": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo check -q --workspace 2>&1", + "usage": null + }, + "toolchain": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/fc14b2ba2d770e5cd3169df7a29525c962adfc4cfa3097b9098c63ebd61a748c" + }, + "notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", + "usage": null + }, + "start": { + "status": "succeeded", + "usage": null + }, + "preflight_lint": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "usage": null + } + }, + "next_node_id": "implement", + "git_commit_sha": "fd766da739d290bfb86a40ecff238505eaf1a104", + "node_visits": { + "start": 1, + "preflight_lint": 1, + "toolchain": 1, + "preflight_compile": 1 + } + }, + "diff": { + "summary": { + "files_changed": 0, + "additions": 0, + "deletions": 0 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-05-24T17:58:54.384576Z", + "current_node": "implement", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement" + ], + "node_retries": {}, "context_values": { "thread.toolchain.current_node": "preflight_compile", "failure_class": "", + "last_stage": "implement", "internal.retry_count.toolchain": 0, + "response.implement": "Implemented and committed the server sandbox provider policy.\n\nCommit: `f5eac31bc feat: add server sandbox provider policy`\n\nSummary:\n- Added resolved `[server.sandbox.providers.{local,docker,daytona}].enabled` settings with backward-compatible defaults (`true`) and closed TOML schema validation.\n- Enforced disabled-provider policy for:\n - run creation admission\n - preflight reports\n - launch-time rechecks before sandbox setup\n- Policy uses the effective provider after dry-run coercion to local.\n- Installer now writes all three provider policy tables with `enabled = true`.\n- Updated OpenAPI schema, Rust API replacements/tests, regenerated TypeScript API client models, and docs.\n\nVerification run:\n- `cargo test -p fabro-config server_sandbox --quiet`\n- `cargo test -p fabro-install write_sandbox_settings_records --quiet`\n- `cargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet`\n- `cargo nextest run -p fabro-server --features test-support sandbox_provider_policy`\n- `cargo nextest run -p fabro-server --features test-support --test it runs::create_run_rejects_disabled_sandbox_provider`\n- `cargo nextest run -p fabro-server --features test-support --test it runs::preflight_reports_disabled_sandbox_provider`\n- `cargo nextest run -p fabro-server --features test-support --test it install::token_install_finish_persists_settings_env_and_vault`\n- `cargo nextest run -p fabro-server --features test-support --test it install::daytona_install_finish_writes_settings_and_vault_secret`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n\nWorking tree is clean.", "outcome": "succeeded", "graph.rankdir": "LR", "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", - "current_node": "preflight_lint", + "last_response": "Implemented and committed the server sandbox provider policy.\n\nCommit: `f5eac31bc feat: add server sandbox provider policy`\n\nSummary:\n- Added resolved `[server.sandbox.providers.{local,docker,daytona}", + "current_node": "implement", "thread.start.current_node": "toolchain", "internal.retry_count.start": 0, "internal.work_dir": "/home/daytona/workspace/fabro", "failure_signature": "", "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", - "internal.thread_id": "preflight_compile", + "internal.thread_id": "preflight_lint", "internal.node_visit_count": 1, + "internal.retry_count.implement": 0, "internal.retry_count.preflight_compile": 0, "internal.fidelity": "compact", "thread.preflight_compile.current_node": "preflight_lint", + "thread.preflight_lint.current_node": "implement", "internal.retry_count.preflight_lint": 0, "internal.run_id": "01KSDGBEV87WMKH9NQ7301WCKA", "graph.goal": "# Server Sandbox Provider Enablement Implementation Plan\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** Add server-owned sandbox provider enablement policy at `[server.sandbox.providers.]`, enforce it for launched runs, and make the installer write explicit provider policy entries.\n\n**Architecture:** Model sandbox provider policy as resolved server settings, separate from run environments. Missing config remains backward-compatible by resolving all providers to enabled, while explicit false values block the corresponding effective sandbox provider at server admission, preflight, and launch.\n\n**Tech Stack:** Rust, `serde`, existing `fabro-config` layer/resolve patterns, OpenAPI/progenitor, generated TypeScript API client, Axum server handlers, `cargo nextest`.\n\n---\n\n## File Structure\n\n- Modify `lib/crates/fabro-config/src/layers/server.rs`\n - Add sparse `[server.sandbox]` layer structs with closed `providers.local`, `providers.docker`, and `providers.daytona` tables.\n- Modify `lib/crates/fabro-config/src/resolve/server.rs`\n - Resolve missing sandbox provider policy to all providers enabled.\n- Modify `lib/crates/fabro-types/src/settings/server.rs`\n - Add resolved `ServerSandboxSettings`, `ServerSandboxProvidersSettings`, and `ServerSandboxProviderSettings` types under `ServerNamespace`.\n- Modify `lib/crates/fabro-install/src/lib.rs`\n - Make `write_sandbox_settings` write all three provider enablement tables with `enabled = true`.\n- Modify `lib/crates/fabro-server/src/run_manifest.rs` and `lib/crates/fabro-server/src/server/handler/runs.rs`\n - Add policy checks for run creation and preflight.\n- Modify `lib/crates/fabro-server/src/server.rs`\n - Add a launch-time recheck before sandbox setup.\n- Modify `docs/public/api-reference/fabro-api.yaml`\n - Include `server.sandbox` in the `ServerSettings` API shape.\n- Regenerate `lib/packages/fabro-api-client/src/models/*`\n - Include TypeScript client models for the new settings shape.\n- Modify docs:\n - `docs/public/administration/server-configuration.mdx`\n - `docs/public/administration/sandboxing.mdx`\n\n## Contract\n\nSupported TOML shape:\n\n```toml\n[server.sandbox.providers.local]\nenabled = true\n\n[server.sandbox.providers.docker]\nenabled = true\n\n[server.sandbox.providers.daytona]\nenabled = true\n```\n\nResolution rules:\n\n- Missing `[server.sandbox]` means all providers are enabled.\n- Missing `[server.sandbox.providers]` means all providers are enabled.\n- Missing individual provider tables mean that provider is enabled.\n- Missing individual `enabled` values mean that provider is enabled.\n- Unknown keys under `[server.sandbox]`, `[server.sandbox.providers]`, or provider tables are schema errors.\n\nPolicy rule:\n\n- The server checks the **effective** provider, after existing dry-run coercion from Docker/Daytona to Local.\n- Disabled-provider failures use this message:\n\n```text\nsandbox provider \"\" is disabled by server.sandbox.providers..enabled\n```\n\nInstaller rule:\n\n- Browser install and CLI/shared install persistence keep using the chosen sandbox provider as the default run environment.\n- Installer-generated `settings.toml` always writes all three sandbox provider entries with `enabled = true`.\n\n## Task 1: Add Server Config Types and Resolution\n\n**Files:**\n- Modify: `lib/crates/fabro-config/src/layers/server.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/server.rs`\n- Modify: `lib/crates/fabro-types/src/settings/server.rs`\n- Test: `lib/crates/fabro-config/src/tests/resolve_server.rs`\n\n- [ ] **Step 1: Write failing config tests**\n\nAdd tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`:\n\n```rust\n#[test]\nfn server_sandbox_defaults_all_providers_enabled() {\n let settings = super::server_settings_from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\"#,\n );\n\n let sandbox = settings.server.sandbox;\n assert!(sandbox.providers.local.enabled);\n assert!(sandbox.providers.docker.enabled);\n assert!(sandbox.providers.daytona.enabled);\n}\n\n#[test]\nfn server_sandbox_allows_partial_provider_overrides() {\n let settings = super::server_settings_from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\n[server.sandbox.providers.daytona]\nenabled = false\n\"#,\n );\n\n let sandbox = settings.server.sandbox;\n assert!(sandbox.providers.local.enabled);\n assert!(sandbox.providers.docker.enabled);\n assert!(!sandbox.providers.daytona.enabled);\n}\n\n#[test]\nfn parsing_rejects_unknown_server_sandbox_provider() {\n let err = fabro_config::ServerSettingsBuilder::from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\n[server.sandbox.providers.exe]\nenabled = true\n\"#,\n )\n .expect_err(\"unknown sandbox provider should be rejected\");\n\n assert!(\n err.to_string().contains(\"unknown field `exe`\"),\n \"unexpected error: {err}\"\n );\n}\n```\n\nRun:\n\n```bash\ncargo test -p fabro-config server_sandbox --quiet\ncargo test -p fabro-config parsing_rejects_unknown_server_sandbox_provider --quiet\n```\n\nExpected: tests fail because `server.sandbox` does not exist yet.\n\n- [ ] **Step 2: Add sparse config layer types**\n\nIn `lib/crates/fabro-config/src/layers/server.rs`, add `sandbox` to `ServerLayer`:\n\n```rust\n#[serde(default, skip_serializing_if = \"Option::is_none\")]\npub sandbox: Option,\n```\n\nAdd the layer structs near the other server subdomain structs:\n\n```rust\n/// `[server.sandbox]` — server-owned sandbox provider policy.\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n#[serde(deny_unknown_fields)]\npub struct ServerSandboxLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub providers: Option,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n#[serde(deny_unknown_fields)]\npub struct ServerSandboxProvidersLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub local: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub docker: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub daytona: Option,\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n#[serde(deny_unknown_fields)]\npub struct ServerSandboxProviderLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub enabled: Option,\n}\n```\n\n- [ ] **Step 3: Add resolved server settings types**\n\nIn `lib/crates/fabro-types/src/settings/server.rs`, add `sandbox` to `ServerNamespace` after `ip_allowlist` or before `storage`:\n\n```rust\npub sandbox: ServerSandboxSettings,\n```\n\nUpdate `ServerNamespace::test_default()` to initialize it:\n\n```rust\nsandbox: ServerSandboxSettings::default(),\n```\n\nAdd resolved structs:\n\n```rust\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ServerSandboxSettings {\n pub providers: ServerSandboxProvidersSettings,\n}\n\nimpl Default for ServerSandboxSettings {\n fn default() -> Self {\n Self {\n providers: ServerSandboxProvidersSettings::default(),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ServerSandboxProvidersSettings {\n pub local: ServerSandboxProviderSettings,\n pub docker: ServerSandboxProviderSettings,\n pub daytona: ServerSandboxProviderSettings,\n}\n\nimpl Default for ServerSandboxProvidersSettings {\n fn default() -> Self {\n Self {\n local: ServerSandboxProviderSettings::default(),\n docker: ServerSandboxProviderSettings::default(),\n daytona: ServerSandboxProviderSettings::default(),\n }\n }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub struct ServerSandboxProviderSettings {\n pub enabled: bool,\n}\n\nimpl Default for ServerSandboxProviderSettings {\n fn default() -> Self {\n Self { enabled: true }\n }\n}\n```\n\n- [ ] **Step 4: Resolve the new settings**\n\nIn `lib/crates/fabro-config/src/resolve/server.rs`, import the new layer and resolved types, then add `sandbox` to `ServerNamespace` construction:\n\n```rust\nsandbox: resolve_sandbox(layer.sandbox.as_ref()),\n```\n\nAdd resolver helpers:\n\n```rust\nfn resolve_sandbox(layer: Option<&ServerSandboxLayer>) -> ServerSandboxSettings {\n let providers = layer.and_then(|sandbox| sandbox.providers.as_ref());\n ServerSandboxSettings {\n providers: ServerSandboxProvidersSettings {\n local: resolve_sandbox_provider(providers.and_then(|providers| providers.local.as_ref())),\n docker: resolve_sandbox_provider(providers.and_then(|providers| providers.docker.as_ref())),\n daytona: resolve_sandbox_provider(providers.and_then(|providers| providers.daytona.as_ref())),\n },\n }\n}\n\nfn resolve_sandbox_provider(\n layer: Option<&ServerSandboxProviderLayer>,\n) -> ServerSandboxProviderSettings {\n ServerSandboxProviderSettings {\n enabled: layer.and_then(|provider| provider.enabled).unwrap_or(true),\n }\n}\n```\n\n- [ ] **Step 5: Run config tests**\n\nRun:\n\n```bash\ncargo test -p fabro-config server_sandbox --quiet\ncargo test -p fabro-config parsing_rejects_unknown_server_sandbox_provider --quiet\n```\n\nExpected: all tests pass.\n\n## Task 2: Enforce Policy in Server Run Paths\n\n**Files:**\n- Modify: `lib/crates/fabro-server/src/run_manifest.rs`\n- Modify: `lib/crates/fabro-server/src/server/handler/runs.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Test: `lib/crates/fabro-server/src/server/tests.rs`\n- Test: `lib/crates/fabro-server/tests/it/api/runs.rs`\n\n- [ ] **Step 1: Add the shared policy helper**\n\nIn `lib/crates/fabro-server/src/run_manifest.rs`, add this helper near `resolve_sandbox_provider`:\n\n```rust\npub(crate) fn sandbox_provider_policy_error(\n server_settings: &fabro_types::ServerSettings,\n provider: SandboxProvider,\n) -> Option {\n let enabled = match provider {\n SandboxProvider::Local => server_settings.server.sandbox.providers.local.enabled,\n SandboxProvider::Docker => server_settings.server.sandbox.providers.docker.enabled,\n SandboxProvider::Daytona => server_settings.server.sandbox.providers.daytona.enabled,\n };\n\n (!enabled).then(|| {\n format!(\n \"sandbox provider \\\"{provider}\\\" is disabled by server.sandbox.providers.{provider}.enabled\"\n )\n })\n}\n\npub(crate) fn effective_sandbox_provider(settings: &RunNamespace) -> SandboxProvider {\n let provider = resolve_sandbox_provider(settings);\n if settings.execution.mode == RunMode::DryRun && !provider.is_local() {\n SandboxProvider::Local\n } else {\n provider\n }\n}\n```\n\nReplace local duplicate dry-run effective-provider logic in `build_preflight_report` with `effective_sandbox_provider(&resolved_run)`.\n\n- [ ] **Step 2: Add preflight policy failure**\n\nIn `build_preflight_report`, after `sandbox_provider` is computed and before runtime sandbox checks:\n\n```rust\nif let Some(error) = sandbox_provider_policy_error(&server_settings, sandbox_provider) {\n checks.push(CheckResult {\n name: \"Sandbox Provider Policy\".into(),\n status: CheckStatus::Error,\n summary: error,\n details: Vec::new(),\n remediation: None,\n });\n return Ok((\n CheckReport {\n title: \"Run Preflight\".into(),\n sections: vec![CheckSection {\n title: String::new(),\n checks,\n }],\n },\n false,\n ));\n}\n```\n\n- [ ] **Step 3: Reject disabled providers at run creation**\n\nIn `lib/crates/fabro-server/src/server/handler/runs.rs`, after `prepared` is created and before parent validation:\n\n```rust\nlet provider = run_manifest::effective_sandbox_provider(&prepared.settings.run);\nif let Some(error) = run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider)\n{\n return ApiError::bad_request(error).into_response();\n}\n```\n\nThis deliberately uses the resolved run settings already produced by `prepare_manifest_with_environment_defaults`; sandbox provider selection is not graph-dependent.\n\n- [ ] **Step 4: Recheck policy at launch**\n\nIn `lib/crates/fabro-server/src/server.rs`, after loading `persisted` and before resolving GitHub credentials:\n\n```rust\nlet effective_provider = run_manifest::effective_sandbox_provider(&persisted.run_spec().settings.run);\nif let Some(error) = run_manifest::sandbox_provider_policy_error(&server_settings, effective_provider)\n{\n tracing::error!(run_id = %run_id, error = %error, \"Sandbox provider disabled by server policy\");\n fail_run_before_execution(&state, run_id, FailureReason::LaunchFailed, error).await;\n return;\n}\n```\n\n- [ ] **Step 5: Test server behavior**\n\nAdd tests covering:\n\n```rust\n#[test]\nfn sandbox_provider_policy_error_reports_disabled_provider() {\n let settings = server_settings_from_toml(\n r#\"\n_version = 1\n\n[server.auth]\nmethods = [\"dev-token\"]\n\n[server.sandbox.providers.daytona]\nenabled = false\n\"#,\n );\n\n assert_eq!(\n crate::run_manifest::sandbox_provider_policy_error(\n &settings,\n fabro_sandbox::SandboxProvider::Daytona,\n )\n .as_deref(),\n Some(\n \"sandbox provider \\\"daytona\\\" is disabled by server.sandbox.providers.daytona.enabled\"\n )\n );\n}\n```\n\nAdd an API integration test in `lib/crates/fabro-server/tests/it/api/runs.rs` that creates a test app with Daytona disabled and a manifest selecting a Daytona environment. Assert `POST /api/v1/runs` returns `400` and the policy message.\n\nAdd a preflight test that sends the same manifest to `/api/v1/runs/preflight` and asserts `ok = false` plus a `Sandbox Provider Policy` error check.\n\nRun:\n\n```bash\ncargo nextest run -p fabro-server sandbox_provider_policy\ncargo nextest run -p fabro-server --test it runs::create_run_rejects_disabled_sandbox_provider\n```\n\nExpected: all new tests pass.\n\n## Task 3: Update Installer Persistence\n\n**Files:**\n- Modify: `lib/crates/fabro-install/src/lib.rs`\n- Test: `lib/crates/fabro-install/src/lib.rs`\n- Test: `lib/crates/fabro-server/tests/it/api/install.rs`\n\n- [ ] **Step 1: Add installer unit assertions**\n\nExtend `write_sandbox_settings_records_docker_provider` and `write_sandbox_settings_records_daytona_provider` to assert all three provider policies:\n\n```rust\nfn sandbox_provider_enabled(doc: &toml::Value, provider: &str) -> Option {\n doc.get(\"server\")\n .and_then(toml::Value::as_table)\n .and_then(|server| server.get(\"sandbox\"))\n .and_then(toml::Value::as_table)\n .and_then(|sandbox| sandbox.get(\"providers\"))\n .and_then(toml::Value::as_table)\n .and_then(|providers| providers.get(provider))\n .and_then(toml::Value::as_table)\n .and_then(|provider| provider.get(\"enabled\"))\n .and_then(toml::Value::as_bool)\n}\n\nassert_eq!(sandbox_provider_enabled(&doc, \"local\"), Some(true));\nassert_eq!(sandbox_provider_enabled(&doc, \"docker\"), Some(true));\nassert_eq!(sandbox_provider_enabled(&doc, \"daytona\"), Some(true));\n```\n\nRun:\n\n```bash\ncargo test -p fabro-install write_sandbox_settings_records --quiet\n```\n\nExpected: tests fail because policy entries are not written yet.\n\n- [ ] **Step 2: Write all provider policy entries**\n\nAdd helper functions in `lib/crates/fabro-install/src/lib.rs`:\n\n```rust\nfn write_sandbox_provider_enabled(\n providers: &mut toml::Table,\n provider: &str,\n enabled: bool,\n) -> Result<()> {\n let table = ensure_table(providers, provider)?;\n table.insert(\"enabled\".to_string(), toml::Value::Boolean(enabled));\n Ok(())\n}\n\nfn write_sandbox_provider_policy(server: &mut toml::Table) -> Result<()> {\n let sandbox = ensure_table(server, \"sandbox\")?;\n let providers = ensure_table(sandbox, \"providers\")?;\n write_sandbox_provider_enabled(providers, \"local\", true)?;\n write_sandbox_provider_enabled(providers, \"docker\", true)?;\n write_sandbox_provider_enabled(providers, \"daytona\", true)?;\n Ok(())\n}\n```\n\nIn `write_sandbox_settings`, after obtaining the root table and before returning:\n\n```rust\nlet server = ensure_table(root, \"server\")?;\nwrite_sandbox_provider_policy(server)?;\n```\n\n- [ ] **Step 3: Update browser install finish tests**\n\nIn `lib/crates/fabro-server/tests/it/api/install.rs`, update Docker and Daytona install finish tests to assert:\n\n```rust\nassert!(settings.contains(\"[server.sandbox.providers.local]\"));\nassert!(settings.contains(\"[server.sandbox.providers.docker]\"));\nassert!(settings.contains(\"[server.sandbox.providers.daytona]\"));\nassert!(settings.contains(\"enabled = true\"));\n```\n\nAlso parse the generated settings with `ServerSettingsBuilder::from_toml` and assert all three resolved providers are enabled.\n\n- [ ] **Step 4: Run installer tests**\n\nRun:\n\n```bash\ncargo test -p fabro-install write_sandbox_settings_records --quiet\ncargo nextest run -p fabro-server --test it install::token_install_finish_persists_settings_env_and_vault\ncargo nextest run -p fabro-server --test it install::daytona_install_finish_writes_settings_and_vault_secret\n```\n\nExpected: all tests pass.\n\n## Task 4: Update API Schema, Generated Clients, and Docs\n\n**Files:**\n- Modify: `docs/public/api-reference/fabro-api.yaml`\n- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs`\n- Regenerate: `lib/packages/fabro-api-client/src/models/*`\n- Modify: `docs/public/administration/server-configuration.mdx`\n- Modify: `docs/public/administration/sandboxing.mdx`\n\n- [ ] **Step 1: Update OpenAPI server settings schema**\n\nIn `docs/public/api-reference/fabro-api.yaml`, add `sandbox` as required on `ServerNamespace` and define:\n\n```yaml\n ServerSandboxSettings:\n type: object\n required: [providers]\n properties:\n providers:\n $ref: \"#/components/schemas/ServerSandboxProvidersSettings\"\n\n ServerSandboxProvidersSettings:\n type: object\n required: [local, docker, daytona]\n properties:\n local:\n $ref: \"#/components/schemas/ServerSandboxProviderSettings\"\n docker:\n $ref: \"#/components/schemas/ServerSandboxProviderSettings\"\n daytona:\n $ref: \"#/components/schemas/ServerSandboxProviderSettings\"\n\n ServerSandboxProviderSettings:\n type: object\n required: [enabled]\n properties:\n enabled:\n type: boolean\n```\n\n- [ ] **Step 2: Update API round-trip test**\n\nIn `lib/crates/fabro-api/tests/server_settings_round_trip.rs`, add TOML to the sample:\n\n```toml\n[server.sandbox.providers.daytona]\nenabled = false\n```\n\nAdd JSON assertions:\n\n```rust\nassert_eq!(json[\"server\"][\"sandbox\"][\"providers\"][\"local\"][\"enabled\"], true);\nassert_eq!(json[\"server\"][\"sandbox\"][\"providers\"][\"docker\"][\"enabled\"], true);\nassert_eq!(json[\"server\"][\"sandbox\"][\"providers\"][\"daytona\"][\"enabled\"], false);\n```\n\n- [ ] **Step 3: Regenerate API artifacts**\n\nRun:\n\n```bash\ncargo build -p fabro-api\ncd lib/packages/fabro-api-client && bun run generate\n```\n\nExpected: generated Rust/API and TypeScript client types include the new sandbox settings models.\n\n- [ ] **Step 4: Update docs**\n\nIn `docs/public/administration/server-configuration.mdx`, add `[server.sandbox]` to the server-owned sections table and full reference:\n\n```toml\n[server.sandbox.providers.local]\nenabled = true\n\n[server.sandbox.providers.docker]\nenabled = true\n\n[server.sandbox.providers.daytona]\nenabled = true\n```\n\nAdd a short section:\n\n```md\n### `[server.sandbox.providers]` section\n\nControls which sandbox providers the server may launch. Missing provider entries default to `enabled = true` for backward compatibility. Disabling a provider rejects new runs whose effective provider is disabled; dry-run Docker/Daytona runs use the local provider and are governed by `server.sandbox.providers.local.enabled`.\n```\n\nIn `docs/public/administration/sandboxing.mdx`, add one paragraph pointing operators to `[server.sandbox.providers.]` for enable/disable policy.\n\n- [ ] **Step 5: Run API/docs tests**\n\nRun:\n\n```bash\ncargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: tests and typecheck pass.\n\n## Task 5: Final Verification\n\n**Files:**\n- No new files.\n\n- [ ] **Step 1: Run focused Rust tests**\n\nRun:\n\n```bash\ncargo test -p fabro-config server_sandbox --quiet\ncargo test -p fabro-install write_sandbox_settings_records --quiet\ncargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet\ncargo nextest run -p fabro-server sandbox_provider_policy\n```\n\nExpected: all focused tests pass.\n\n- [ ] **Step 2: Run formatting and lint checks**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 fmt --check --all\ncargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings\n```\n\nExpected: both pass.\n\n- [ ] **Step 3: Run frontend typecheck if generated TypeScript changed**\n\nRun:\n\n```bash\ncd apps/fabro-web && bun run typecheck\n```\n\nExpected: typecheck passes.\n\n- [ ] **Step 4: Commit**\n\nRun:\n\n```bash\ngit add lib/crates/fabro-config lib/crates/fabro-types lib/crates/fabro-install lib/crates/fabro-server docs/public lib/crates/fabro-api lib/packages/fabro-api-client apps/fabro-web\ngit commit -m \"feat: add server sandbox provider policy\"\n```\n\nExpected: commit succeeds with only intended files staged.\n" @@ -709,6 +796,41 @@ "notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", "usage": null }, + "implement": { + "status": "succeeded", + "context_updates": { + "last_response": "Implemented and committed the server sandbox provider policy.\n\nCommit: `f5eac31bc feat: add server sandbox provider policy`\n\nSummary:\n- Added resolved `[server.sandbox.providers.{local,docker,daytona}", + "response.implement": "Implemented and committed the server sandbox provider policy.\n\nCommit: `f5eac31bc feat: add server sandbox provider policy`\n\nSummary:\n- Added resolved `[server.sandbox.providers.{local,docker,daytona}].enabled` settings with backward-compatible defaults (`true`) and closed TOML schema validation.\n- Enforced disabled-provider policy for:\n - run creation admission\n - preflight reports\n - launch-time rechecks before sandbox setup\n- Policy uses the effective provider after dry-run coercion to local.\n- Installer now writes all three provider policy tables with `enabled = true`.\n- Updated OpenAPI schema, Rust API replacements/tests, regenerated TypeScript API client models, and docs.\n\nVerification run:\n- `cargo test -p fabro-config server_sandbox --quiet`\n- `cargo test -p fabro-install write_sandbox_settings_records --quiet`\n- `cargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet`\n- `cargo nextest run -p fabro-server --features test-support sandbox_provider_policy`\n- `cargo nextest run -p fabro-server --features test-support --test it runs::create_run_rejects_disabled_sandbox_provider`\n- `cargo nextest run -p fabro-server --features test-support --test it runs::preflight_reports_disabled_sandbox_provider`\n- `cargo nextest run -p fabro-server --features test-support --test it install::token_install_finish_persists_settings_env_and_vault`\n- `cargo nextest run -p fabro-server --features test-support --test it install::daytona_install_finish_writes_settings_and_vault_secret`\n- `cargo build -p fabro-api`\n- `cd lib/packages/fabro-api-client && bun run generate`\n- `cargo +nightly-2026-04-14 fmt --check --all`\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings`\n- `cd lib/packages/fabro-api-client && bun run typecheck`\n- `cd apps/fabro-web && bun run typecheck`\n\nWorking tree is clean.", + "last_stage": "implement" + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "tokens": { + "input_tokens": 5701485, + "output_tokens": 22374, + "reasoning_tokens": 9843, + "cache_read_tokens": 18817536, + "cache_write_tokens": 0 + } + }, + "facts": { + "algorithm": "openai" + } + }, + "total_usd_micros": 38882703 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts", + "/home/daytona/workspace/fabro/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts", + "/home/daytona/workspace/fabro/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts" + ] + }, "start": { "status": "succeeded", "usage": null @@ -722,11 +844,12 @@ "usage": null } }, - "next_node_id": "implement", + "next_node_id": "simplify_opus", "node_visits": { "start": 1, "preflight_lint": 1, "preflight_compile": 1, + "implement": 1, "toolchain": 1 } }, @@ -849,6 +972,153 @@ }, "state": "succeeded" }, + "implement@1": { + "first_event_seq": 52, + "prompt": null, + "response": null, + "completion": null, + "provider_used": { + "mode": "agent", + "provider": "openai", + "model": "gpt-5.5", + "reasoning_effort": "xhigh" + }, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "started_at": "2026-05-24T17:30:59.321665Z", + "handler": "agent", + "usage": { + "input_tokens": 5701485, + "output_tokens": 22374, + "total_tokens": 24551238, + "reasoning_tokens": 9843, + "cache_read_tokens": 18817536, + "cache_write_tokens": 0, + "total_usd_micros": 38882703 + }, + "model": { + "provider": "openai", + "model_id": "gpt-5.5" + }, + "todos": { + "kind": "openai_plan", + "list_id": "openai_plan:962789cd-f13c-400e-a785-9863a3466708", + "items": [ + { + "id": "070db5a18513a19b", + "status": "completed", + "order": 0, + "subject": "Inspect config/server/API/installer code and add failing tests for sandbox provider policy" + }, + { + "id": "50d06ad0b1270850", + "status": "completed", + "order": 1, + "subject": "Implement config layer, resolved settings, and config resolution defaults" + }, + { + "id": "8ffc07aaff9c6245", + "status": "completed", + "order": 2, + "subject": "Implement server admission, preflight, and launch policy enforcement with tests" + }, + { + "id": "00a6c566fbade0a1", + "status": "completed", + "order": 3, + "subject": "Update installer persistence and installer/browser install tests" + }, + { + "id": "9f9d32c881ae6e06", + "status": "completed", + "order": 4, + "subject": "Update OpenAPI schema, regenerated clients, and docs" + }, + { + "id": "e83c35f8a6d44030", + "status": "completed", + "order": 5, + "subject": "Run focused tests, formatting/lint/typecheck, and finalize changes" + } + ] + }, + "subagents": [ + { + "agent_id": "0d8bc6be", + "depth": 1, + "task": "Subagent-driven development support task: inspect fabro-server run creation/preflight tests and helpers relevant to adding sandbox provider policy. Do not modify files. Report exact test helper names, likely insertion points, and any status/body assertion conventions.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 9 + } + }, + { + "agent_id": "4bef4738", + "depth": 1, + "task": "Subagent-driven development support task: inspect OpenAPI ServerSettings schema, fabro-api round-trip test, and TypeScript generated client conventions relevant to adding server.sandbox settings. Do not modify files. Report exact schema insertion points and generation expectations.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 9 + } + }, + { + "agent_id": "d784fa3a", + "depth": 1, + "task": "Subagent-driven development support task: inspect fabro-install write_sandbox_settings tests and browser install integration tests. Do not modify files. Report exact functions/tests to update and helper patterns.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 9 + } + } + ], + "permission_level": "full", + "context_window": { + "provider": "openai", + "model": "gpt-5.5", + "context_window_tokens": 1050000, + "input_tokens": 220828, + "usage_percent": 21.031238095238095, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-05-24T17:58:54.323302Z", + "event_seq": 1379, + "breakdown": [ + { + "category": "system_prompt", + "tokens": 939, + "usage_percent": 0.08942857142857143 + }, + { + "category": "tools", + "tokens": 1345, + "usage_percent": 0.1280952380952381 + }, + { + "category": "memory", + "tokens": 3147, + "usage_percent": 0.2997142857142857 + }, + { + "category": "conversation", + "tokens": 215390, + "usage_percent": 20.513333333333332 + }, + { + "category": "other", + "tokens": 7, + "usage_percent": 0.0006666666666666666 + } + ], + "warnings": [] + }, + "state": "running" + }, "start@1": { "first_event_seq": 18, "prompt": null, @@ -887,7 +1157,12 @@ "first_event_seq": 42, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "failure_reason": null, + "timestamp": "2026-05-24T17:30:55.888246Z" + }, "provider_used": null, "diff": null, "script_invocation": { @@ -895,11 +1170,27 @@ "command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", "language": "shell" }, - "script_timing": null, + "script_timing": { + "output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "exit_code": 0, + "duration_ms": 141029, + "termination": "exited", + "output_bytes": 0, + "live_streaming": false + }, "parallel_results": null, "output": null, + "output_bytes": 0, + "live_streaming": false, + "termination": "exited", "started_at": "2026-05-24T17:28:34.852156Z", "handler": "command", + "timing": { + "wall_time_ms": 141034, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 0, "output_tokens": 0, @@ -908,7 +1199,7 @@ "cache_read_tokens": 0, "cache_write_tokens": 0 }, - "state": "running" + "state": "succeeded" } } } \ No newline at end of file diff --git a/stages/004-preflight_lint@1/output.log b/stages/004-preflight_lint@1/output.log new file mode 100644 index 000000000..d87ba9545 --- /dev/null +++ b/stages/004-preflight_lint@1/output.log @@ -0,0 +1 @@ +blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126 \ No newline at end of file diff --git a/stages/004-preflight_lint@1/script_timing.json b/stages/004-preflight_lint@1/script_timing.json new file mode 100644 index 000000000..566d0f9e5 --- /dev/null +++ b/stages/004-preflight_lint@1/script_timing.json @@ -0,0 +1,8 @@ +{ + "output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "exit_code": 0, + "duration_ms": 141029, + "termination": "exited", + "output_bytes": 0, + "live_streaming": false +} \ No newline at end of file diff --git a/stages/004-preflight_lint@1/status.json b/stages/004-preflight_lint@1/status.json new file mode 100644 index 000000000..5d3a1f6ba --- /dev/null +++ b/stages/004-preflight_lint@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "failure_reason": null, + "timestamp": "2026-05-24T17:30:55.888246Z" +} \ No newline at end of file diff --git a/stages/005-implement@1/prompt.md b/stages/005-implement@1/prompt.md new file mode 100644 index 000000000..bd2a29590 --- /dev/null +++ b/stages/005-implement@1/prompt.md @@ -0,0 +1,700 @@ +Goal: # Server Sandbox Provider Enablement Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add server-owned sandbox provider enablement policy at `[server.sandbox.providers.]`, enforce it for launched runs, and make the installer write explicit provider policy entries. + +**Architecture:** Model sandbox provider policy as resolved server settings, separate from run environments. Missing config remains backward-compatible by resolving all providers to enabled, while explicit false values block the corresponding effective sandbox provider at server admission, preflight, and launch. + +**Tech Stack:** Rust, `serde`, existing `fabro-config` layer/resolve patterns, OpenAPI/progenitor, generated TypeScript API client, Axum server handlers, `cargo nextest`. + +--- + +## File Structure + +- Modify `lib/crates/fabro-config/src/layers/server.rs` + - Add sparse `[server.sandbox]` layer structs with closed `providers.local`, `providers.docker`, and `providers.daytona` tables. +- Modify `lib/crates/fabro-config/src/resolve/server.rs` + - Resolve missing sandbox provider policy to all providers enabled. +- Modify `lib/crates/fabro-types/src/settings/server.rs` + - Add resolved `ServerSandboxSettings`, `ServerSandboxProvidersSettings`, and `ServerSandboxProviderSettings` types under `ServerNamespace`. +- Modify `lib/crates/fabro-install/src/lib.rs` + - Make `write_sandbox_settings` write all three provider enablement tables with `enabled = true`. +- Modify `lib/crates/fabro-server/src/run_manifest.rs` and `lib/crates/fabro-server/src/server/handler/runs.rs` + - Add policy checks for run creation and preflight. +- Modify `lib/crates/fabro-server/src/server.rs` + - Add a launch-time recheck before sandbox setup. +- Modify `docs/public/api-reference/fabro-api.yaml` + - Include `server.sandbox` in the `ServerSettings` API shape. +- Regenerate `lib/packages/fabro-api-client/src/models/*` + - Include TypeScript client models for the new settings shape. +- Modify docs: + - `docs/public/administration/server-configuration.mdx` + - `docs/public/administration/sandboxing.mdx` + +## Contract + +Supported TOML shape: + +```toml +[server.sandbox.providers.local] +enabled = true + +[server.sandbox.providers.docker] +enabled = true + +[server.sandbox.providers.daytona] +enabled = true +``` + +Resolution rules: + +- Missing `[server.sandbox]` means all providers are enabled. +- Missing `[server.sandbox.providers]` means all providers are enabled. +- Missing individual provider tables mean that provider is enabled. +- Missing individual `enabled` values mean that provider is enabled. +- Unknown keys under `[server.sandbox]`, `[server.sandbox.providers]`, or provider tables are schema errors. + +Policy rule: + +- The server checks the **effective** provider, after existing dry-run coercion from Docker/Daytona to Local. +- Disabled-provider failures use this message: + +```text +sandbox provider "" is disabled by server.sandbox.providers..enabled +``` + +Installer rule: + +- Browser install and CLI/shared install persistence keep using the chosen sandbox provider as the default run environment. +- Installer-generated `settings.toml` always writes all three sandbox provider entries with `enabled = true`. + +## Task 1: Add Server Config Types and Resolution + +**Files:** +- Modify: `lib/crates/fabro-config/src/layers/server.rs` +- Modify: `lib/crates/fabro-config/src/resolve/server.rs` +- Modify: `lib/crates/fabro-types/src/settings/server.rs` +- Test: `lib/crates/fabro-config/src/tests/resolve_server.rs` + +- [ ] **Step 1: Write failing config tests** + +Add tests in `lib/crates/fabro-config/src/tests/resolve_server.rs`: + +```rust +#[test] +fn server_sandbox_defaults_all_providers_enabled() { + let settings = super::server_settings_from_toml( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] +"#, + ); + + let sandbox = settings.server.sandbox; + assert!(sandbox.providers.local.enabled); + assert!(sandbox.providers.docker.enabled); + assert!(sandbox.providers.daytona.enabled); +} + +#[test] +fn server_sandbox_allows_partial_provider_overrides() { + let settings = super::server_settings_from_toml( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.sandbox.providers.daytona] +enabled = false +"#, + ); + + let sandbox = settings.server.sandbox; + assert!(sandbox.providers.local.enabled); + assert!(sandbox.providers.docker.enabled); + assert!(!sandbox.providers.daytona.enabled); +} + +#[test] +fn parsing_rejects_unknown_server_sandbox_provider() { + let err = fabro_config::ServerSettingsBuilder::from_toml( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.sandbox.providers.exe] +enabled = true +"#, + ) + .expect_err("unknown sandbox provider should be rejected"); + + assert!( + err.to_string().contains("unknown field `exe`"), + "unexpected error: {err}" + ); +} +``` + +Run: + +```bash +cargo test -p fabro-config server_sandbox --quiet +cargo test -p fabro-config parsing_rejects_unknown_server_sandbox_provider --quiet +``` + +Expected: tests fail because `server.sandbox` does not exist yet. + +- [ ] **Step 2: Add sparse config layer types** + +In `lib/crates/fabro-config/src/layers/server.rs`, add `sandbox` to `ServerLayer`: + +```rust +#[serde(default, skip_serializing_if = "Option::is_none")] +pub sandbox: Option, +``` + +Add the layer structs near the other server subdomain structs: + +```rust +/// `[server.sandbox]` — server-owned sandbox provider policy. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] +#[serde(deny_unknown_fields)] +pub struct ServerSandboxLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub providers: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] +#[serde(deny_unknown_fields)] +pub struct ServerSandboxProvidersLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub local: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub docker: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub daytona: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] +#[serde(deny_unknown_fields)] +pub struct ServerSandboxProviderLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, +} +``` + +- [ ] **Step 3: Add resolved server settings types** + +In `lib/crates/fabro-types/src/settings/server.rs`, add `sandbox` to `ServerNamespace` after `ip_allowlist` or before `storage`: + +```rust +pub sandbox: ServerSandboxSettings, +``` + +Update `ServerNamespace::test_default()` to initialize it: + +```rust +sandbox: ServerSandboxSettings::default(), +``` + +Add resolved structs: + +```rust +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerSandboxSettings { + pub providers: ServerSandboxProvidersSettings, +} + +impl Default for ServerSandboxSettings { + fn default() -> Self { + Self { + providers: ServerSandboxProvidersSettings::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerSandboxProvidersSettings { + pub local: ServerSandboxProviderSettings, + pub docker: ServerSandboxProviderSettings, + pub daytona: ServerSandboxProviderSettings, +} + +impl Default for ServerSandboxProvidersSettings { + fn default() -> Self { + Self { + local: ServerSandboxProviderSettings::default(), + docker: ServerSandboxProviderSettings::default(), + daytona: ServerSandboxProviderSettings::default(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerSandboxProviderSettings { + pub enabled: bool, +} + +impl Default for ServerSandboxProviderSettings { + fn default() -> Self { + Self { enabled: true } + } +} +``` + +- [ ] **Step 4: Resolve the new settings** + +In `lib/crates/fabro-config/src/resolve/server.rs`, import the new layer and resolved types, then add `sandbox` to `ServerNamespace` construction: + +```rust +sandbox: resolve_sandbox(layer.sandbox.as_ref()), +``` + +Add resolver helpers: + +```rust +fn resolve_sandbox(layer: Option<&ServerSandboxLayer>) -> ServerSandboxSettings { + let providers = layer.and_then(|sandbox| sandbox.providers.as_ref()); + ServerSandboxSettings { + providers: ServerSandboxProvidersSettings { + local: resolve_sandbox_provider(providers.and_then(|providers| providers.local.as_ref())), + docker: resolve_sandbox_provider(providers.and_then(|providers| providers.docker.as_ref())), + daytona: resolve_sandbox_provider(providers.and_then(|providers| providers.daytona.as_ref())), + }, + } +} + +fn resolve_sandbox_provider( + layer: Option<&ServerSandboxProviderLayer>, +) -> ServerSandboxProviderSettings { + ServerSandboxProviderSettings { + enabled: layer.and_then(|provider| provider.enabled).unwrap_or(true), + } +} +``` + +- [ ] **Step 5: Run config tests** + +Run: + +```bash +cargo test -p fabro-config server_sandbox --quiet +cargo test -p fabro-config parsing_rejects_unknown_server_sandbox_provider --quiet +``` + +Expected: all tests pass. + +## Task 2: Enforce Policy in Server Run Paths + +**Files:** +- Modify: `lib/crates/fabro-server/src/run_manifest.rs` +- Modify: `lib/crates/fabro-server/src/server/handler/runs.rs` +- Modify: `lib/crates/fabro-server/src/server.rs` +- Test: `lib/crates/fabro-server/src/server/tests.rs` +- Test: `lib/crates/fabro-server/tests/it/api/runs.rs` + +- [ ] **Step 1: Add the shared policy helper** + +In `lib/crates/fabro-server/src/run_manifest.rs`, add this helper near `resolve_sandbox_provider`: + +```rust +pub(crate) fn sandbox_provider_policy_error( + server_settings: &fabro_types::ServerSettings, + provider: SandboxProvider, +) -> Option { + let enabled = match provider { + SandboxProvider::Local => server_settings.server.sandbox.providers.local.enabled, + SandboxProvider::Docker => server_settings.server.sandbox.providers.docker.enabled, + SandboxProvider::Daytona => server_settings.server.sandbox.providers.daytona.enabled, + }; + + (!enabled).then(|| { + format!( + "sandbox provider \"{provider}\" is disabled by server.sandbox.providers.{provider}.enabled" + ) + }) +} + +pub(crate) fn effective_sandbox_provider(settings: &RunNamespace) -> SandboxProvider { + let provider = resolve_sandbox_provider(settings); + if settings.execution.mode == RunMode::DryRun && !provider.is_local() { + SandboxProvider::Local + } else { + provider + } +} +``` + +Replace local duplicate dry-run effective-provider logic in `build_preflight_report` with `effective_sandbox_provider(&resolved_run)`. + +- [ ] **Step 2: Add preflight policy failure** + +In `build_preflight_report`, after `sandbox_provider` is computed and before runtime sandbox checks: + +```rust +if let Some(error) = sandbox_provider_policy_error(&server_settings, sandbox_provider) { + checks.push(CheckResult { + name: "Sandbox Provider Policy".into(), + status: CheckStatus::Error, + summary: error, + details: Vec::new(), + remediation: None, + }); + return Ok(( + CheckReport { + title: "Run Preflight".into(), + sections: vec![CheckSection { + title: String::new(), + checks, + }], + }, + false, + )); +} +``` + +- [ ] **Step 3: Reject disabled providers at run creation** + +In `lib/crates/fabro-server/src/server/handler/runs.rs`, after `prepared` is created and before parent validation: + +```rust +let provider = run_manifest::effective_sandbox_provider(&prepared.settings.run); +if let Some(error) = run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider) +{ + return ApiError::bad_request(error).into_response(); +} +``` + +This deliberately uses the resolved run settings already produced by `prepare_manifest_with_environment_defaults`; sandbox provider selection is not graph-dependent. + +- [ ] **Step 4: Recheck policy at launch** + +In `lib/crates/fabro-server/src/server.rs`, after loading `persisted` and before resolving GitHub credentials: + +```rust +let effective_provider = run_manifest::effective_sandbox_provider(&persisted.run_spec().settings.run); +if let Some(error) = run_manifest::sandbox_provider_policy_error(&server_settings, effective_provider) +{ + tracing::error!(run_id = %run_id, error = %error, "Sandbox provider disabled by server policy"); + fail_run_before_execution(&state, run_id, FailureReason::LaunchFailed, error).await; + return; +} +``` + +- [ ] **Step 5: Test server behavior** + +Add tests covering: + +```rust +#[test] +fn sandbox_provider_policy_error_reports_disabled_provider() { + let settings = server_settings_from_toml( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.sandbox.providers.daytona] +enabled = false +"#, + ); + + assert_eq!( + crate::run_manifest::sandbox_provider_policy_error( + &settings, + fabro_sandbox::SandboxProvider::Daytona, + ) + .as_deref(), + Some( + "sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled" + ) + ); +} +``` + +Add an API integration test in `lib/crates/fabro-server/tests/it/api/runs.rs` that creates a test app with Daytona disabled and a manifest selecting a Daytona environment. Assert `POST /api/v1/runs` returns `400` and the policy message. + +Add a preflight test that sends the same manifest to `/api/v1/runs/preflight` and asserts `ok = false` plus a `Sandbox Provider Policy` error check. + +Run: + +```bash +cargo nextest run -p fabro-server sandbox_provider_policy +cargo nextest run -p fabro-server --test it runs::create_run_rejects_disabled_sandbox_provider +``` + +Expected: all new tests pass. + +## Task 3: Update Installer Persistence + +**Files:** +- Modify: `lib/crates/fabro-install/src/lib.rs` +- Test: `lib/crates/fabro-install/src/lib.rs` +- Test: `lib/crates/fabro-server/tests/it/api/install.rs` + +- [ ] **Step 1: Add installer unit assertions** + +Extend `write_sandbox_settings_records_docker_provider` and `write_sandbox_settings_records_daytona_provider` to assert all three provider policies: + +```rust +fn sandbox_provider_enabled(doc: &toml::Value, provider: &str) -> Option { + doc.get("server") + .and_then(toml::Value::as_table) + .and_then(|server| server.get("sandbox")) + .and_then(toml::Value::as_table) + .and_then(|sandbox| sandbox.get("providers")) + .and_then(toml::Value::as_table) + .and_then(|providers| providers.get(provider)) + .and_then(toml::Value::as_table) + .and_then(|provider| provider.get("enabled")) + .and_then(toml::Value::as_bool) +} + +assert_eq!(sandbox_provider_enabled(&doc, "local"), Some(true)); +assert_eq!(sandbox_provider_enabled(&doc, "docker"), Some(true)); +assert_eq!(sandbox_provider_enabled(&doc, "daytona"), Some(true)); +``` + +Run: + +```bash +cargo test -p fabro-install write_sandbox_settings_records --quiet +``` + +Expected: tests fail because policy entries are not written yet. + +- [ ] **Step 2: Write all provider policy entries** + +Add helper functions in `lib/crates/fabro-install/src/lib.rs`: + +```rust +fn write_sandbox_provider_enabled( + providers: &mut toml::Table, + provider: &str, + enabled: bool, +) -> Result<()> { + let table = ensure_table(providers, provider)?; + table.insert("enabled".to_string(), toml::Value::Boolean(enabled)); + Ok(()) +} + +fn write_sandbox_provider_policy(server: &mut toml::Table) -> Result<()> { + let sandbox = ensure_table(server, "sandbox")?; + let providers = ensure_table(sandbox, "providers")?; + write_sandbox_provider_enabled(providers, "local", true)?; + write_sandbox_provider_enabled(providers, "docker", true)?; + write_sandbox_provider_enabled(providers, "daytona", true)?; + Ok(()) +} +``` + +In `write_sandbox_settings`, after obtaining the root table and before returning: + +```rust +let server = ensure_table(root, "server")?; +write_sandbox_provider_policy(server)?; +``` + +- [ ] **Step 3: Update browser install finish tests** + +In `lib/crates/fabro-server/tests/it/api/install.rs`, update Docker and Daytona install finish tests to assert: + +```rust +assert!(settings.contains("[server.sandbox.providers.local]")); +assert!(settings.contains("[server.sandbox.providers.docker]")); +assert!(settings.contains("[server.sandbox.providers.daytona]")); +assert!(settings.contains("enabled = true")); +``` + +Also parse the generated settings with `ServerSettingsBuilder::from_toml` and assert all three resolved providers are enabled. + +- [ ] **Step 4: Run installer tests** + +Run: + +```bash +cargo test -p fabro-install write_sandbox_settings_records --quiet +cargo nextest run -p fabro-server --test it install::token_install_finish_persists_settings_env_and_vault +cargo nextest run -p fabro-server --test it install::daytona_install_finish_writes_settings_and_vault_secret +``` + +Expected: all tests pass. + +## Task 4: Update API Schema, Generated Clients, and Docs + +**Files:** +- Modify: `docs/public/api-reference/fabro-api.yaml` +- Modify: `lib/crates/fabro-api/tests/server_settings_round_trip.rs` +- Regenerate: `lib/packages/fabro-api-client/src/models/*` +- Modify: `docs/public/administration/server-configuration.mdx` +- Modify: `docs/public/administration/sandboxing.mdx` + +- [ ] **Step 1: Update OpenAPI server settings schema** + +In `docs/public/api-reference/fabro-api.yaml`, add `sandbox` as required on `ServerNamespace` and define: + +```yaml + ServerSandboxSettings: + type: object + required: [providers] + properties: + providers: + $ref: "#/components/schemas/ServerSandboxProvidersSettings" + + ServerSandboxProvidersSettings: + type: object + required: [local, docker, daytona] + properties: + local: + $ref: "#/components/schemas/ServerSandboxProviderSettings" + docker: + $ref: "#/components/schemas/ServerSandboxProviderSettings" + daytona: + $ref: "#/components/schemas/ServerSandboxProviderSettings" + + ServerSandboxProviderSettings: + type: object + required: [enabled] + properties: + enabled: + type: boolean +``` + +- [ ] **Step 2: Update API round-trip test** + +In `lib/crates/fabro-api/tests/server_settings_round_trip.rs`, add TOML to the sample: + +```toml +[server.sandbox.providers.daytona] +enabled = false +``` + +Add JSON assertions: + +```rust +assert_eq!(json["server"]["sandbox"]["providers"]["local"]["enabled"], true); +assert_eq!(json["server"]["sandbox"]["providers"]["docker"]["enabled"], true); +assert_eq!(json["server"]["sandbox"]["providers"]["daytona"]["enabled"], false); +``` + +- [ ] **Step 3: Regenerate API artifacts** + +Run: + +```bash +cargo build -p fabro-api +cd lib/packages/fabro-api-client && bun run generate +``` + +Expected: generated Rust/API and TypeScript client types include the new sandbox settings models. + +- [ ] **Step 4: Update docs** + +In `docs/public/administration/server-configuration.mdx`, add `[server.sandbox]` to the server-owned sections table and full reference: + +```toml +[server.sandbox.providers.local] +enabled = true + +[server.sandbox.providers.docker] +enabled = true + +[server.sandbox.providers.daytona] +enabled = true +``` + +Add a short section: + +```md +### `[server.sandbox.providers]` section + +Controls which sandbox providers the server may launch. Missing provider entries default to `enabled = true` for backward compatibility. Disabling a provider rejects new runs whose effective provider is disabled; dry-run Docker/Daytona runs use the local provider and are governed by `server.sandbox.providers.local.enabled`. +``` + +In `docs/public/administration/sandboxing.mdx`, add one paragraph pointing operators to `[server.sandbox.providers.]` for enable/disable policy. + +- [ ] **Step 5: Run API/docs tests** + +Run: + +```bash +cargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet +cd apps/fabro-web && bun run typecheck +``` + +Expected: tests and typecheck pass. + +## Task 5: Final Verification + +**Files:** +- No new files. + +- [ ] **Step 1: Run focused Rust tests** + +Run: + +```bash +cargo test -p fabro-config server_sandbox --quiet +cargo test -p fabro-install write_sandbox_settings_records --quiet +cargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet +cargo nextest run -p fabro-server sandbox_provider_policy +``` + +Expected: all focused tests pass. + +- [ ] **Step 2: Run formatting and lint checks** + +Run: + +```bash +cargo +nightly-2026-04-14 fmt --check --all +cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings +``` + +Expected: both pass. + +- [ ] **Step 3: Run frontend typecheck if generated TypeScript changed** + +Run: + +```bash +cd apps/fabro-web && bun run typecheck +``` + +Expected: typecheck passes. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add lib/crates/fabro-config lib/crates/fabro-types lib/crates/fabro-install lib/crates/fabro-server docs/public lib/crates/fabro-api lib/packages/fabro-api-client apps/fabro-web +git commit -m "feat: add server sandbox provider policy" +``` + +Expected: commit succeeds with only intended files staged. + + +## Completed stages +- **toolchain**: succeeded + - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1` + - Output: + ``` + cargo 1.95.0 (f2d3ce0bd 2026-03-21) + ``` +- **preflight_compile**: succeeded + - Script: `cargo check -q --workspace 2>&1` + - Output: (empty) +- **preflight_lint**: succeeded + - Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1` + - Output: (empty) + + +Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD. \ No newline at end of file diff --git a/stages/005-implement@1/provider_used.json b/stages/005-implement@1/provider_used.json new file mode 100644 index 000000000..c57772db6 --- /dev/null +++ b/stages/005-implement@1/provider_used.json @@ -0,0 +1,6 @@ +{ + "mode": "agent", + "provider": "openai", + "model": "gpt-5.5", + "reasoning_effort": "xhigh" +} \ No newline at end of file diff --git a/stages/005-implement@1/response.md b/stages/005-implement@1/response.md new file mode 100644 index 000000000..bd5202e66 --- /dev/null +++ b/stages/005-implement@1/response.md @@ -0,0 +1,31 @@ +Implemented and committed the server sandbox provider policy. + +Commit: `f5eac31bc feat: add server sandbox provider policy` + +Summary: +- Added resolved `[server.sandbox.providers.{local,docker,daytona}].enabled` settings with backward-compatible defaults (`true`) and closed TOML schema validation. +- Enforced disabled-provider policy for: + - run creation admission + - preflight reports + - launch-time rechecks before sandbox setup +- Policy uses the effective provider after dry-run coercion to local. +- Installer now writes all three provider policy tables with `enabled = true`. +- Updated OpenAPI schema, Rust API replacements/tests, regenerated TypeScript API client models, and docs. + +Verification run: +- `cargo test -p fabro-config server_sandbox --quiet` +- `cargo test -p fabro-install write_sandbox_settings_records --quiet` +- `cargo test -p fabro-api server_settings_json_matches_openapi_shape --quiet` +- `cargo nextest run -p fabro-server --features test-support sandbox_provider_policy` +- `cargo nextest run -p fabro-server --features test-support --test it runs::create_run_rejects_disabled_sandbox_provider` +- `cargo nextest run -p fabro-server --features test-support --test it runs::preflight_reports_disabled_sandbox_provider` +- `cargo nextest run -p fabro-server --features test-support --test it install::token_install_finish_persists_settings_env_and_vault` +- `cargo nextest run -p fabro-server --features test-support --test it install::daytona_install_finish_writes_settings_and_vault_secret` +- `cargo build -p fabro-api` +- `cd lib/packages/fabro-api-client && bun run generate` +- `cargo +nightly-2026-04-14 fmt --check --all` +- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` +- `cd lib/packages/fabro-api-client && bun run typecheck` +- `cd apps/fabro-web && bun run typecheck` + +Working tree is clean. \ No newline at end of file