diff --git a/run.json b/run.json index 70a530f0b..18e548acb 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:58:54.327166Z", + "last_event_at": "2026-05-24T18:13:18.965611Z", "pending_control": null, "checkpoints": [ { @@ -740,9 +740,9 @@ } }, { - "seq": 0, + "seq": 1388, "checkpoint": { - "timestamp": "2026-05-24T17:58:54.384576Z", + "timestamp": "2026-05-24T17:58:58.189793Z", "current_node": "implement", "completed_nodes": [ "start", @@ -753,31 +753,159 @@ ], "node_retries": {}, "context_values": { - "thread.toolchain.current_node": "preflight_compile", - "failure_class": "", - "last_stage": "implement", + "internal.thread_id": "preflight_lint", "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.", + "current_node": "implement", + "internal.run_id": "01KSDGBEV87WMKH9NQ7301WCKA", + "internal.retry_count.implement": 0, + "failure_class": "", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "failure_signature": "", + "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", + "internal.retry_count.preflight_lint": 0, + "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}", + "thread.preflight_compile.current_node": "preflight_lint", + "thread.preflight_lint.current_node": "implement", + "thread.toolchain.current_node": "preflight_compile", "outcome": "succeeded", + "internal.work_dir": "/home/daytona/workspace/fabro", + "internal.retry_count.start": 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.", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "graph.rankdir": "LR", + "internal.retry_count.preflight_compile": 0, + "last_stage": "implement", + "thread.start.current_node": "toolchain", + "internal.fidelity": "compact", + "internal.node_visit_count": 1 + }, + "node_outcomes": { + "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" + ] + }, + "preflight_compile": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo check -q --workspace 2>&1", + "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 + }, + "start": { + "status": "succeeded", + "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 + } + }, + "next_node_id": "simplify_opus", + "git_commit_sha": "6b3fcd3e6265a9401ccd4326686adb7eba8dd410", + "node_visits": { + "implement": 1, + "toolchain": 1, + "preflight_compile": 1, + "start": 1, + "preflight_lint": 1 + } + }, + "diff": { + "patch": "diff --git a/docs/public/administration/sandboxing.mdx b/docs/public/administration/sandboxing.mdx\nindex 749eedeed..e74dad1cc 100644\n--- a/docs/public/administration/sandboxing.mdx\n+++ b/docs/public/administration/sandboxing.mdx\n@@ -7,6 +7,11 @@ Sandboxes isolate agent execution from the host machine. When an agent runs a sh\n \n Fabro supports three sandbox providers: `local` (no isolation), `docker` (container-level), and `daytona` (cloud VM). See [Environments](/execution/environments) for full provider-specific configuration.\n \n+Operators can enable or disable which providers the server may launch with\n+`[server.sandbox.providers.]` in `settings.toml`. Missing entries default to\n+`enabled = true`; setting `enabled = false` rejects new runs whose effective provider is disabled.\n+Dry-run Docker/Daytona runs execute locally, so they are governed by the `local` provider policy.\n+\n ## Network access control\n \n For cloud sandboxes (Daytona), you can control outbound network access with `[environments..network]`. Three modes are available: `\"allow_all\"` (default), `\"block\"`, and `\"cidr_allow_list\"` with an `allow = [\"...\"]` CIDR list.\ndiff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx\nindex 3822174ad..a6ad03475 100644\n--- a/docs/public/administration/server-configuration.mdx\n+++ b/docs/public/administration/server-configuration.mdx\n@@ -17,7 +17,7 @@ Fabro only reads `settings.toml`. Older `server.toml`, `user.toml`, and `cli.tom\n \n | Scope | Examples |\n |---|---|\n-| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |\n+| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.sandbox]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |\n | Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.environment]`, `[environments.]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |\n \n The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.\n@@ -46,6 +46,15 @@ methods = [\"dev-token\", \"github\"]\n [server.auth.github]\n allowed_usernames = [\"alice\", \"bob\"]\n \n+[server.sandbox.providers.local]\n+enabled = true\n+\n+[server.sandbox.providers.docker]\n+enabled = true\n+\n+[server.sandbox.providers.daytona]\n+enabled = true\n+\n [server.integrations.github]\n app_id = \"123456\"\n client_id = \"Iv1.abc123\"\n@@ -165,6 +174,24 @@ GitHub-specific auth policy.\n \n The GitHub OAuth client ID still lives under `[server.integrations.github].client_id`.\n \n+### `[server.sandbox.providers]` section\n+\n+Controls which sandbox providers the server may launch. Missing provider entries default to\n+`enabled = true` for backward compatibility. Disabling a provider rejects new runs whose effective\n+provider is disabled; dry-run Docker/Daytona runs use the local provider and are governed by\n+`server.sandbox.providers.local.enabled`.\n+\n+```toml title=\"settings.toml\"\n+[server.sandbox.providers.local]\n+enabled = true\n+\n+[server.sandbox.providers.docker]\n+enabled = true\n+\n+[server.sandbox.providers.daytona]\n+enabled = true\n+```\n+\n ### `[server.slatedb]` section\n \n Configure the embedded SlateDB key-value store used for run event storage.\ndiff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml\nindex 58a1e02e9..3ec1be7f8 100644\n--- a/docs/public/api-reference/fabro-api.yaml\n+++ b/docs/public/api-reference/fabro-api.yaml\n@@ -11192,6 +11192,7 @@ components:\n - web\n - auth\n - ip_allowlist\n+ - sandbox\n - storage\n - artifacts\n - slatedb\n@@ -11209,6 +11210,8 @@ components:\n $ref: \"#/components/schemas/ServerAuthSettings\"\n ip_allowlist:\n $ref: \"#/components/schemas/ServerIpAllowlistSettings\"\n+ sandbox:\n+ $ref: \"#/components/schemas/ServerSandboxSettings\"\n storage:\n $ref: \"#/components/schemas/ServerStorageSettings\"\n artifacts:\n@@ -11325,6 +11328,31 @@ components:\n type: string\n enum: [GitHubMetaHooks]\n \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 ServerStorageSettings:\n type: object\n required: [root]\ndiff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs\nindex 4649ef19e..86e848f38 100644\n--- a/lib/crates/fabro-api/build.rs\n+++ b/lib/crates/fabro-api/build.rs\n@@ -259,6 +259,21 @@ fn main() {\n \"fabro_types::settings::server::IpAllowEntry\",\n &[],\n ),\n+ (\n+ \"ServerSandboxSettings\",\n+ \"fabro_types::settings::server::ServerSandboxSettings\",\n+ &[],\n+ ),\n+ (\n+ \"ServerSandboxProvidersSettings\",\n+ \"fabro_types::settings::server::ServerSandboxProvidersSettings\",\n+ &[],\n+ ),\n+ (\n+ \"ServerSandboxProviderSettings\",\n+ \"fabro_types::settings::server::ServerSandboxProviderSettings\",\n+ &[],\n+ ),\n (\n \"ServerStorageSettings\",\n \"fabro_types::settings::server::ServerStorageSettings\",\ndiff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs\nindex a148d9aac..b8f3a02a1 100644\n--- a/lib/crates/fabro-api/src/lib.rs\n+++ b/lib/crates/fabro-api/src/lib.rs\n@@ -25,7 +25,8 @@ pub mod types {\n IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings,\n ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,\n ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,\n- ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings,\n+ ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings,\n+ ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,\n ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,\n WebhookStrategy,\n };\ndiff --git a/lib/crates/fabro-api/tests/server_settings_round_trip.rs b/lib/crates/fabro-api/tests/server_settings_round_trip.rs\nindex 45d8ef97e..a7421641b 100644\n--- a/lib/crates/fabro-api/tests/server_settings_round_trip.rs\n+++ b/lib/crates/fabro-api/tests/server_settings_round_trip.rs\n@@ -2,12 +2,18 @@ use std::any::{TypeId, type_name};\n \n use fabro_api::types::{\n LogDestination as ApiLogDestination, ObjectStoreSettings as ApiObjectStoreSettings,\n- ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings,\n+ ServerNamespace as ApiServerNamespace,\n+ ServerSandboxProviderSettings as ApiServerSandboxProviderSettings,\n+ ServerSandboxProvidersSettings as ApiServerSandboxProvidersSettings,\n+ ServerSandboxSettings as ApiServerSandboxSettings, ServerSettings as ApiServerSettings,\n };\n use fabro_config::ServerSettingsBuilder;\n use fabro_types::ServerSettings;\n use fabro_types::settings::ServerNamespace;\n-use fabro_types::settings::server::{LogDestination, ObjectStoreSettings};\n+use fabro_types::settings::server::{\n+ LogDestination, ObjectStoreSettings, ServerSandboxProviderSettings,\n+ ServerSandboxProvidersSettings, ServerSandboxSettings,\n+};\n \n #[test]\n fn server_settings_family_reuses_domain_types() {\n@@ -15,6 +21,9 @@ fn server_settings_family_reuses_domain_types() {\n assert_same_type::();\n assert_same_type::();\n assert_same_type::();\n+ assert_same_type::();\n+ assert_same_type::();\n+ assert_same_type::();\n }\n \n #[test]\n@@ -40,6 +49,9 @@ methods = [\"dev-token\", \"github\"]\n [server.auth.github]\n allowed_usernames = [\"alice\"]\n \n+[server.sandbox.providers.daytona]\n+enabled = false\n+\n [server.storage]\n root = \"/srv/fabro\"\n \n@@ -61,6 +73,18 @@ slug = \"fabro-dev\"\n assert_eq!(json[\"server\"][\"listen\"][\"address\"], \"127.0.0.1:32276\");\n assert_eq!(json[\"server\"][\"storage\"][\"root\"], \"/srv/fabro\");\n assert_eq!(json[\"server\"][\"logging\"][\"destination\"], \"stdout\");\n+ assert_eq!(\n+ json[\"server\"][\"sandbox\"][\"providers\"][\"local\"][\"enabled\"],\n+ true\n+ );\n+ assert_eq!(\n+ json[\"server\"][\"sandbox\"][\"providers\"][\"docker\"][\"enabled\"],\n+ true\n+ );\n+ assert_eq!(\n+ json[\"server\"][\"sandbox\"][\"providers\"][\"daytona\"][\"enabled\"],\n+ false\n+ );\n assert!(json.get(\"features\").is_none());\n \n let round_trip: ApiServerSettings =\ndiff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs\nindex bf5f0a38b..6c425c33d 100644\n--- a/lib/crates/fabro-config/src/layers/mod.rs\n+++ b/lib/crates/fabro-config/src/layers/mod.rs\n@@ -42,8 +42,9 @@ pub use server::{\n GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,\n ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,\n ServerIntegrationsLayer, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer,\n- ServerListenLayer, ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer,\n- ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer,\n+ ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer,\n+ ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer,\n+ ServerWebLayer, SlackIntegrationLayer,\n };\n pub use settings::SettingsLayer;\n pub use workflow::WorkflowLayer;\ndiff --git a/lib/crates/fabro-config/src/layers/server.rs b/lib/crates/fabro-config/src/layers/server.rs\nindex bc03a1824..62939a0e3 100644\n--- a/lib/crates/fabro-config/src/layers/server.rs\n+++ b/lib/crates/fabro-config/src/layers/server.rs\n@@ -23,6 +23,8 @@ pub struct ServerLayer {\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub ip_allowlist: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n+ pub sandbox: Option,\n+ #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub storage: Option,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub artifacts: Option,\n@@ -109,6 +111,32 @@ pub struct ServerIpAllowlistOverrideLayer {\n pub trusted_proxy_count: Option,\n }\n \n+/// `[server.sandbox]` — server-owned sandbox provider policy.\n+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n+#[serde(deny_unknown_fields)]\n+pub 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)]\n+pub 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)]\n+pub struct ServerSandboxProviderLayer {\n+ #[serde(default, skip_serializing_if = \"Option::is_none\")]\n+ pub enabled: Option,\n+}\n+\n /// `[server.storage]` — single managed local disk root.\n #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]\n #[serde(deny_unknown_fields)]\ndiff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs\nindex 7ae698007..0fb321caa 100644\n--- a/lib/crates/fabro-config/src/lib.rs\n+++ b/lib/crates/fabro-config/src/lib.rs\n@@ -57,6 +57,7 @@ pub use layers::{\n RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,\n ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,\n ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,\n+ ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,\n ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer,\n SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,\n };\ndiff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs\nindex f6c22c699..cfdf08c7a 100644\n--- a/lib/crates/fabro-config/src/resolve/server.rs\n+++ b/lib/crates/fabro-config/src/resolve/server.rs\n@@ -4,7 +4,8 @@ use fabro_types::settings::server::{\n IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings,\n ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,\n ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings,\n- ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings,\n+ ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSandboxProviderSettings,\n+ ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,\n ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,\n WebhookStrategy,\n };\n@@ -15,8 +16,8 @@ use crate::user::default_storage_dir;\n use crate::{\n IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer,\n ServerArtifactsLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,\n- ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerSlateDbLayer,\n- ServerStorageLayer, ServerWebLayer,\n+ ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerSandboxLayer,\n+ ServerSandboxProviderLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,\n };\n \n pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> ServerNamespace {\n@@ -38,6 +39,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se\n web,\n auth,\n ip_allowlist,\n+ sandbox: resolve_sandbox(layer.sandbox.as_ref()),\n storage: storage.clone(),\n artifacts: resolve_artifacts(layer.artifacts.as_ref(), &storage.root, errors),\n slatedb: resolve_slatedb(layer.slatedb.as_ref(), &storage.root, errors),\n@@ -64,6 +66,31 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se\n }\n }\n \n+fn 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(\n+ providers.and_then(|providers| providers.local.as_ref()),\n+ ),\n+ docker: resolve_sandbox_provider(\n+ providers.and_then(|providers| providers.docker.as_ref()),\n+ ),\n+ daytona: resolve_sandbox_provider(\n+ providers.and_then(|providers| providers.daytona.as_ref()),\n+ ),\n+ },\n+ }\n+}\n+\n+fn 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 fn resolve_storage(layer: Option<&ServerStorageLayer>) -> ServerStorageSettings {\n ServerStorageSettings {\n root: layer\ndiff --git a/lib/crates/fabro-config/src/tests/resolve_server.rs b/lib/crates/fabro-config/src/tests/resolve_server.rs\nindex d3c7899d4..edd419619 100644\n--- a/lib/crates/fabro-config/src/tests/resolve_server.rs\n+++ b/lib/crates/fabro-config/src/tests/resolve_server.rs\n@@ -135,6 +135,66 @@ fn resolved_server_integrations_are_slack_only_for_chat() {\n );\n }\n \n+#[test]\n+fn server_sandbox_defaults_all_providers_enabled() {\n+ let settings = ServerSettingsBuilder::from_toml(\n+ r#\"\n+_version = 1\n+\n+[server.auth]\n+methods = [\"dev-token\"]\n+\"#,\n+ )\n+ .expect(\"server settings should resolve\");\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]\n+fn server_sandbox_allows_partial_provider_overrides() {\n+ let settings = ServerSettingsBuilder::from_toml(\n+ r#\"\n+_version = 1\n+\n+[server.auth]\n+methods = [\"dev-token\"]\n+\n+[server.sandbox.providers.daytona]\n+enabled = false\n+\"#,\n+ )\n+ .expect(\"server settings should resolve\");\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]\n+fn parsing_rejects_unknown_server_sandbox_provider() {\n+ let err = ServerSettingsBuilder::from_toml(\n+ r#\"\n+_version = 1\n+\n+[server.auth]\n+methods = [\"dev-token\"]\n+\n+[server.sandbox.providers.exe]\n+enabled = 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 #[test]\n fn parsing_rejects_unknown_server_integrations() {\n let source = r\"\ndiff --git a/lib/crates/fabro-install/src/lib.rs b/lib/crates/fabro-install/src/lib.rs\nindex f62de5414..2176b7fb2 100644\n--- a/lib/crates/fabro-install/src/lib.rs\n+++ b/lib/crates/fabro-install/src/lib.rs\n@@ -442,6 +442,25 @@ pub fn write_object_store_settings(\n }\n }\n \n+fn 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+\n+fn 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 pub fn write_sandbox_settings(\n doc: &mut toml::Value,\n selection: InstallSandboxSelection,\n@@ -461,6 +480,8 @@ pub fn write_sandbox_settings(\n \"provider\".to_string(),\n toml::Value::String(provider.to_string()),\n );\n+ let server = ensure_table(root, \"server\")?;\n+ write_sandbox_provider_policy(server)?;\n Ok(())\n }\n \n@@ -1407,6 +1428,9 @@ stale = \"remove-me\"\n .and_then(toml::Value::as_str),\n Some(\"docker\")\n );\n+ assert_eq!(sandbox_provider_enabled(&doc, \"local\"), Some(true));\n+ assert_eq!(sandbox_provider_enabled(&doc, \"docker\"), Some(true));\n+ assert_eq!(sandbox_provider_enabled(&doc, \"daytona\"), Some(true));\n }\n \n #[test]\n@@ -1433,6 +1457,22 @@ stale = \"remove-me\"\n .and_then(toml::Value::as_str),\n Some(\"daytona\")\n );\n+ assert_eq!(sandbox_provider_enabled(&doc, \"local\"), Some(true));\n+ assert_eq!(sandbox_provider_enabled(&doc, \"docker\"), Some(true));\n+ assert_eq!(sandbox_provider_enabled(&doc, \"daytona\"), Some(true));\n+ }\n+\n+ fn 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 \n #[test]\ndiff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs\nindex ecc61e589..e7f9884a6 100644\n--- a/lib/crates/fabro-server/src/run_manifest.rs\n+++ b/lib/crates/fabro-server/src/run_manifest.rs\n@@ -26,7 +26,7 @@ use fabro_static::EnvVars;\n use fabro_types::settings::cli::OutputVerbosity;\n use fabro_types::settings::interp::InterpString;\n use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunMode, RunNamespace};\n-use fabro_types::{ManifestPath, RunId, WorkflowSettings};\n+use fabro_types::{ManifestPath, RunId, ServerSettings, WorkflowSettings};\n use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};\n use fabro_validate::Severity;\n use fabro_workflow::Error as WorkflowError;\n@@ -502,13 +502,26 @@ async fn build_preflight_report(\n let resolved_run = materialized.run;\n let server_settings = state.server_settings();\n let github_integration = &server_settings.server.integrations.github;\n- let sandbox_provider = resolve_sandbox_provider(&resolved_run);\n- let sandbox_provider =\n- if resolved_run.execution.mode == RunMode::DryRun && !sandbox_provider.is_local() {\n- SandboxProvider::Local\n- } else {\n- sandbox_provider\n- };\n+ let sandbox_provider = effective_sandbox_provider(&resolved_run);\n+ if 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 run_environment_capability_check(&mut checks, &resolved_run);\n let needs_github_credentials =\n sandbox_provider.is_clone_based() || resolved_run.integrations.github.is_token_requested();\n@@ -621,6 +634,32 @@ fn resolve_sandbox_provider(settings: &RunNamespace) -> SandboxProvider {\n SandboxProvider::from(settings.environment.provider)\n }\n \n+pub(crate) fn sandbox_provider_policy_error(\n+ server_settings: &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+\n+pub(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 fn resolve_daytona_config(settings: &RunNamespace) -> DaytonaConfig {\n daytona_config_from_environment(&settings.environment, !settings.clone.enabled)\n }\ndiff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs\nindex 3b6069115..d2f1813f9 100644\n--- a/lib/crates/fabro-server/src/server.rs\n+++ b/lib/crates/fabro-server/src/server.rs\n@@ -3580,6 +3580,15 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) {\n finish_cancelled_run_before_execution(&state, run_id).await;\n return;\n }\n+ let effective_provider =\n+ run_manifest::effective_sandbox_provider(&persisted.run_spec().settings.run);\n+ if let Some(error) =\n+ 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 let github_app_result = {\n let run_spec = persisted.run_spec();\n let settings = &run_spec.settings.run;\n@@ -3804,6 +3813,15 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) {\n }\n };\n let agent_fabro_tools_enabled = run_state.spec.settings.run.agent.fabro_tools;\n+ let server_settings = state.server_settings();\n+ let effective_provider = run_manifest::effective_sandbox_provider(&run_state.spec.settings.run);\n+ if let Some(error) =\n+ 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 let state_for_build = Arc::clone(&state);\n let run_dir_for_build = run_dir.clone();\ndiff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs\nindex cfc8dba88..a313d3fda 100644\n--- a/lib/crates/fabro-server/src/server/handler/runs.rs\n+++ b/lib/crates/fabro-server/src/server/handler/runs.rs\n@@ -606,6 +606,12 @@ async fn create_run(\n Err(err) => return ApiError::bad_request(err.to_string()).into_response(),\n };\n let run_id = prepared.run_id.unwrap_or_else(RunId::new);\n+ let provider = run_manifest::effective_sandbox_provider(&prepared.settings.run);\n+ if let Some(error) =\n+ run_manifest::sandbox_provider_policy_error(&state.server_settings(), provider)\n+ {\n+ return ApiError::bad_request(error).into_response();\n+ }\n if let Some(parent_id) = prepared.parent_id {\n if parent_id == run_id {\n return ApiError::bad_request(\"A run cannot be its own parent.\").into_response();\ndiff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs\nindex 6488ab364..fe8bce65c 100644\n--- a/lib/crates/fabro-server/src/server/tests.rs\n+++ b/lib/crates/fabro-server/src/server/tests.rs\n@@ -915,6 +915,29 @@ id = \"missing\"\n );\n }\n \n+#[test]\n+fn sandbox_provider_policy_error_reports_disabled_provider() {\n+ let settings = server_settings_from_toml(\n+ r#\"\n+_version = 1\n+\n+[server.auth]\n+methods = [\"dev-token\"]\n+\n+[server.sandbox.providers.daytona]\n+enabled = false\n+\"#,\n+ );\n+\n+ assert_eq!(\n+ crate::run_manifest::sandbox_provider_policy_error(&settings, SandboxProvider::Daytona)\n+ .as_deref(),\n+ Some(\n+ \"sandbox provider \\\"daytona\\\" is disabled by server.sandbox.providers.daytona.enabled\"\n+ )\n+ );\n+}\n+\n #[test]\n fn clone_sandbox_credentials_are_available_for_clone_based_providers() {\n use fabro_types::settings::run::EnvironmentProvider;\ndiff --git a/lib/crates/fabro-server/tests/it/api/install.rs b/lib/crates/fabro-server/tests/it/api/install.rs\nindex 8b9bea24e..d5c431fde 100644\n--- a/lib/crates/fabro-server/tests/it/api/install.rs\n+++ b/lib/crates/fabro-server/tests/it/api/install.rs\n@@ -34,6 +34,22 @@ fn spa_fixture_root() -> PathBuf {\n PathBuf::from(env!(\"CARGO_MANIFEST_DIR\")).join(\"tests/fixtures/spa\")\n }\n \n+fn assert_sandbox_provider_policy_enabled(settings: &str) {\n+ assert!(settings.contains(\"[server.sandbox.providers.local]\"));\n+ assert!(settings.contains(\"[server.sandbox.providers.docker]\"));\n+ assert!(settings.contains(\"[server.sandbox.providers.daytona]\"));\n+ assert!(settings.contains(\"enabled = true\"));\n+\n+ let resolved = ServerSettingsBuilder::from_toml(settings)\n+ .expect(\"settings should resolve\")\n+ .server\n+ .sandbox\n+ .providers;\n+ assert!(resolved.local.enabled);\n+ assert!(resolved.docker.enabled);\n+ assert!(resolved.daytona.enabled);\n+}\n+\n async fn mock_daytona_auth_probe(server: &MockServer) -> httpmock::Mock<'_> {\n server\n .mock_async(|when, then| {\n@@ -918,6 +934,7 @@ async fn token_install_finish_persists_settings_env_and_vault() {\n settings.contains(\"provider = \\\"docker\\\"\"),\n \"settings.toml should record explicit docker sandbox provider\"\n );\n+ assert_sandbox_provider_policy_enabled(&settings);\n let resolved = ServerSettingsBuilder::from_toml(&settings)\n .expect(\"settings should resolve\")\n .server;\n@@ -2677,6 +2694,7 @@ async fn daytona_install_finish_writes_settings_and_vault_secret() {\n settings.contains(\"provider = \\\"daytona\\\"\"),\n \"settings.toml should record daytona sandbox provider\"\n );\n+ assert_sandbox_provider_policy_enabled(&settings);\n \n let vault = Vault::load(Storage::new(temp_dir.path()).secrets_path()).unwrap();\n assert_eq!(vault.get(\"DAYTONA_API_KEY\"), Some(api_key));\ndiff --git a/lib/crates/fabro-server/tests/it/api/runs.rs b/lib/crates/fabro-server/tests/it/api/runs.rs\nindex 32c205c1e..c6521ee7c 100644\n--- a/lib/crates/fabro-server/tests/it/api/runs.rs\n+++ b/lib/crates/fabro-server/tests/it/api/runs.rs\n@@ -52,6 +52,84 @@ async fn request_json(\n .await\n }\n \n+fn daytona_manifest() -> serde_json::Value {\n+ let mut manifest = minimal_manifest_json(MINIMAL_DOT);\n+ manifest[\"args\"] = serde_json::json!({ \"environment\": \"daytona\" });\n+ manifest\n+}\n+\n+fn daytona_disabled_settings() -> crate::helpers::TestAppSettings {\n+ settings_from_toml(\n+ r\"\n+_version = 1\n+\n+[server.sandbox.providers.daytona]\n+enabled = false\n+\",\n+ )\n+}\n+\n+#[tokio::test]\n+async fn create_run_rejects_disabled_sandbox_provider() {\n+ let app = fabro_server::test_support::build_test_router(test_app_state_with_options(\n+ daytona_disabled_settings(),\n+ 5,\n+ ));\n+\n+ let request = Request::builder()\n+ .method(\"POST\")\n+ .uri(api(\"/runs\"))\n+ .header(\"content-type\", \"application/json\")\n+ .body(Body::from(daytona_manifest().to_string()))\n+ .expect(\"create run request should build\");\n+ let body = response_json(\n+ app.clone().oneshot(request).await.unwrap(),\n+ StatusCode::BAD_REQUEST,\n+ \"POST /api/v1/runs\",\n+ )\n+ .await;\n+\n+ assert_eq!(\n+ body[\"errors\"][0][\"detail\"],\n+ \"sandbox provider \\\"daytona\\\" is disabled by server.sandbox.providers.daytona.enabled\"\n+ );\n+}\n+\n+#[tokio::test]\n+async fn preflight_reports_disabled_sandbox_provider() {\n+ let app = fabro_server::test_support::build_test_router(test_app_state_with_options(\n+ daytona_disabled_settings(),\n+ 5,\n+ ));\n+\n+ let request = Request::builder()\n+ .method(\"POST\")\n+ .uri(api(\"/preflight\"))\n+ .header(\"content-type\", \"application/json\")\n+ .body(Body::from(daytona_manifest().to_string()))\n+ .expect(\"preflight request should build\");\n+ let body = response_json(\n+ app.clone().oneshot(request).await.unwrap(),\n+ StatusCode::OK,\n+ \"POST /api/v1/preflight\",\n+ )\n+ .await;\n+\n+ assert_eq!(body[\"ok\"], false);\n+ let checks = body[\"checks\"][\"sections\"][0][\"checks\"]\n+ .as_array()\n+ .expect(\"preflight checks should be an array\");\n+ let policy_check = checks\n+ .iter()\n+ .find(|check| check[\"name\"] == \"Sandbox Provider Policy\")\n+ .expect(\"policy check should be present\");\n+ assert_eq!(policy_check[\"status\"], \"error\");\n+ assert_eq!(\n+ policy_check[\"summary\"],\n+ \"sandbox provider \\\"daytona\\\" is disabled by server.sandbox.providers.daytona.enabled\"\n+ );\n+}\n+\n #[tokio::test]\n async fn run_responses_include_ask_fabro_affordance() {\n let settings = settings_from_toml(\ndiff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs\nindex fa92d3f64..9f7daba0d 100644\n--- a/lib/crates/fabro-types/src/settings/server.rs\n+++ b/lib/crates/fabro-types/src/settings/server.rs\n@@ -29,6 +29,7 @@ pub struct ServerNamespace {\n pub web: ServerWebSettings,\n pub auth: ServerAuthSettings,\n pub ip_allowlist: ServerIpAllowlistSettings,\n+ pub sandbox: ServerSandboxSettings,\n pub storage: ServerStorageSettings,\n pub artifacts: ServerArtifactsSettings,\n pub slatedb: ServerSlateDbSettings,\n@@ -50,6 +51,7 @@ impl ServerNamespace {\n web: ServerWebSettings::default(),\n auth: ServerAuthSettings::default(),\n ip_allowlist: ServerIpAllowlistSettings::default(),\n+ sandbox: ServerSandboxSettings::default(),\n storage: ServerStorageSettings::default(),\n artifacts: ServerArtifactsSettings::default(),\n slatedb: ServerSlateDbSettings::default(),\n@@ -133,6 +135,29 @@ pub struct ServerIpAllowlistOverrideSettings {\n pub trusted_proxy_count: Option,\n }\n \n+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n+pub struct ServerSandboxSettings {\n+ pub providers: ServerSandboxProvidersSettings,\n+}\n+\n+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\n+pub struct ServerSandboxProvidersSettings {\n+ pub local: ServerSandboxProviderSettings,\n+ pub docker: ServerSandboxProviderSettings,\n+ pub daytona: ServerSandboxProviderSettings,\n+}\n+\n+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\n+pub struct ServerSandboxProviderSettings {\n+ pub enabled: bool,\n+}\n+\n+impl Default for ServerSandboxProviderSettings {\n+ fn default() -> Self {\n+ Self { enabled: true }\n+ }\n+}\n+\n #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n pub enum IpAllowEntry {\n Literal(IpNet),\ndiff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\nindex f8d333e14..633d462aa 100644\n--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n+++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES\n@@ -389,6 +389,9 @@ models/server-listen-tcp-settings.ts\n models/server-listen-unix-settings.ts\n models/server-logging-settings.ts\n models/server-namespace.ts\n+models/server-sandbox-provider-settings.ts\n+models/server-sandbox-providers-settings.ts\n+models/server-sandbox-settings.ts\n models/server-scheduler-settings.ts\n models/server-settings.ts\n models/server-slate-db-settings.ts\ndiff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts\nindex 78d106d70..26530b0fe 100644\n--- a/lib/packages/fabro-api-client/src/models/index.ts\n+++ b/lib/packages/fabro-api-client/src/models/index.ts\n@@ -365,6 +365,9 @@ export * from './server-listen-tcp-settings';\n export * from './server-listen-unix-settings';\n export * from './server-logging-settings';\n export * from './server-namespace';\n+export * from './server-sandbox-provider-settings';\n+export * from './server-sandbox-providers-settings';\n+export * from './server-sandbox-settings';\n export * from './server-scheduler-settings';\n export * from './server-settings';\n export * from './server-slate-db-settings';\ndiff --git a/lib/packages/fabro-api-client/src/models/server-namespace.ts b/lib/packages/fabro-api-client/src/models/server-namespace.ts\nindex f162fe6cc..315243e88 100644\n--- a/lib/packages/fabro-api-client/src/models/server-namespace.ts\n+++ b/lib/packages/fabro-api-client/src/models/server-namespace.ts\n@@ -36,6 +36,9 @@ import type { ServerListenSettings } from './server-listen-settings';\n import type { ServerLoggingSettings } from './server-logging-settings';\n // May contain unused imports in some cases\n // @ts-ignore\n+import type { ServerSandboxSettings } from './server-sandbox-settings';\n+// May contain unused imports in some cases\n+// @ts-ignore\n import type { ServerSchedulerSettings } from './server-scheduler-settings';\n // May contain unused imports in some cases\n // @ts-ignore\n@@ -53,6 +56,7 @@ export interface ServerNamespace {\n 'web': ServerWebSettings;\n 'auth': ServerAuthSettings;\n 'ip_allowlist': ServerIpAllowlistSettings;\n+ 'sandbox': ServerSandboxSettings;\n 'storage': ServerStorageSettings;\n 'artifacts': ServerArtifactsSettings;\n 'slatedb': ServerSlateDbSettings;\ndiff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts\nnew file mode 100644\nindex 000000000..c50d09f6d\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts\n@@ -0,0 +1,19 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+\n+export interface ServerSandboxProviderSettings {\n+ 'enabled': boolean;\n+}\ndiff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts\nnew file mode 100644\nindex 000000000..9fa9a35a1\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts\n@@ -0,0 +1,24 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { ServerSandboxProviderSettings } from './server-sandbox-provider-settings';\n+\n+export interface ServerSandboxProvidersSettings {\n+ 'local': ServerSandboxProviderSettings;\n+ 'docker': ServerSandboxProviderSettings;\n+ 'daytona': ServerSandboxProviderSettings;\n+}\ndiff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts\nnew file mode 100644\nindex 000000000..cb6a3b2d6\n--- /dev/null\n+++ b/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts\n@@ -0,0 +1,22 @@\n+/* tslint:disable */\n+/* eslint-disable */\n+/**\n+ * Fabro Run API\n+ * HTTP API for managing Fabro workflow run executions.\n+ *\n+ * The version of the OpenAPI document: 0.1.0\n+ *\n+ *\n+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n+ * https://openapi-generator.tech\n+ * Do not edit the class manually.\n+ */\n+\n+\n+// May contain unused imports in some cases\n+// @ts-ignore\n+import type { ServerSandboxProvidersSettings } from './server-sandbox-providers-settings';\n+\n+export interface ServerSandboxSettings {\n+ 'providers': ServerSandboxProvidersSettings;\n+}\n", + "summary": { + "files_changed": 25, + "additions": 556, + "deletions": 17 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-05-24T18:13:19.020455Z", + "current_node": "simplify_opus", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement", + "simplify_opus" + ], + "node_retries": {}, + "context_values": { + "failure_class": "", + "last_stage": "simplify_opus", + "thread.implement.current_node": "simplify_opus", "graph.rankdir": "LR", "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", - "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", + "last_response": "## Summary\n\nThe three review agents flagged several issues; I implemented the high-value fixes and left low-value ones alone.\n\n### Fixes applied\n\n1. **Dedup'd dry-run coercion** — added `SandboxProv", "internal.retry_count.start": 0, "internal.work_dir": "/home/daytona/workspace/fabro", "failure_signature": "", - "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", - "internal.thread_id": "preflight_lint", - "internal.node_visit_count": 1, + "response.simplify_opus": "## Summary\n\nThe three review agents flagged several issues; I implemented the high-value fixes and left low-value ones alone.\n\n### Fixes applied\n\n1. **Dedup'd dry-run coercion** — added `SandboxProvider::effective_for(mode)` in `fabro-types/src/sandbox_provider.rs`. Replaced the duplicated `if RunMode::DryRun && !is_local()` block in both `fabro-server/src/run_manifest.rs::effective_sandbox_provider` and `fabro-workflow/src/operations/start.rs` with a single method call. Also inlined the trivial one-line `resolve_sandbox_provider` wrapper in `run_manifest.rs`.\n\n2. **Encapsulated the stringly-typed match** — added `ServerSandboxProvidersSettings::for_provider(SandboxProvider) -> &ServerSandboxProviderSettings` in `fabro-types/src/settings/server.rs`. `sandbox_provider_policy_error` now uses that instead of hand-matching variants.\n\n3. **Extracted launch-time policy helper** — `fail_if_sandbox_provider_disabled` in `fabro-server/src/server.rs` replaces two identical 7-line blocks in `execute_run_in_process` and `execute_run_subprocess` (including the duplicated `tracing::error!` message).\n\n4. **Looped installer writes** — `write_sandbox_provider_policy` in `fabro-install/src/lib.rs` now iterates over `[SandboxProvider::Local, Docker, Daytona]` (via `Display`) instead of three hardcoded string-literal calls. Removed the now-redundant `write_sandbox_provider_enabled` helper.\n\n### Findings deliberately skipped\n\n- **Collapsing the 3-struct hierarchy** to a flat `{ local: bool, docker: bool, daytona: bool }` — keeping the per-provider struct preserves room for future per-provider fields and matches the TOML shape.\n- **Returning `Result<(), String>` from `sandbox_provider_policy_error`** — the four call sites use the error in different ways (response body, check report, run-fail message), so `Option` is the right shape.\n- **Backtick-quoting the config path in the error message** — the spec mandates the exact string, and the new tests assert it verbatim.\n- **Hoisting `state.server_settings()` in `create_run`** — `RwLock::read + Arc::clone` is negligible.\n- **Pulling `assert_sandbox_provider_policy_enabled` into a shared test-support module** — only used in two tests of one crate.\n\n### Verification\n\n- `cargo check --workspace` clean\n- `cargo nextest run -p fabro-config` 220/220 pass (incl. new policy tests)\n- `cargo nextest run -p fabro-types -p fabro-install` 326+install pass\n- `cargo nextest run -p fabro-server` 631/634 pass — the 3 failures (`get_graph_returns_svg` + 2 related) are pre-existing and reproduced on the parent commit before any cleanup edits\n- `cargo nextest run -p fabro-server --test it --features test-support` 143/143 pass\n- `cargo nextest run -p fabro-workflow` 1162/1162 pass\n- `cargo nextest run -p fabro-api` 153/153 pass (incl. round-trip)\n- `cargo +nightly-2026-04-14 fmt --check --all` clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` clean\n\nNet change: **+76/−48 lines** removed across 6 files (further reducing duplication on top of the original feature commit).", "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" + "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.retry_count.simplify_opus": 0, + "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", + "current_node": "simplify_opus", + "thread.start.current_node": "toolchain", + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "internal.thread_id": "implement", + "internal.node_visit_count": 1, + "internal.fidelity": "compact", + "thread.preflight_lint.current_node": "implement", + "internal.retry_count.preflight_lint": 0 }, "node_outcomes": { "preflight_lint": { @@ -842,12 +970,53 @@ }, "notes": "Script completed: cargo check -q --workspace 2>&1", "usage": null + }, + "simplify_opus": { + "status": "succeeded", + "context_updates": { + "last_stage": "simplify_opus", + "last_response": "## Summary\n\nThe three review agents flagged several issues; I implemented the high-value fixes and left low-value ones alone.\n\n### Fixes applied\n\n1. **Dedup'd dry-run coercion** — added `SandboxProv", + "response.simplify_opus": "## Summary\n\nThe three review agents flagged several issues; I implemented the high-value fixes and left low-value ones alone.\n\n### Fixes applied\n\n1. **Dedup'd dry-run coercion** — added `SandboxProvider::effective_for(mode)` in `fabro-types/src/sandbox_provider.rs`. Replaced the duplicated `if RunMode::DryRun && !is_local()` block in both `fabro-server/src/run_manifest.rs::effective_sandbox_provider` and `fabro-workflow/src/operations/start.rs` with a single method call. Also inlined the trivial one-line `resolve_sandbox_provider` wrapper in `run_manifest.rs`.\n\n2. **Encapsulated the stringly-typed match** — added `ServerSandboxProvidersSettings::for_provider(SandboxProvider) -> &ServerSandboxProviderSettings` in `fabro-types/src/settings/server.rs`. `sandbox_provider_policy_error` now uses that instead of hand-matching variants.\n\n3. **Extracted launch-time policy helper** — `fail_if_sandbox_provider_disabled` in `fabro-server/src/server.rs` replaces two identical 7-line blocks in `execute_run_in_process` and `execute_run_subprocess` (including the duplicated `tracing::error!` message).\n\n4. **Looped installer writes** — `write_sandbox_provider_policy` in `fabro-install/src/lib.rs` now iterates over `[SandboxProvider::Local, Docker, Daytona]` (via `Display`) instead of three hardcoded string-literal calls. Removed the now-redundant `write_sandbox_provider_enabled` helper.\n\n### Findings deliberately skipped\n\n- **Collapsing the 3-struct hierarchy** to a flat `{ local: bool, docker: bool, daytona: bool }` — keeping the per-provider struct preserves room for future per-provider fields and matches the TOML shape.\n- **Returning `Result<(), String>` from `sandbox_provider_policy_error`** — the four call sites use the error in different ways (response body, check report, run-fail message), so `Option` is the right shape.\n- **Backtick-quoting the config path in the error message** — the spec mandates the exact string, and the new tests assert it verbatim.\n- **Hoisting `state.server_settings()` in `create_run`** — `RwLock::read + Arc::clone` is negligible.\n- **Pulling `assert_sandbox_provider_policy_enabled` into a shared test-support module** — only used in two tests of one crate.\n\n### Verification\n\n- `cargo check --workspace` clean\n- `cargo nextest run -p fabro-config` 220/220 pass (incl. new policy tests)\n- `cargo nextest run -p fabro-types -p fabro-install` 326+install pass\n- `cargo nextest run -p fabro-server` 631/634 pass — the 3 failures (`get_graph_returns_svg` + 2 related) are pre-existing and reproduced on the parent commit before any cleanup edits\n- `cargo nextest run -p fabro-server --test it --features test-support` 143/143 pass\n- `cargo nextest run -p fabro-workflow` 1162/1162 pass\n- `cargo nextest run -p fabro-api` 153/153 pass (incl. round-trip)\n- `cargo +nightly-2026-04-14 fmt --check --all` clean\n- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` clean\n\nNet change: **+76/−48 lines** removed across 6 files (further reducing duplication on top of the original feature commit)." + }, + "notes": "Stage completed: simplify_opus", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 80191, + "output_tokens": 20961, + "reasoning_tokens": 0, + "cache_read_tokens": 4224193, + "cache_write_tokens": 303315 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 303315, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 4932794 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-install/src/lib.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/run_manifest.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/sandbox_provider.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-types/src/settings/server.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-workflow/src/operations/start.rs" + ] } }, - "next_node_id": "simplify_opus", + "next_node_id": "simplify_gpt", "node_visits": { "start": 1, "preflight_lint": 1, + "simplify_opus": 1, "preflight_compile": 1, "implement": 1, "toolchain": 1 @@ -876,6 +1045,148 @@ "superseded_by": null, "pending_interviews": {}, "stages": { + "simplify_opus@1": { + "first_event_seq": 1391, + "prompt": null, + "response": null, + "completion": null, + "provider_used": { + "mode": "agent", + "provider": "anthropic", + "model": "claude-opus-4-7" + }, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "started_at": "2026-05-24T17:58:58.191385Z", + "handler": "agent", + "usage": { + "input_tokens": 80191, + "output_tokens": 20961, + "total_tokens": 4628660, + "reasoning_tokens": 0, + "cache_read_tokens": 4224193, + "cache_write_tokens": 303315, + "total_usd_micros": 4932794 + }, + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "todos": { + "kind": "anthropic_tasks", + "list_id": "anthropic_tasks:120098ad-9ab9-4b3f-8583-303241c9e084", + "items": [ + { + "id": "1", + "status": "completed", + "order": 0, + "subject": "Add SandboxProvider::effective_for_dry_run + ProvidersSettings::is_enabled", + "description": "Move dry-run coercion to fabro-types as a method; add accessor on ServerSandboxProvidersSettings to encapsulate the match.", + "active_form": "Adding SandboxProvider and ProvidersSettings helper methods" + }, + { + "id": "2", + "status": "completed", + "order": 1, + "subject": "Use new helpers in fabro-server + fabro-workflow", + "description": "Simplify run_manifest.rs, dedupe launch-time recheck in server.rs, and use the same helper in fabro-workflow/start.rs.", + "active_form": "Refactoring callers to use the new helpers" + }, + { + "id": "3", + "status": "completed", + "order": 2, + "subject": "Loop installer writes using SandboxProvider variants", + "description": "Replace three explicit calls with iteration over [Local, Docker, Daytona].", + "active_form": "Updating installer to iterate provider variants" + }, + { + "id": "4", + "status": "completed", + "order": 3, + "subject": "Verify build, tests, fmt, clippy", + "description": "Run cargo check, focused tests, fmt --check, and clippy to confirm clean state.", + "active_form": "Verifying build and tests" + } + ] + }, + "subagents": [ + { + "agent_id": "040301ba", + "depth": 1, + "task": "You are reviewing a Rust/TypeScript codebase change for code reuse opportunities.\n\nThe full diff for the change is at /tmp/full_diff.txt. Read it carefully.\n\nThe change adds a server-owned sandbox provider enablement policy at `[server.sandbox.providers.]` (where provider is local/docker/daytona). It enforces this policy across:\n- run creation (server/handler/runs.rs)\n- preflight (run_manifest.rs)\n- launch time (server.rs - in two places: execute_run_in_process and execute_run_subprocess)\n- installer persistence (fabro-install/src/lib.rs)\n- API spec + generated TypeScript client\n- Docs\n\nWorking dir: /home/daytona/workspace/fabro\n\nYour task: Review for code REUSE issues:\n\n1. The new `sandbox_provider_policy_error` function does a manual `match` over `SandboxProvider` enum variants to look up enabled flags. Is there an existing accessor pattern for this? Search for how SandboxProvider is mapped to fields in other places. Could it use IntoStaticStr or an indexed lookup?\n\n2. The new helper `effective_sandbox_provider` replaces inline dry-run coercion logic in `build_preflight_report`. Search if there is similar dry-run coercion logic elsewhere in the codebase that should also use this helper.\n\n3. The installer adds `write_sandbox_provider_enabled` and `write_sandbox_provider_policy` helpers. Search for similar helper patterns in fabro-install (look for write_object_store_settings, ensure_table usage). Is there a more direct way to write a known-shape TOML table? Could the three identical `enabled = true` entries be written via a loop or array iteration?\n\n4. The new policy check pattern (4 call sites) is essentially:\n```rust\nif let Some(error) = run_manifest::sandbox_provider_policy_error(&server_settings, provider) {\n fail or error\n}\n```\nCould the launch-time recheck in `execute_run_in_process` and `execute_run_subprocess` share a helper since both do the same thing?\n\n5. The TypeScript models are generated, so skip those.\n\n6. In `sandbox_provider_policy_error`, the format string uses `Display` for `SandboxProvider`. Verify SandboxProvider has a Display impl that produces \"local\"/\"docker\"/\"daytona\" lowercase strings (look in fabro-sandbox crate).\n\n7. Check the new test helper `assert_sandbox_provider_policy_enabled` in install.rs — is it duplicated with the install.rs internal `sandbox_provider_enabled` helper in fabro-install/src/lib.rs tests? Could they share?\n\nFor each finding, give a concrete, actionable suggestion with file paths and line numbers. If something is already idiomatic and reused appropriately, say so briefly. Do not implement fixes — just report findings.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 54 + } + }, + { + "agent_id": "aec277e6", + "depth": 1, + "task": "You are reviewing a Rust/TypeScript codebase change for code quality issues.\n\nThe full diff for the change is at /tmp/full_diff.txt. Read it carefully.\n\nThe change adds a server-owned sandbox provider enablement policy at `[server.sandbox.providers.]` (where provider is local/docker/daytona). It enforces policy across run creation, preflight, launch time, installer persistence, API spec, and generated clients.\n\nWorking dir: /home/daytona/workspace/fabro\n\nYour task: Review for code QUALITY issues. This is a greenfield app — be aggressive in optimizing quality.\n\n1. **Redundant state / parameter sprawl / copy-paste**:\n - Are `ServerSandboxSettings`, `ServerSandboxProvidersSettings`, `ServerSandboxProviderSettings` over-nested? They could be flatter (e.g., `ServerSandboxSettings { local: bool, docker: bool, daytona: bool }`). What is the design rationale and is it worth the extra types?\n - The resolver `resolve_sandbox` and `resolve_sandbox_provider` mirror the 3-layer nesting. Same question.\n - The install helper writes three nearly identical entries — could it be a loop/array iteration?\n\n2. **Stringly-typed code**:\n - In `sandbox_provider_policy_error`, the matching on `SandboxProvider` variants is hand-written. Should there be a `.providers.get(provider)` accessor on `ServerSandboxProvidersSettings`? An indexed map keyed by `SandboxProvider`?\n - The installer uses raw strings \"local\"/\"docker\"/\"daytona\". Should it use the SandboxProvider enum's `Display`/`IntoStaticStr` to derive these names?\n - The error string format `\"sandbox provider \\\"{provider}\\\" is disabled by server.sandbox.providers.{provider}.enabled\"` interpolates provider name twice in a TOML-path string. Acceptable, but is it consistent with other config error messages in this codebase?\n\n3. **Leaky abstractions**:\n - `sandbox_provider_policy_error` takes `&ServerSettings` and reaches into `server.sandbox.providers.{variant}.enabled`. Should this logic live as a method on `ServerSandboxProvidersSettings` (e.g., `is_enabled(provider: SandboxProvider) -> bool`) so callers don't navigate the full path?\n - `effective_sandbox_provider` is `pub(crate)` and only used in run_manifest + server.rs. Is the abstraction reasonable, or should server.rs use a different boundary?\n\n4. **Test code duplication**:\n - The 4 call sites for the policy check all look very similar. Consider whether a small helper trait/method or even an `enforce_or_fail!` macro would clean up server.rs.\n - `daytona_disabled_settings()` and `daytona_manifest()` helpers in tests/it/api/runs.rs — are they over-engineered for 2 tests?\n\n5. **Naming**:\n - `sandbox_provider_policy_error` returns `Option` (error message). Is there a more direct API like `enforce(provider) -> Result<()>` that would compose better?\n - `ServerSandboxProviderSettings` (3 nested structs) — naming bloat. Just `SandboxProviderPolicy`? Just `ProviderEnabled`?\n\n6. **Other**:\n - The launch-time recheck appears in TWO places (execute_run_in_process AND execute_run_subprocess) doing the same thing with the same tracing message. This is copy-paste — should be one helper.\n - Look at the format string in run_manifest.rs `sandbox_provider_policy_error`: the closing `})` is on a new line which suggests the line is too long. Check if rustfmt formatted it well.\n\nFor each finding, give a concrete, actionable suggestion with file paths and line numbers. If something is already clean, say so briefly. Do not implement fixes — just report findings.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 27 + } + }, + { + "agent_id": "2d1bf849", + "depth": 1, + "task": "You are reviewing a Rust/TypeScript codebase change for efficiency issues.\n\nThe full diff for the change is at /tmp/full_diff.txt. Read it carefully.\n\nThe change adds server-owned sandbox provider enablement policy enforced across run creation, preflight, launch, and installer persistence.\n\nWorking dir: /home/daytona/workspace/fabro\n\nYour task: Review for EFFICIENCY issues:\n\n1. **Hot-path bloat**:\n - The policy check is added to `create_run`, preflight, AND launch (in two places). Per-request hot path — is `state.server_settings()` cheap (cloned vs reference)? Check the AppState.\n - `effective_sandbox_provider` is called both at create_run and at launch (execute_run_in_process / execute_run_subprocess). For a single run, this re-resolves the provider on settings that should not change. Is there caching, and is it needed?\n\n2. **Unnecessary work / repeated calls**:\n - In `execute_run_in_process` and `execute_run_subprocess`, the new policy check is added. But there's likely already a `server_settings = state.server_settings()` call earlier in those functions. Check whether the new code adds a duplicate fetch.\n - In `build_preflight_report`, the check uses `server_settings` (already loaded) — OK.\n - In `create_run` handler, `state.server_settings()` is called — verify if it's called multiple times in the handler now.\n\n3. **Memory / allocations**:\n - `sandbox_provider_policy_error` allocates a `String` even just to detect disabled. The `(!enabled).then(|| format!(...))` pattern only formats when disabled — OK.\n - The `_format!_` interpolates `provider` twice. Cheap. OK.\n\n4. **Overall**:\n - Settings reads are typically once per request and the cost is negligible. Verify that's true here.\n - Is the recheck at launch necessary if it was already checked at create_run? (Yes — operator could disable provider between create and launch.)\n\nFor each finding, give a concrete, actionable suggestion with file paths and line numbers. Be specific about whether something is actually a problem worth fixing or just noise. Do not implement fixes — just report findings.", + "status": { + "kind": "completed", + "success": true, + "turns_used": 29 + } + } + ], + "permission_level": "full", + "context_window": { + "provider": "anthropic", + "model": "claude-opus-4-7", + "context_window_tokens": 1000000, + "input_tokens": 92408, + "usage_percent": 9.2408, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-05-24T18:13:18.965263Z", + "event_seq": 2133, + "breakdown": [ + { + "category": "system_prompt", + "tokens": 2521, + "usage_percent": 0.2521 + }, + { + "category": "tools", + "tokens": 2872, + "usage_percent": 0.2872 + }, + { + "category": "memory", + "tokens": 5998, + "usage_percent": 0.5998 + }, + { + "category": "conversation", + "tokens": 81009, + "usage_percent": 8.1009 + }, + { + "category": "other", + "tokens": 8, + "usage_percent": 0.0008 + } + ], + "warnings": [] + }, + "state": "running" + }, "preflight_compile@1": { "first_event_seq": 32, "prompt": null, @@ -976,7 +1287,12 @@ "first_event_seq": 52, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-05-24T17:58:54.383898Z" + }, "provider_used": { "mode": "agent", "provider": "openai", @@ -990,6 +1306,12 @@ "output": null, "started_at": "2026-05-24T17:30:59.321665Z", "handler": "agent", + "timing": { + "wall_time_ms": 1675050, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 5701485, "output_tokens": 22374, @@ -1117,7 +1439,7 @@ ], "warnings": [] }, - "state": "running" + "state": "succeeded" }, "start@1": { "first_event_seq": 18, diff --git a/stages/005-implement@1/diff.patch b/stages/005-implement@1/diff.patch new file mode 100644 index 000000000..2d075bf62 --- /dev/null +++ b/stages/005-implement@1/diff.patch @@ -0,0 +1,991 @@ +diff --git a/docs/public/administration/sandboxing.mdx b/docs/public/administration/sandboxing.mdx +index 749eedeed..e74dad1cc 100644 +--- a/docs/public/administration/sandboxing.mdx ++++ b/docs/public/administration/sandboxing.mdx +@@ -7,6 +7,11 @@ Sandboxes isolate agent execution from the host machine. When an agent runs a sh + + Fabro supports three sandbox providers: `local` (no isolation), `docker` (container-level), and `daytona` (cloud VM). See [Environments](/execution/environments) for full provider-specific configuration. + ++Operators can enable or disable which providers the server may launch with ++`[server.sandbox.providers.]` in `settings.toml`. Missing entries default to ++`enabled = true`; setting `enabled = false` rejects new runs whose effective provider is disabled. ++Dry-run Docker/Daytona runs execute locally, so they are governed by the `local` provider policy. ++ + ## Network access control + + For cloud sandboxes (Daytona), you can control outbound network access with `[environments..network]`. Three modes are available: `"allow_all"` (default), `"block"`, and `"cidr_allow_list"` with an `allow = ["..."]` CIDR list. +diff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx +index 3822174ad..a6ad03475 100644 +--- a/docs/public/administration/server-configuration.mdx ++++ b/docs/public/administration/server-configuration.mdx +@@ -17,7 +17,7 @@ Fabro only reads `settings.toml`. Older `server.toml`, `user.toml`, and `cli.tom + + | Scope | Examples | + |---|---| +-| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` | ++| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.sandbox]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` | + | Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.environment]`, `[environments.]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` | + + The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing. +@@ -46,6 +46,15 @@ methods = ["dev-token", "github"] + [server.auth.github] + allowed_usernames = ["alice", "bob"] + ++[server.sandbox.providers.local] ++enabled = true ++ ++[server.sandbox.providers.docker] ++enabled = true ++ ++[server.sandbox.providers.daytona] ++enabled = true ++ + [server.integrations.github] + app_id = "123456" + client_id = "Iv1.abc123" +@@ -165,6 +174,24 @@ GitHub-specific auth policy. + + The GitHub OAuth client ID still lives under `[server.integrations.github].client_id`. + ++### `[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`. ++ ++```toml title="settings.toml" ++[server.sandbox.providers.local] ++enabled = true ++ ++[server.sandbox.providers.docker] ++enabled = true ++ ++[server.sandbox.providers.daytona] ++enabled = true ++``` ++ + ### `[server.slatedb]` section + + Configure the embedded SlateDB key-value store used for run event storage. +diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml +index 58a1e02e9..3ec1be7f8 100644 +--- a/docs/public/api-reference/fabro-api.yaml ++++ b/docs/public/api-reference/fabro-api.yaml +@@ -11192,6 +11192,7 @@ components: + - web + - auth + - ip_allowlist ++ - sandbox + - storage + - artifacts + - slatedb +@@ -11209,6 +11210,8 @@ components: + $ref: "#/components/schemas/ServerAuthSettings" + ip_allowlist: + $ref: "#/components/schemas/ServerIpAllowlistSettings" ++ sandbox: ++ $ref: "#/components/schemas/ServerSandboxSettings" + storage: + $ref: "#/components/schemas/ServerStorageSettings" + artifacts: +@@ -11325,6 +11328,31 @@ components: + type: string + enum: [GitHubMetaHooks] + ++ 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 ++ + ServerStorageSettings: + type: object + required: [root] +diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs +index 4649ef19e..86e848f38 100644 +--- a/lib/crates/fabro-api/build.rs ++++ b/lib/crates/fabro-api/build.rs +@@ -259,6 +259,21 @@ fn main() { + "fabro_types::settings::server::IpAllowEntry", + &[], + ), ++ ( ++ "ServerSandboxSettings", ++ "fabro_types::settings::server::ServerSandboxSettings", ++ &[], ++ ), ++ ( ++ "ServerSandboxProvidersSettings", ++ "fabro_types::settings::server::ServerSandboxProvidersSettings", ++ &[], ++ ), ++ ( ++ "ServerSandboxProviderSettings", ++ "fabro_types::settings::server::ServerSandboxProviderSettings", ++ &[], ++ ), + ( + "ServerStorageSettings", + "fabro_types::settings::server::ServerStorageSettings", +diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs +index a148d9aac..b8f3a02a1 100644 +--- a/lib/crates/fabro-api/src/lib.rs ++++ b/lib/crates/fabro-api/src/lib.rs +@@ -25,7 +25,8 @@ pub mod types { + IpAllowEntry, LogDestination, ObjectStoreSettings, ServerApiSettings, + ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, + ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, +- ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, ++ ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings, ++ ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + WebhookStrategy, + }; +diff --git a/lib/crates/fabro-api/tests/server_settings_round_trip.rs b/lib/crates/fabro-api/tests/server_settings_round_trip.rs +index 45d8ef97e..a7421641b 100644 +--- a/lib/crates/fabro-api/tests/server_settings_round_trip.rs ++++ b/lib/crates/fabro-api/tests/server_settings_round_trip.rs +@@ -2,12 +2,18 @@ use std::any::{TypeId, type_name}; + + use fabro_api::types::{ + LogDestination as ApiLogDestination, ObjectStoreSettings as ApiObjectStoreSettings, +- ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings, ++ ServerNamespace as ApiServerNamespace, ++ ServerSandboxProviderSettings as ApiServerSandboxProviderSettings, ++ ServerSandboxProvidersSettings as ApiServerSandboxProvidersSettings, ++ ServerSandboxSettings as ApiServerSandboxSettings, ServerSettings as ApiServerSettings, + }; + use fabro_config::ServerSettingsBuilder; + use fabro_types::ServerSettings; + use fabro_types::settings::ServerNamespace; +-use fabro_types::settings::server::{LogDestination, ObjectStoreSettings}; ++use fabro_types::settings::server::{ ++ LogDestination, ObjectStoreSettings, ServerSandboxProviderSettings, ++ ServerSandboxProvidersSettings, ServerSandboxSettings, ++}; + + #[test] + fn server_settings_family_reuses_domain_types() { +@@ -15,6 +21,9 @@ fn server_settings_family_reuses_domain_types() { + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); ++ assert_same_type::(); ++ assert_same_type::(); ++ assert_same_type::(); + } + + #[test] +@@ -40,6 +49,9 @@ methods = ["dev-token", "github"] + [server.auth.github] + allowed_usernames = ["alice"] + ++[server.sandbox.providers.daytona] ++enabled = false ++ + [server.storage] + root = "/srv/fabro" + +@@ -61,6 +73,18 @@ slug = "fabro-dev" + assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276"); + assert_eq!(json["server"]["storage"]["root"], "/srv/fabro"); + assert_eq!(json["server"]["logging"]["destination"], "stdout"); ++ 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 ++ ); + assert!(json.get("features").is_none()); + + let round_trip: ApiServerSettings = +diff --git a/lib/crates/fabro-config/src/layers/mod.rs b/lib/crates/fabro-config/src/layers/mod.rs +index bf5f0a38b..6c425c33d 100644 +--- a/lib/crates/fabro-config/src/layers/mod.rs ++++ b/lib/crates/fabro-config/src/layers/mod.rs +@@ -42,8 +42,9 @@ pub use server::{ + GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, + ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer, + ServerIntegrationsLayer, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer, ServerLayer, +- ServerListenLayer, ServerLoggingLayer, ServerSchedulerLayer, ServerSlateDbLayer, +- ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer, ++ ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer, ++ ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ++ ServerWebLayer, SlackIntegrationLayer, + }; + pub use settings::SettingsLayer; + pub use workflow::WorkflowLayer; +diff --git a/lib/crates/fabro-config/src/layers/server.rs b/lib/crates/fabro-config/src/layers/server.rs +index bc03a1824..62939a0e3 100644 +--- a/lib/crates/fabro-config/src/layers/server.rs ++++ b/lib/crates/fabro-config/src/layers/server.rs +@@ -23,6 +23,8 @@ pub struct ServerLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ip_allowlist: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] ++ pub sandbox: Option, ++ #[serde(default, skip_serializing_if = "Option::is_none")] + pub storage: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub artifacts: Option, +@@ -109,6 +111,32 @@ pub struct ServerIpAllowlistOverrideLayer { + pub trusted_proxy_count: Option, + } + ++/// `[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, ++} ++ + /// `[server.storage]` — single managed local disk root. + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] + #[serde(deny_unknown_fields)] +diff --git a/lib/crates/fabro-config/src/lib.rs b/lib/crates/fabro-config/src/lib.rs +index 7ae698007..0fb321caa 100644 +--- a/lib/crates/fabro-config/src/lib.rs ++++ b/lib/crates/fabro-config/src/lib.rs +@@ -57,6 +57,7 @@ pub use layers::{ + RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer, + ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer, + ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer, ++ ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer, + ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer, + SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer, + }; +diff --git a/lib/crates/fabro-config/src/resolve/server.rs b/lib/crates/fabro-config/src/resolve/server.rs +index f6c22c699..cfdf08c7a 100644 +--- a/lib/crates/fabro-config/src/resolve/server.rs ++++ b/lib/crates/fabro-config/src/resolve/server.rs +@@ -4,7 +4,8 @@ use fabro_types::settings::server::{ + IpAllowEntry, ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings, + ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, + ServerIntegrationsSettings, ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, +- ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, ++ ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSandboxProviderSettings, ++ ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + WebhookStrategy, + }; +@@ -15,8 +16,8 @@ use crate::user::default_storage_dir; + use crate::{ + IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer, + ServerArtifactsLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer, +- ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerSlateDbLayer, +- ServerStorageLayer, ServerWebLayer, ++ ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerSandboxLayer, ++ ServerSandboxProviderLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, + }; + + pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> ServerNamespace { +@@ -38,6 +39,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se + web, + auth, + ip_allowlist, ++ sandbox: resolve_sandbox(layer.sandbox.as_ref()), + storage: storage.clone(), + artifacts: resolve_artifacts(layer.artifacts.as_ref(), &storage.root, errors), + slatedb: resolve_slatedb(layer.slatedb.as_ref(), &storage.root, errors), +@@ -64,6 +66,31 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se + } + } + ++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), ++ } ++} ++ + fn resolve_storage(layer: Option<&ServerStorageLayer>) -> ServerStorageSettings { + ServerStorageSettings { + root: layer +diff --git a/lib/crates/fabro-config/src/tests/resolve_server.rs b/lib/crates/fabro-config/src/tests/resolve_server.rs +index d3c7899d4..edd419619 100644 +--- a/lib/crates/fabro-config/src/tests/resolve_server.rs ++++ b/lib/crates/fabro-config/src/tests/resolve_server.rs +@@ -135,6 +135,66 @@ fn resolved_server_integrations_are_slack_only_for_chat() { + ); + } + ++#[test] ++fn server_sandbox_defaults_all_providers_enabled() { ++ let settings = ServerSettingsBuilder::from_toml( ++ r#" ++_version = 1 ++ ++[server.auth] ++methods = ["dev-token"] ++"#, ++ ) ++ .expect("server settings should resolve"); ++ ++ 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 = ServerSettingsBuilder::from_toml( ++ r#" ++_version = 1 ++ ++[server.auth] ++methods = ["dev-token"] ++ ++[server.sandbox.providers.daytona] ++enabled = false ++"#, ++ ) ++ .expect("server settings should resolve"); ++ ++ 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 = 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}" ++ ); ++} ++ + #[test] + fn parsing_rejects_unknown_server_integrations() { + let source = r" +diff --git a/lib/crates/fabro-install/src/lib.rs b/lib/crates/fabro-install/src/lib.rs +index f62de5414..2176b7fb2 100644 +--- a/lib/crates/fabro-install/src/lib.rs ++++ b/lib/crates/fabro-install/src/lib.rs +@@ -442,6 +442,25 @@ pub fn write_object_store_settings( + } + } + ++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(()) ++} ++ + pub fn write_sandbox_settings( + doc: &mut toml::Value, + selection: InstallSandboxSelection, +@@ -461,6 +480,8 @@ pub fn write_sandbox_settings( + "provider".to_string(), + toml::Value::String(provider.to_string()), + ); ++ let server = ensure_table(root, "server")?; ++ write_sandbox_provider_policy(server)?; + Ok(()) + } + +@@ -1407,6 +1428,9 @@ stale = "remove-me" + .and_then(toml::Value::as_str), + Some("docker") + ); ++ 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)); + } + + #[test] +@@ -1433,6 +1457,22 @@ stale = "remove-me" + .and_then(toml::Value::as_str), + Some("daytona") + ); ++ 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)); ++ } ++ ++ 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) + } + + #[test] +diff --git a/lib/crates/fabro-server/src/run_manifest.rs b/lib/crates/fabro-server/src/run_manifest.rs +index ecc61e589..e7f9884a6 100644 +--- a/lib/crates/fabro-server/src/run_manifest.rs ++++ b/lib/crates/fabro-server/src/run_manifest.rs +@@ -26,7 +26,7 @@ use fabro_static::EnvVars; + use fabro_types::settings::cli::OutputVerbosity; + use fabro_types::settings::interp::InterpString; + use fabro_types::settings::run::{EnvironmentProvider, RunGoal, RunMode, RunNamespace}; +-use fabro_types::{ManifestPath, RunId, WorkflowSettings}; ++use fabro_types::{ManifestPath, RunId, ServerSettings, WorkflowSettings}; + use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; + use fabro_validate::Severity; + use fabro_workflow::Error as WorkflowError; +@@ -502,13 +502,26 @@ async fn build_preflight_report( + let resolved_run = materialized.run; + let server_settings = state.server_settings(); + let github_integration = &server_settings.server.integrations.github; +- let sandbox_provider = resolve_sandbox_provider(&resolved_run); +- let sandbox_provider = +- if resolved_run.execution.mode == RunMode::DryRun && !sandbox_provider.is_local() { +- SandboxProvider::Local +- } else { +- sandbox_provider +- }; ++ let sandbox_provider = effective_sandbox_provider(&resolved_run); ++ 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, ++ )); ++ } + run_environment_capability_check(&mut checks, &resolved_run); + let needs_github_credentials = + sandbox_provider.is_clone_based() || resolved_run.integrations.github.is_token_requested(); +@@ -621,6 +634,32 @@ fn resolve_sandbox_provider(settings: &RunNamespace) -> SandboxProvider { + SandboxProvider::from(settings.environment.provider) + } + ++pub(crate) fn sandbox_provider_policy_error( ++ server_settings: &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 ++ } ++} ++ + fn resolve_daytona_config(settings: &RunNamespace) -> DaytonaConfig { + daytona_config_from_environment(&settings.environment, !settings.clone.enabled) + } +diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs +index 3b6069115..d2f1813f9 100644 +--- a/lib/crates/fabro-server/src/server.rs ++++ b/lib/crates/fabro-server/src/server.rs +@@ -3580,6 +3580,15 @@ async fn execute_run_in_process(state: Arc, run_id: RunId) { + finish_cancelled_run_before_execution(&state, run_id).await; + return; + } ++ 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; ++ } + let github_app_result = { + let run_spec = persisted.run_spec(); + let settings = &run_spec.settings.run; +@@ -3804,6 +3813,15 @@ async fn execute_run_subprocess(state: Arc, run_id: RunId) { + } + }; + let agent_fabro_tools_enabled = run_state.spec.settings.run.agent.fabro_tools; ++ let server_settings = state.server_settings(); ++ let effective_provider = run_manifest::effective_sandbox_provider(&run_state.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; ++ } + + let state_for_build = Arc::clone(&state); + let run_dir_for_build = run_dir.clone(); +diff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs +index cfc8dba88..a313d3fda 100644 +--- a/lib/crates/fabro-server/src/server/handler/runs.rs ++++ b/lib/crates/fabro-server/src/server/handler/runs.rs +@@ -606,6 +606,12 @@ async fn create_run( + Err(err) => return ApiError::bad_request(err.to_string()).into_response(), + }; + let run_id = prepared.run_id.unwrap_or_else(RunId::new); ++ 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(); ++ } + if let Some(parent_id) = prepared.parent_id { + if parent_id == run_id { + return ApiError::bad_request("A run cannot be its own parent.").into_response(); +diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs +index 6488ab364..fe8bce65c 100644 +--- a/lib/crates/fabro-server/src/server/tests.rs ++++ b/lib/crates/fabro-server/src/server/tests.rs +@@ -915,6 +915,29 @@ id = "missing" + ); + } + ++#[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, SandboxProvider::Daytona) ++ .as_deref(), ++ Some( ++ "sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled" ++ ) ++ ); ++} ++ + #[test] + fn clone_sandbox_credentials_are_available_for_clone_based_providers() { + use fabro_types::settings::run::EnvironmentProvider; +diff --git a/lib/crates/fabro-server/tests/it/api/install.rs b/lib/crates/fabro-server/tests/it/api/install.rs +index 8b9bea24e..d5c431fde 100644 +--- a/lib/crates/fabro-server/tests/it/api/install.rs ++++ b/lib/crates/fabro-server/tests/it/api/install.rs +@@ -34,6 +34,22 @@ fn spa_fixture_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/spa") + } + ++fn assert_sandbox_provider_policy_enabled(settings: &str) { ++ 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")); ++ ++ let resolved = ServerSettingsBuilder::from_toml(settings) ++ .expect("settings should resolve") ++ .server ++ .sandbox ++ .providers; ++ assert!(resolved.local.enabled); ++ assert!(resolved.docker.enabled); ++ assert!(resolved.daytona.enabled); ++} ++ + async fn mock_daytona_auth_probe(server: &MockServer) -> httpmock::Mock<'_> { + server + .mock_async(|when, then| { +@@ -918,6 +934,7 @@ async fn token_install_finish_persists_settings_env_and_vault() { + settings.contains("provider = \"docker\""), + "settings.toml should record explicit docker sandbox provider" + ); ++ assert_sandbox_provider_policy_enabled(&settings); + let resolved = ServerSettingsBuilder::from_toml(&settings) + .expect("settings should resolve") + .server; +@@ -2677,6 +2694,7 @@ async fn daytona_install_finish_writes_settings_and_vault_secret() { + settings.contains("provider = \"daytona\""), + "settings.toml should record daytona sandbox provider" + ); ++ assert_sandbox_provider_policy_enabled(&settings); + + let vault = Vault::load(Storage::new(temp_dir.path()).secrets_path()).unwrap(); + assert_eq!(vault.get("DAYTONA_API_KEY"), Some(api_key)); +diff --git a/lib/crates/fabro-server/tests/it/api/runs.rs b/lib/crates/fabro-server/tests/it/api/runs.rs +index 32c205c1e..c6521ee7c 100644 +--- a/lib/crates/fabro-server/tests/it/api/runs.rs ++++ b/lib/crates/fabro-server/tests/it/api/runs.rs +@@ -52,6 +52,84 @@ async fn request_json( + .await + } + ++fn daytona_manifest() -> serde_json::Value { ++ let mut manifest = minimal_manifest_json(MINIMAL_DOT); ++ manifest["args"] = serde_json::json!({ "environment": "daytona" }); ++ manifest ++} ++ ++fn daytona_disabled_settings() -> crate::helpers::TestAppSettings { ++ settings_from_toml( ++ r" ++_version = 1 ++ ++[server.sandbox.providers.daytona] ++enabled = false ++", ++ ) ++} ++ ++#[tokio::test] ++async fn create_run_rejects_disabled_sandbox_provider() { ++ let app = fabro_server::test_support::build_test_router(test_app_state_with_options( ++ daytona_disabled_settings(), ++ 5, ++ )); ++ ++ let request = Request::builder() ++ .method("POST") ++ .uri(api("/runs")) ++ .header("content-type", "application/json") ++ .body(Body::from(daytona_manifest().to_string())) ++ .expect("create run request should build"); ++ let body = response_json( ++ app.clone().oneshot(request).await.unwrap(), ++ StatusCode::BAD_REQUEST, ++ "POST /api/v1/runs", ++ ) ++ .await; ++ ++ assert_eq!( ++ body["errors"][0]["detail"], ++ "sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled" ++ ); ++} ++ ++#[tokio::test] ++async fn preflight_reports_disabled_sandbox_provider() { ++ let app = fabro_server::test_support::build_test_router(test_app_state_with_options( ++ daytona_disabled_settings(), ++ 5, ++ )); ++ ++ let request = Request::builder() ++ .method("POST") ++ .uri(api("/preflight")) ++ .header("content-type", "application/json") ++ .body(Body::from(daytona_manifest().to_string())) ++ .expect("preflight request should build"); ++ let body = response_json( ++ app.clone().oneshot(request).await.unwrap(), ++ StatusCode::OK, ++ "POST /api/v1/preflight", ++ ) ++ .await; ++ ++ assert_eq!(body["ok"], false); ++ let checks = body["checks"]["sections"][0]["checks"] ++ .as_array() ++ .expect("preflight checks should be an array"); ++ let policy_check = checks ++ .iter() ++ .find(|check| check["name"] == "Sandbox Provider Policy") ++ .expect("policy check should be present"); ++ assert_eq!(policy_check["status"], "error"); ++ assert_eq!( ++ policy_check["summary"], ++ "sandbox provider \"daytona\" is disabled by server.sandbox.providers.daytona.enabled" ++ ); ++} ++ + #[tokio::test] + async fn run_responses_include_ask_fabro_affordance() { + let settings = settings_from_toml( +diff --git a/lib/crates/fabro-types/src/settings/server.rs b/lib/crates/fabro-types/src/settings/server.rs +index fa92d3f64..9f7daba0d 100644 +--- a/lib/crates/fabro-types/src/settings/server.rs ++++ b/lib/crates/fabro-types/src/settings/server.rs +@@ -29,6 +29,7 @@ pub struct ServerNamespace { + pub web: ServerWebSettings, + pub auth: ServerAuthSettings, + pub ip_allowlist: ServerIpAllowlistSettings, ++ pub sandbox: ServerSandboxSettings, + pub storage: ServerStorageSettings, + pub artifacts: ServerArtifactsSettings, + pub slatedb: ServerSlateDbSettings, +@@ -50,6 +51,7 @@ impl ServerNamespace { + web: ServerWebSettings::default(), + auth: ServerAuthSettings::default(), + ip_allowlist: ServerIpAllowlistSettings::default(), ++ sandbox: ServerSandboxSettings::default(), + storage: ServerStorageSettings::default(), + artifacts: ServerArtifactsSettings::default(), + slatedb: ServerSlateDbSettings::default(), +@@ -133,6 +135,29 @@ pub struct ServerIpAllowlistOverrideSettings { + pub trusted_proxy_count: Option, + } + ++#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] ++pub struct ServerSandboxSettings { ++ pub providers: ServerSandboxProvidersSettings, ++} ++ ++#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] ++pub struct ServerSandboxProvidersSettings { ++ pub local: ServerSandboxProviderSettings, ++ pub docker: ServerSandboxProviderSettings, ++ pub daytona: ServerSandboxProviderSettings, ++} ++ ++#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] ++pub struct ServerSandboxProviderSettings { ++ pub enabled: bool, ++} ++ ++impl Default for ServerSandboxProviderSettings { ++ fn default() -> Self { ++ Self { enabled: true } ++ } ++} ++ + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + pub enum IpAllowEntry { + Literal(IpNet), +diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +index f8d333e14..633d462aa 100644 +--- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES ++++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES +@@ -389,6 +389,9 @@ models/server-listen-tcp-settings.ts + models/server-listen-unix-settings.ts + models/server-logging-settings.ts + models/server-namespace.ts ++models/server-sandbox-provider-settings.ts ++models/server-sandbox-providers-settings.ts ++models/server-sandbox-settings.ts + models/server-scheduler-settings.ts + models/server-settings.ts + models/server-slate-db-settings.ts +diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts +index 78d106d70..26530b0fe 100644 +--- a/lib/packages/fabro-api-client/src/models/index.ts ++++ b/lib/packages/fabro-api-client/src/models/index.ts +@@ -365,6 +365,9 @@ export * from './server-listen-tcp-settings'; + export * from './server-listen-unix-settings'; + export * from './server-logging-settings'; + export * from './server-namespace'; ++export * from './server-sandbox-provider-settings'; ++export * from './server-sandbox-providers-settings'; ++export * from './server-sandbox-settings'; + export * from './server-scheduler-settings'; + export * from './server-settings'; + export * from './server-slate-db-settings'; +diff --git a/lib/packages/fabro-api-client/src/models/server-namespace.ts b/lib/packages/fabro-api-client/src/models/server-namespace.ts +index f162fe6cc..315243e88 100644 +--- a/lib/packages/fabro-api-client/src/models/server-namespace.ts ++++ b/lib/packages/fabro-api-client/src/models/server-namespace.ts +@@ -36,6 +36,9 @@ import type { ServerListenSettings } from './server-listen-settings'; + import type { ServerLoggingSettings } from './server-logging-settings'; + // May contain unused imports in some cases + // @ts-ignore ++import type { ServerSandboxSettings } from './server-sandbox-settings'; ++// May contain unused imports in some cases ++// @ts-ignore + import type { ServerSchedulerSettings } from './server-scheduler-settings'; + // May contain unused imports in some cases + // @ts-ignore +@@ -53,6 +56,7 @@ export interface ServerNamespace { + 'web': ServerWebSettings; + 'auth': ServerAuthSettings; + 'ip_allowlist': ServerIpAllowlistSettings; ++ 'sandbox': ServerSandboxSettings; + 'storage': ServerStorageSettings; + 'artifacts': ServerArtifactsSettings; + 'slatedb': ServerSlateDbSettings; +diff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts +new file mode 100644 +index 000000000..c50d09f6d +--- /dev/null ++++ b/lib/packages/fabro-api-client/src/models/server-sandbox-provider-settings.ts +@@ -0,0 +1,19 @@ ++/* 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. ++ */ ++ ++ ++ ++export interface ServerSandboxProviderSettings { ++ 'enabled': boolean; ++} +diff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts +new file mode 100644 +index 000000000..9fa9a35a1 +--- /dev/null ++++ b/lib/packages/fabro-api-client/src/models/server-sandbox-providers-settings.ts +@@ -0,0 +1,24 @@ ++/* 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. ++ */ ++ ++ ++// May contain unused imports in some cases ++// @ts-ignore ++import type { ServerSandboxProviderSettings } from './server-sandbox-provider-settings'; ++ ++export interface ServerSandboxProvidersSettings { ++ 'local': ServerSandboxProviderSettings; ++ 'docker': ServerSandboxProviderSettings; ++ 'daytona': ServerSandboxProviderSettings; ++} +diff --git a/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts b/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts +new file mode 100644 +index 000000000..cb6a3b2d6 +--- /dev/null ++++ b/lib/packages/fabro-api-client/src/models/server-sandbox-settings.ts +@@ -0,0 +1,22 @@ ++/* 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. ++ */ ++ ++ ++// May contain unused imports in some cases ++// @ts-ignore ++import type { ServerSandboxProvidersSettings } from './server-sandbox-providers-settings'; ++ ++export interface ServerSandboxSettings { ++ 'providers': ServerSandboxProvidersSettings; ++} diff --git a/stages/005-implement@1/status.json b/stages/005-implement@1/status.json new file mode 100644 index 000000000..62a7aa7c5 --- /dev/null +++ b/stages/005-implement@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Stage completed: implement", + "failure_reason": null, + "timestamp": "2026-05-24T17:58:54.383898Z" +} \ No newline at end of file diff --git a/stages/006-simplify_opus@1/prompt.md b/stages/006-simplify_opus@1/prompt.md new file mode 100644 index 000000000..64dbdba62 --- /dev/null +++ b/stages/006-simplify_opus@1/prompt.md @@ -0,0 +1,752 @@ +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) +- **implement**: succeeded + - Model: gpt-5.5, 5.7m tokens in / 32.2k out + - Files: /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 + + +# Simplify: Code Review and Cleanup + +Review changes vs. origin for reuse, quality, and efficiency. Fix any issues found. + +## Phase 1: Identify Changes + +Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation. + +## Phase 2: Launch Three Review Agents in Parallel + +Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context. + +### Agent 1: Code Reuse Review + +For each change: + +1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones. +2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead. +3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates. + +Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it. + +### Agent 2: Code Quality Review + +Review the same changes for hacky patterns: + +1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls +2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones +3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction +4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries +5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase + +Note: This is a greenfield app, so be aggressive in optimizing quality. + +### Agent 3: Efficiency Review + +Review the same changes for efficiency: + +1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns +2. Missed concurrency: independent operations run sequentially when they could run in parallel +3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths +4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error +5. Memory: unbounded data structures, missing cleanup, event listener leaks +6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one + +## Phase 3: Fix Issues + +Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it. + +When done, briefly summarize what was fixed (or confirm the code was already clean). \ No newline at end of file diff --git a/stages/006-simplify_opus@1/provider_used.json b/stages/006-simplify_opus@1/provider_used.json new file mode 100644 index 000000000..672cc50e5 --- /dev/null +++ b/stages/006-simplify_opus@1/provider_used.json @@ -0,0 +1,5 @@ +{ + "mode": "agent", + "provider": "anthropic", + "model": "claude-opus-4-7" +} \ No newline at end of file diff --git a/stages/006-simplify_opus@1/response.md b/stages/006-simplify_opus@1/response.md new file mode 100644 index 000000000..a9461dfd1 --- /dev/null +++ b/stages/006-simplify_opus@1/response.md @@ -0,0 +1,35 @@ +## Summary + +The three review agents flagged several issues; I implemented the high-value fixes and left low-value ones alone. + +### Fixes applied + +1. **Dedup'd dry-run coercion** — added `SandboxProvider::effective_for(mode)` in `fabro-types/src/sandbox_provider.rs`. Replaced the duplicated `if RunMode::DryRun && !is_local()` block in both `fabro-server/src/run_manifest.rs::effective_sandbox_provider` and `fabro-workflow/src/operations/start.rs` with a single method call. Also inlined the trivial one-line `resolve_sandbox_provider` wrapper in `run_manifest.rs`. + +2. **Encapsulated the stringly-typed match** — added `ServerSandboxProvidersSettings::for_provider(SandboxProvider) -> &ServerSandboxProviderSettings` in `fabro-types/src/settings/server.rs`. `sandbox_provider_policy_error` now uses that instead of hand-matching variants. + +3. **Extracted launch-time policy helper** — `fail_if_sandbox_provider_disabled` in `fabro-server/src/server.rs` replaces two identical 7-line blocks in `execute_run_in_process` and `execute_run_subprocess` (including the duplicated `tracing::error!` message). + +4. **Looped installer writes** — `write_sandbox_provider_policy` in `fabro-install/src/lib.rs` now iterates over `[SandboxProvider::Local, Docker, Daytona]` (via `Display`) instead of three hardcoded string-literal calls. Removed the now-redundant `write_sandbox_provider_enabled` helper. + +### Findings deliberately skipped + +- **Collapsing the 3-struct hierarchy** to a flat `{ local: bool, docker: bool, daytona: bool }` — keeping the per-provider struct preserves room for future per-provider fields and matches the TOML shape. +- **Returning `Result<(), String>` from `sandbox_provider_policy_error`** — the four call sites use the error in different ways (response body, check report, run-fail message), so `Option` is the right shape. +- **Backtick-quoting the config path in the error message** — the spec mandates the exact string, and the new tests assert it verbatim. +- **Hoisting `state.server_settings()` in `create_run`** — `RwLock::read + Arc::clone` is negligible. +- **Pulling `assert_sandbox_provider_policy_enabled` into a shared test-support module** — only used in two tests of one crate. + +### Verification + +- `cargo check --workspace` clean +- `cargo nextest run -p fabro-config` 220/220 pass (incl. new policy tests) +- `cargo nextest run -p fabro-types -p fabro-install` 326+install pass +- `cargo nextest run -p fabro-server` 631/634 pass — the 3 failures (`get_graph_returns_svg` + 2 related) are pre-existing and reproduced on the parent commit before any cleanup edits +- `cargo nextest run -p fabro-server --test it --features test-support` 143/143 pass +- `cargo nextest run -p fabro-workflow` 1162/1162 pass +- `cargo nextest run -p fabro-api` 153/153 pass (incl. round-trip) +- `cargo +nightly-2026-04-14 fmt --check --all` clean +- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` clean + +Net change: **+76/−48 lines** removed across 6 files (further reducing duplication on top of the original feature commit). \ No newline at end of file