From 9f244bc2a0b36c1ed24477824851ef03dcea55e8 Mon Sep 17 00:00:00 2001 From: Fabro Date: Fri, 22 May 2026 10:06:36 -0400 Subject: [PATCH] =?UTF-8?q?checkpoint=20=E2=9A=92=EF=B8=8F=20Generated=20w?= =?UTF-8?q?ith=20[Fabro](https://fabro.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.json | 197 ++++- stages/004-preflight_lint@1/output.log | 1 + .../004-preflight_lint@1/script_timing.json | 8 + stages/004-preflight_lint@1/status.json | 6 + stages/005-implement@1/prompt.md | 707 ++++++++++++++++++ stages/005-implement@1/provider_used.json | 5 + stages/005-implement@1/response.md | 28 + 7 files changed, 943 insertions(+), 9 deletions(-) create mode 100644 stages/004-preflight_lint@1/output.log create mode 100644 stages/004-preflight_lint@1/script_timing.json create mode 100644 stages/004-preflight_lint@1/status.json create mode 100644 stages/005-implement@1/prompt.md create mode 100644 stages/005-implement@1/provider_used.json create mode 100644 stages/005-implement@1/response.md diff --git a/run.json b/run.json index 0aa739507..bc3e07006 100644 --- a/run.json +++ b/run.json @@ -517,7 +517,7 @@ "kind": "running" }, "status_updated_at": "2026-05-22T13:49:58.598463Z", - "last_event_at": "2026-05-22T13:52:16.449947Z", + "last_event_at": "2026-05-22T14:06:36.101568Z", "pending_control": null, "checkpoints": [ { @@ -684,9 +684,9 @@ } }, { - "seq": 0, + "seq": 47, "checkpoint": { - "timestamp": "2026-05-22T13:54:33.612952Z", + "timestamp": "2026-05-22T13:54:37.709957Z", "current_node": "preflight_lint", "completed_nodes": [ "start", @@ -696,24 +696,111 @@ ], "node_retries": {}, "context_values": { + "outcome": "succeeded", + "thread.start.current_node": "toolchain", "failure_class": "", + "graph.rankdir": "LR", + "internal.fidelity": "compact", + "thread.preflight_compile.current_node": "preflight_lint", + "internal.node_visit_count": 1, + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "internal.retry_count.preflight_lint": 0, + "internal.retry_count.start": 0, + "thread.toolchain.current_node": "preflight_compile", + "failure_signature": "", + "graph.goal": "# Run Agent Fabro Tools Opt-In 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 `[run.agent] fabro_tools = true/false`, defaulting to `false`, so workflow agents only get Fabro run tools and the `agent:run_tools` worker JWT scope when a run opts in.\n\n**Architecture:** Treat `run.agent.fabro_tools` as the source of truth in resolved run settings. The server reads the effective run setting before spawning `__run-worker`, issues the worker token with or without `agent:run_tools`, and passes a private worker env flag so the CLI worker registers Fabro run tools only for opted-in runs. Server-side JWT scope checks remain the authorization backstop.\n\n**Tech Stack:** Rust, Serde TOML config layers, Fabro worker JWT scopes, Tokio subprocess spawning, `cargo nextest`.\n\n---\n\n## File Map\n\n- Modify `lib/crates/fabro-types/src/settings/run.rs`: add the resolved `RunAgentSettings::fabro_tools` boolean.\n- Modify `lib/crates/fabro-config/src/layers/run.rs`: add the optional layered `[run.agent] fabro_tools` field and options metadata.\n- Modify `lib/crates/fabro-config/src/resolve/run.rs`: resolve missing config to `false`.\n- Modify `lib/crates/fabro-config/src/tests/resolve_run.rs`: cover default, true, false, and layer override behavior.\n- Modify `lib/crates/fabro-static/src/env_vars.rs`: add a typed internal worker env var name.\n- Modify `lib/crates/fabro-server/src/worker_token.rs`: make `WorkerScopeSet::run_worker()` available to production code.\n- Modify `lib/crates/fabro-server/src/server.rs`: compute the opt-in flag from the run spec, choose worker JWT scopes, and pass the worker env flag.\n- Modify `lib/crates/fabro-server/src/server/tests.rs`: update worker command tests for default and opted-in scope/env behavior.\n- Modify `lib/crates/fabro-cli/src/commands/run/runner.rs`: gate `FabroRunToolServices` construction on the worker env flag and add unit coverage for the env parser.\n- Modify docs generator/reference docs: `lib/crates/fabro-dev/src/commands/docs_options_reference.rs`, `docs/public/reference/user-configuration.mdx`, and `docs/public/execution/run-configuration.mdx`.\n\n---\n\n### Task 1: Add Resolved Run Config\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/run.rs`\n- Modify: `lib/crates/fabro-config/src/layers/run.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/run.rs`\n- Test: `lib/crates/fabro-config/src/tests/resolve_run.rs`\n\n- [ ] **Step 1: Write config resolver tests first**\n\nAdd a `run_agent_fabro_tools` test module to `lib/crates/fabro-config/src/tests/resolve_run.rs` near the existing run settings tests.\n\n```rust\nmod run_agent_fabro_tools {\n use crate::layers::Combine;\n use crate::{SettingsLayer, WorkflowSettingsBuilder};\n\n fn parse_settings(source: &str) -> SettingsLayer {\n source\n .parse::()\n .expect(\"fixture should parse via SettingsLayer\")\n }\n\n #[test]\n fn defaults_to_false_when_run_agent_is_absent() {\n let settings = WorkflowSettingsBuilder::from_layer(&SettingsLayer::default())\n .expect(\"empty settings should resolve\")\n .run;\n\n assert!(!settings.agent.fabro_tools);\n }\n\n #[test]\n fn resolves_true_from_run_agent_table() {\n let settings = WorkflowSettingsBuilder::from_toml(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = true\n\"#,\n )\n .expect(\"run.agent.fabro_tools should resolve\");\n\n assert!(settings.run.agent.fabro_tools);\n }\n\n #[test]\n fn resolves_explicit_false_from_run_agent_table() {\n let settings = WorkflowSettingsBuilder::from_toml(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = false\n\"#,\n )\n .expect(\"run.agent.fabro_tools false should resolve\");\n\n assert!(!settings.run.agent.fabro_tools);\n }\n\n #[test]\n fn higher_layer_false_overrides_lower_true() {\n let workflow = parse_settings(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = false\n\"#,\n );\n let user = parse_settings(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = true\n\"#,\n );\n let merged = workflow.combine(user);\n\n let settings = WorkflowSettingsBuilder::from_layer(&merged)\n .expect(\"merged settings should resolve\")\n .run;\n\n assert!(!settings.agent.fabro_tools);\n }\n}\n```\n\n- [ ] **Step 2: Run the new tests and confirm they fail**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-config run_agent_fabro_tools\n```\n\nExpected: compile failure mentioning `fabro_tools` is not a field, or parse failure saying `fabro_tools` is unknown.\n\n- [ ] **Step 3: Add the resolved setting**\n\nUpdate `RunAgentSettings` in `lib/crates/fabro-types/src/settings/run.rs`:\n\n```rust\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]\npub struct RunAgentSettings {\n pub fabro_tools: bool,\n pub permissions: Option,\n pub mcps: HashMap,\n}\n```\n\n- [ ] **Step 4: Add the layered TOML field**\n\nUpdate `RunAgentLayer` in `lib/crates/fabro-config/src/layers/run.rs`:\n\n```rust\n/// `[run.agent]` — agent knobs only (Fabro tools, permissions, MCPs).\n#[derive(\n Debug,\n Clone,\n Default,\n PartialEq,\n Serialize,\n Deserialize,\n fabro_macros::Combine,\n fabro_macros::OptionsMetadata,\n)]\n#[serde(deny_unknown_fields)]\npub struct RunAgentLayer {\n /// Allow workflow agents to use Fabro run-management tools.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n #[option(default = \"false\", value_type = \"boolean\")]\n pub fabro_tools: Option,\n\n /// Default tool permission level for workflow agents.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n #[option(\n default = \"\\\"read-write\\\"\",\n value_type = \"\\\"read-only\\\" | \\\"read-write\\\" | \\\"full\\\"\"\n )]\n pub permissions: Option,\n\n /// Agent-scoped MCP server entries, keyed by name.\n #[serde(default, skip_serializing_if = \"StickyMap::is_empty\")]\n #[option(value_type = \"table\")]\n pub mcps: StickyMap,\n}\n```\n\n- [ ] **Step 5: Resolve the setting**\n\nUpdate `resolve_agent` in `lib/crates/fabro-config/src/resolve/run.rs`:\n\n```rust\nfn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings {\n let Some(agent) = agent else {\n return RunAgentSettings::default();\n };\n\n RunAgentSettings {\n fabro_tools: agent.fabro_tools.unwrap_or(false),\n permissions: agent.permissions,\n mcps: agent\n .mcps\n .iter()\n .map(|(name, entry)| (name.clone(), resolve_mcp_entry(name, entry)))\n .collect(),\n }\n}\n```\n\n- [ ] **Step 6: Run config tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-config run_agent_fabro_tools\n```\n\nExpected: PASS.\n\n- [ ] **Step 7: Commit**\n\n```bash\ngit add lib/crates/fabro-types/src/settings/run.rs lib/crates/fabro-config/src/layers/run.rs lib/crates/fabro-config/src/resolve/run.rs lib/crates/fabro-config/src/tests/resolve_run.rs\ngit commit -m \"feat: add run agent fabro tools setting\"\n```\n\n---\n\n### Task 2: Gate Worker JWT Scope and Worker Env\n\n**Files:**\n- Modify: `lib/crates/fabro-static/src/env_vars.rs`\n- Modify: `lib/crates/fabro-server/src/worker_token.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Test: `lib/crates/fabro-server/src/server/tests.rs`\n\n- [ ] **Step 1: Write server tests for default and opted-in runs**\n\nUpdate `worker_command_always_sets_worker_token_env` in `lib/crates/fabro-server/src/server/tests.rs` so the default case expects only `run:worker`. Add a second test that passes `agent_fabro_tools_enabled = true` and expects both scopes plus the worker env flag.\n\n```rust\n#[cfg(unix)]\n#[test]\nfn worker_command_default_token_omits_agent_run_tools_scope() {\n let storage_dir = tempfile::tempdir().unwrap();\n let state = worker_command_test_state(storage_dir.path(), &[\"dev-token\"], Some(TEST_DEV_TOKEN));\n let run_id = RunId::new();\n\n let cmd = worker_command(\n state.as_ref(),\n run_id,\n RunExecutionMode::Start,\n storage_dir.path(),\n false,\n )\n .unwrap();\n\n let EnvOverride::Set(token) = command_env_value(&cmd, EnvVars::FABRO_WORKER_TOKEN) else {\n panic!(\"worker token env should be set\");\n };\n let claims = jsonwebtoken::decode::(\n &token,\n state.worker_token_keys().decoding_key(),\n state.worker_token_keys().validation(),\n )\n .expect(\"worker token should decode\")\n .claims;\n\n assert_eq!(claims.scope.split_whitespace().collect::>(), vec![\"run:worker\"]);\n assert_eq!(\n command_env_value(&cmd, EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS),\n EnvOverride::Removed\n );\n}\n\n#[cfg(unix)]\n#[test]\nfn worker_command_opt_in_token_includes_agent_run_tools_scope() {\n let storage_dir = tempfile::tempdir().unwrap();\n let state = worker_command_test_state(storage_dir.path(), &[\"dev-token\"], Some(TEST_DEV_TOKEN));\n let run_id = RunId::new();\n\n let cmd = worker_command(\n state.as_ref(),\n run_id,\n RunExecutionMode::Start,\n storage_dir.path(),\n true,\n )\n .unwrap();\n\n let EnvOverride::Set(token) = command_env_value(&cmd, EnvVars::FABRO_WORKER_TOKEN) else {\n panic!(\"worker token env should be set\");\n };\n let claims = jsonwebtoken::decode::(\n &token,\n state.worker_token_keys().decoding_key(),\n state.worker_token_keys().validation(),\n )\n .expect(\"worker token should decode\")\n .claims;\n\n assert_eq!(\n claims.scope.split_whitespace().collect::>(),\n vec![\"run:worker\", \"agent:run_tools\"]\n );\n assert_eq!(\n command_env_value(&cmd, EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS),\n EnvOverride::Set(\"true\".to_string())\n );\n}\n```\n\n- [ ] **Step 2: Run the server tests and confirm they fail**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-server worker_command_\n```\n\nExpected: compile failure because `FABRO_WORKER_AGENT_RUN_TOOLS` and the new `worker_command` argument do not exist.\n\n- [ ] **Step 3: Add the internal env var constant**\n\nUpdate `lib/crates/fabro-static/src/env_vars.rs` near `FABRO_WORKER_TOKEN`:\n\n```rust\npub const FABRO_WORKER_AGENT_RUN_TOOLS: &'static str = \"FABRO_WORKER_AGENT_RUN_TOOLS\";\npub const FABRO_WORKER_TOKEN: &'static str = \"FABRO_WORKER_TOKEN\";\n```\n\nUpdate the EnvVars tests in the same file so the new constant is included in the alphabetized/core variable expectations.\n\n- [ ] **Step 4: Make the base worker scope constructor available**\n\nUpdate `lib/crates/fabro-server/src/worker_token.rs`:\n\n```rust\nimpl WorkerScopeSet {\n #[must_use]\n pub(crate) const fn run_worker() -> Self {\n Self {\n agent_run_tools: false,\n }\n }\n\n #[must_use]\n pub(crate) const fn run_worker_with_agent_run_tools() -> Self {\n Self {\n agent_run_tools: true,\n }\n }\n}\n```\n\nRemove only the `#[cfg(test)]` attribute from `run_worker`; leave the existing tests intact.\n\n- [ ] **Step 5: Add the worker command parameter and choose scopes**\n\nUpdate `worker_command` in `lib/crates/fabro-server/src/server.rs`:\n\n```rust\nfn worker_command(\n state: &AppState,\n run_id: RunId,\n mode: RunExecutionMode,\n run_dir: &std::path::Path,\n agent_fabro_tools_enabled: bool,\n) -> anyhow::Result {\n // existing setup...\n let scopes = if agent_fabro_tools_enabled {\n WorkerScopeSet::run_worker_with_agent_run_tools()\n } else {\n WorkerScopeSet::run_worker()\n };\n let worker_token = issue_worker_token_with_scopes(state.worker_token_keys(), &run_id, scopes)\n .map_err(|_| anyhow::anyhow!(\"failed to sign worker token\"))?;\n\n // existing Command construction...\n cmd.env_remove(EnvVars::FABRO_WORKER_TOKEN);\n cmd.env(EnvVars::FABRO_WORKER_TOKEN, worker_token);\n cmd.env_remove(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS);\n if agent_fabro_tools_enabled {\n cmd.env(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS, \"true\");\n }\n // existing GitHub key forwarding...\n}\n```\n\nUpdate all `worker_command(...)` test call sites to pass `false` unless the test is explicitly about Fabro tool opt-in.\n\n- [ ] **Step 6: Load the effective run setting before spawning the worker**\n\nUpdate `execute_run_subprocess` in `lib/crates/fabro-server/src/server.rs` after `open_run` succeeds and before `spawn_blocking`:\n\n```rust\nlet run_state = match run_store.state().await {\n Ok(run_state) => run_state,\n Err(err) => {\n tracing::error!(run_id = %run_id, error = %err, \"Failed to load run state\");\n fail_managed_run(\n &state,\n run_id,\n FailureReason::WorkflowError,\n format!(\"Failed to load run state: {err}\"),\n );\n state.scheduler_notify.notify_one();\n return;\n }\n};\nlet agent_fabro_tools_enabled = run_state.spec.settings.run.agent.fabro_tools;\n```\n\nPass the boolean into `worker_command` inside the existing `spawn_blocking` closure:\n\n```rust\nworker_command(\n state_for_build.as_ref(),\n run_id,\n execution_mode,\n &run_dir_for_build,\n agent_fabro_tools_enabled,\n)\n```\n\n- [ ] **Step 7: Run server tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-server worker_command\n```\n\nExpected: PASS.\n\n- [ ] **Step 8: Commit**\n\n```bash\ngit add lib/crates/fabro-static/src/env_vars.rs lib/crates/fabro-server/src/worker_token.rs lib/crates/fabro-server/src/server.rs lib/crates/fabro-server/src/server/tests.rs\ngit commit -m \"feat: gate worker run tool scope by run setting\"\n```\n\n---\n\n### Task 3: Gate CLI Worker Tool Registration\n\n**Files:**\n- Modify: `lib/crates/fabro-cli/src/commands/run/runner.rs`\n- Test: `lib/crates/fabro-cli/src/commands/run/runner.rs`\n\n- [ ] **Step 1: Add a focused test for the env gate**\n\nAdd a `#[cfg(test)]` module at the bottom of `lib/crates/fabro-cli/src/commands/run/runner.rs`:\n\n```rust\n#[cfg(test)]\nmod tests {\n use super::fabro_run_tools_enabled_from_env;\n\n #[test]\n fn fabro_run_tools_enabled_env_requires_true() {\n assert!(!fabro_run_tools_enabled_from_env(None));\n assert!(!fabro_run_tools_enabled_from_env(Some(\"\")));\n assert!(!fabro_run_tools_enabled_from_env(Some(\"false\")));\n assert!(!fabro_run_tools_enabled_from_env(Some(\"1\")));\n assert!(fabro_run_tools_enabled_from_env(Some(\"true\")));\n }\n}\n```\n\n- [ ] **Step 2: Run the new test and confirm it fails**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-cli fabro_run_tools_enabled_env_requires_true\n```\n\nExpected: compile failure because `fabro_run_tools_enabled_from_env` does not exist.\n\n- [ ] **Step 3: Add the env parsing helper**\n\nAdd this helper near `build_fabro_run_tool_services` in `lib/crates/fabro-cli/src/commands/run/runner.rs`:\n\n```rust\nfn fabro_run_tools_enabled_from_env(value: Option<&str>) -> bool {\n value == Some(\"true\")\n}\n```\n\n- [ ] **Step 4: Gate service construction in worker startup**\n\nReplace the unconditional `build_fabro_run_tool_services(...)` call in `execute` with:\n\n```rust\nlet fabro_run_tools = if fabro_run_tools_enabled_from_env(process_env_var(\n EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS,\n).as_deref()) {\n build_fabro_run_tool_services(\n worker_token,\n client.clone_for_reuse(),\n run_id,\n run_spec.source_directory.as_deref(),\n &run_dir,\n Arc::clone(&catalog),\n )\n} else {\n None\n};\n```\n\nKeep `build_fabro_run_tool_services` returning `None` for an empty token. That keeps token presence as a second local guard.\n\n- [ ] **Step 5: Run CLI tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-cli fabro_run_tools_enabled_env_requires_true\ncargo nextest run -p fabro-cli --test it runner\n```\n\nExpected: PASS.\n\n- [ ] **Step 6: Commit**\n\n```bash\ngit add lib/crates/fabro-cli/src/commands/run/runner.rs\ngit commit -m \"feat: register fabro run tools only when opted in\"\n```\n\n---\n\n### Task 4: Update Docs and Generated Reference Text\n\n**Files:**\n- Modify: `lib/crates/fabro-dev/src/commands/docs_options_reference.rs`\n- Modify: `docs/public/reference/user-configuration.mdx`\n- Modify: `docs/public/execution/run-configuration.mdx`\n\n- [ ] **Step 1: Update the docs generator sample**\n\nUpdate the `[run.agent]` sample in `docs_options_reference.rs`:\n\n```rust\nSection::of::(\n \"[run.agent]\",\n r#\"[run.agent]\nfabro_tools = true\npermissions = \"read-write\"\"#,\n),\n```\n\n- [ ] **Step 2: Update generated/reference docs**\n\nIn `docs/public/reference/user-configuration.mdx`, update the `[run.agent]` description, example, and options table:\n\n```mdx\n## `[run.agent]`\n\n`[run.agent]` — agent knobs only (Fabro tools, permissions, MCPs)\n\n```toml\n[run.agent]\nfabro_tools = true\npermissions = \"read-write\"\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `fabro_tools` | boolean | false | Allow workflow agents to use Fabro run-management tools. |\n| `mcps` | table | None | Agent-scoped MCP server entries, keyed by name. |\n| `permissions` | \"read-only\" \\| \"read-write\" \\| \"full\" | \"read-write\" | Default tool permission level for workflow agents. |\n```\n\n- [ ] **Step 3: Add user-facing run configuration docs**\n\nIn `docs/public/execution/run-configuration.mdx`, add a short section before the existing `[run.agent.mcps]` section:\n\n```mdx\n### `[run.agent]`\n\nConfigure workflow agent behavior that is not tied to a single stage.\n\n```toml\n[run.agent]\nfabro_tools = true\n```\n\n`fabro_tools` defaults to `false`. Set it to `true` only for runs whose agents should be able to create, search, inspect, and interact with Fabro runs through the built-in Fabro run tools. This setting is separate from normal agent `permissions` and from MCP server configuration.\n```\n\n- [ ] **Step 4: Run docs/reference checks**\n\nRun:\n\n```bash\ncargo dev docs check\n```\n\nExpected before regenerating docs: FAIL with `docs/public/reference/user-configuration.mdx is stale; run cargo dev docs refresh`.\n\nThen run:\n\n```bash\ncargo dev docs refresh\ncargo dev docs check\n```\n\nExpected: PASS.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add lib/crates/fabro-dev/src/commands/docs_options_reference.rs docs/public/reference/user-configuration.mdx docs/public/execution/run-configuration.mdx\ngit commit -m \"docs: document run agent fabro tools opt in\"\n```\n\n---\n\n### Task 5: Full Verification\n\n**Files:**\n- No source edits unless verification finds a defect.\n\n- [ ] **Step 1: Run targeted package tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-config\ncargo nextest run -p fabro-server\ncargo nextest run -p fabro-cli\n```\n\nExpected: all PASS.\n\n- [ ] **Step 2: Run formatting check**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 fmt --check --all\n```\n\nExpected: PASS.\n\n- [ ] **Step 3: Run clippy for touched Rust crates**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-config -p fabro-static -p fabro-server -p fabro-cli -p fabro-dev --all-targets -- -D warnings\n```\n\nExpected: PASS.\n\n- [ ] **Step 4: Manually review behavior**\n\nConfirm these invariants in the final diff:\n\n```text\nDefault run:\n- resolved run.agent.fabro_tools == false\n- worker JWT scope == \"run:worker\"\n- FABRO_WORKER_AGENT_RUN_TOOLS is absent from worker env\n- StartServices.fabro_run_tools == None\n\nOpted-in run:\n- resolved run.agent.fabro_tools == true\n- worker JWT scope == \"run:worker agent:run_tools\"\n- FABRO_WORKER_AGENT_RUN_TOOLS == \"true\"\n- StartServices.fabro_run_tools is Some(...)\n```\n\n- [ ] **Step 5: Commit verification fixes**\n\nWhen verification changes files, inspect the exact paths and commit them:\n\n```bash\ngit status --short\ngit add -u\ngit commit -m \"test: cover run agent fabro tools opt in\"\n```\n\n---\n\n## Assumptions and Defaults\n\n- `fabro_tools` is a per-run opt-in setting only; this plan does not add a separate server-wide allow/deny policy.\n- Defaulting to `false` intentionally changes existing behavior: runs that need Fabro run tools must set `[run.agent] fabro_tools = true`.\n- `run.agent.permissions` remains about ordinary agent tool permissions and does not imply Fabro API access.\n- `[run.agent.mcps]` remains independent; MCP tools are not enabled or disabled by `fabro_tools`.\n- `fabro mcp start` and standalone MCP exposure of Fabro tools are out of scope.\n- The private worker env var uses the exact string `\"true\"` as the only enabling value, so accidental values such as `\"1\"` or `\"yes\"` do not grant tools.\n- The hidden `__run-worker` CLI argument contract should not grow; use the env var rather than a new hidden CLI flag.\n", + "internal.work_dir": "/home/daytona/workspace/fabro", + "internal.run_id": "01KS7Z5VXPQ37QJQMWSBPMM5MZ", + "internal.thread_id": "preflight_compile", "current_node": "preflight_lint", + "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "internal.retry_count.preflight_compile": 0, + "internal.retry_count.toolchain": 0 + }, + "node_outcomes": { + "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 + }, + "preflight_compile": { + "status": "succeeded", + "context_updates": { + "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126" + }, + "notes": "Script completed: cargo check -q --workspace 2>&1", + "usage": null + } + }, + "next_node_id": "implement", + "git_commit_sha": "e92dc9cd4e0e3dea8442321a3cd5b54eaf0e4cf5", + "node_visits": { + "preflight_lint": 1, + "preflight_compile": 1, + "toolchain": 1, + "start": 1 + } + }, + "diff": { + "summary": { + "files_changed": 0, + "additions": 0, + "deletions": 0 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-05-22T14:06:36.504168Z", + "current_node": "implement", + "completed_nodes": [ + "start", + "toolchain", + "preflight_compile", + "preflight_lint", + "implement" + ], + "node_retries": {}, + "context_values": { + "failure_class": "", + "current_node": "implement", "internal.run_id": "01KS7Z5VXPQ37QJQMWSBPMM5MZ", "failure_signature": "", "internal.retry_count.preflight_compile": 0, + "thread.preflight_lint.current_node": "implement", "internal.fidelity": "compact", - "internal.thread_id": "preflight_compile", + "internal.thread_id": "preflight_lint", "outcome": "succeeded", "command.output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", "internal.retry_count.toolchain": 0, + "last_stage": "implement", "graph.model_stylesheet": "\n * { model: claude-opus-4-7; }\n ", + "last_response": "All changes are in place. Summary of what was done:\n\n**Task 1** — Already completed in prior commits (types/layers/resolver + tests).\n\n**Task 2** — Completed the remaining gaps:\n- Added `FABRO_WOR", "graph.rankdir": "LR", "internal.work_dir": "/home/daytona/workspace/fabro", "internal.retry_count.start": 0, "thread.toolchain.current_node": "preflight_compile", + "response.implement": "All changes are in place. Summary of what was done:\n\n**Task 1** — Already completed in prior commits (types/layers/resolver + tests).\n\n**Task 2** — Completed the remaining gaps:\n- Added `FABRO_WORKER_AGENT_RUN_TOOLS` constant to `fabro-static/src/env_vars.rs` (including the alphabetized test list).\n- Updated `worker_command` in `fabro-server/src/server.rs` to clear the env var and conditionally set it to `\"true\"` when `agent_fabro_tools_enabled` is set.\n- Extended both server tests (`worker_command_default_token_omits_agent_run_tools_scope` and `worker_command_opt_in_token_includes_agent_run_tools_scope`) to assert the env var behavior. The default-case assertion uses `EnvOverride::Unchanged` rather than `Removed` because `apply_worker_env` calls `Command::env_clear()` first, which makes a subsequent `env_remove` a true no-op in `get_envs()` — the conceptual invariant (\"absent from worker env\") still holds.\n\n**Task 3** — Switched the CLI worker to use the env var as the gate:\n- Added `use fabro_static::EnvVars;` import.\n- Replaced `if run_spec.settings.run.agent.fabro_tools` with `if fabro_run_tools_enabled_from_env(process_env_var(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS).as_deref())`.\n- Added the `fabro_run_tools_enabled_from_env` helper that only treats the exact string `\"true\"` as enabled.\n- Added the `fabro_run_tools_enabled_env_requires_true` unit test in the existing `mod tests`.\n\n**Task 4** — Docs already updated in prior commits.\n\n**Task 5** — Verification:\n- `cargo nextest run -p fabro-config` (197 tests pass)\n- `cargo nextest run -p fabro-server` (583 tests pass)\n- `cargo nextest run -p fabro-cli` (906 tests pass)\n- `cargo +nightly-2026-04-14 fmt --check --all` passes\n- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-config -p fabro-static -p fabro-server -p fabro-cli -p fabro-dev --all-targets -- -D warnings` passes\n- `cargo dev docs check` reports docs up to date\n\nFinal invariants are honored:\n- Default run → `run.agent.fabro_tools == false`, JWT scope is just `run:worker`, `FABRO_WORKER_AGENT_RUN_TOOLS` is absent from the worker env, and `StartServices.fabro_run_tools == None`.\n- Opted-in run → `run.agent.fabro_tools == true`, JWT scope is `run:worker agent:run_tools`, `FABRO_WORKER_AGENT_RUN_TOOLS == \"true\"` is set in the worker env, and the env-gated branch in `runner.rs` builds `FabroRunToolServices`.", "internal.retry_count.preflight_lint": 0, "graph.goal": "# Run Agent Fabro Tools Opt-In 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 `[run.agent] fabro_tools = true/false`, defaulting to `false`, so workflow agents only get Fabro run tools and the `agent:run_tools` worker JWT scope when a run opts in.\n\n**Architecture:** Treat `run.agent.fabro_tools` as the source of truth in resolved run settings. The server reads the effective run setting before spawning `__run-worker`, issues the worker token with or without `agent:run_tools`, and passes a private worker env flag so the CLI worker registers Fabro run tools only for opted-in runs. Server-side JWT scope checks remain the authorization backstop.\n\n**Tech Stack:** Rust, Serde TOML config layers, Fabro worker JWT scopes, Tokio subprocess spawning, `cargo nextest`.\n\n---\n\n## File Map\n\n- Modify `lib/crates/fabro-types/src/settings/run.rs`: add the resolved `RunAgentSettings::fabro_tools` boolean.\n- Modify `lib/crates/fabro-config/src/layers/run.rs`: add the optional layered `[run.agent] fabro_tools` field and options metadata.\n- Modify `lib/crates/fabro-config/src/resolve/run.rs`: resolve missing config to `false`.\n- Modify `lib/crates/fabro-config/src/tests/resolve_run.rs`: cover default, true, false, and layer override behavior.\n- Modify `lib/crates/fabro-static/src/env_vars.rs`: add a typed internal worker env var name.\n- Modify `lib/crates/fabro-server/src/worker_token.rs`: make `WorkerScopeSet::run_worker()` available to production code.\n- Modify `lib/crates/fabro-server/src/server.rs`: compute the opt-in flag from the run spec, choose worker JWT scopes, and pass the worker env flag.\n- Modify `lib/crates/fabro-server/src/server/tests.rs`: update worker command tests for default and opted-in scope/env behavior.\n- Modify `lib/crates/fabro-cli/src/commands/run/runner.rs`: gate `FabroRunToolServices` construction on the worker env flag and add unit coverage for the env parser.\n- Modify docs generator/reference docs: `lib/crates/fabro-dev/src/commands/docs_options_reference.rs`, `docs/public/reference/user-configuration.mdx`, and `docs/public/execution/run-configuration.mdx`.\n\n---\n\n### Task 1: Add Resolved Run Config\n\n**Files:**\n- Modify: `lib/crates/fabro-types/src/settings/run.rs`\n- Modify: `lib/crates/fabro-config/src/layers/run.rs`\n- Modify: `lib/crates/fabro-config/src/resolve/run.rs`\n- Test: `lib/crates/fabro-config/src/tests/resolve_run.rs`\n\n- [ ] **Step 1: Write config resolver tests first**\n\nAdd a `run_agent_fabro_tools` test module to `lib/crates/fabro-config/src/tests/resolve_run.rs` near the existing run settings tests.\n\n```rust\nmod run_agent_fabro_tools {\n use crate::layers::Combine;\n use crate::{SettingsLayer, WorkflowSettingsBuilder};\n\n fn parse_settings(source: &str) -> SettingsLayer {\n source\n .parse::()\n .expect(\"fixture should parse via SettingsLayer\")\n }\n\n #[test]\n fn defaults_to_false_when_run_agent_is_absent() {\n let settings = WorkflowSettingsBuilder::from_layer(&SettingsLayer::default())\n .expect(\"empty settings should resolve\")\n .run;\n\n assert!(!settings.agent.fabro_tools);\n }\n\n #[test]\n fn resolves_true_from_run_agent_table() {\n let settings = WorkflowSettingsBuilder::from_toml(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = true\n\"#,\n )\n .expect(\"run.agent.fabro_tools should resolve\");\n\n assert!(settings.run.agent.fabro_tools);\n }\n\n #[test]\n fn resolves_explicit_false_from_run_agent_table() {\n let settings = WorkflowSettingsBuilder::from_toml(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = false\n\"#,\n )\n .expect(\"run.agent.fabro_tools false should resolve\");\n\n assert!(!settings.run.agent.fabro_tools);\n }\n\n #[test]\n fn higher_layer_false_overrides_lower_true() {\n let workflow = parse_settings(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = false\n\"#,\n );\n let user = parse_settings(\n r#\"\n_version = 1\n\n[run.agent]\nfabro_tools = true\n\"#,\n );\n let merged = workflow.combine(user);\n\n let settings = WorkflowSettingsBuilder::from_layer(&merged)\n .expect(\"merged settings should resolve\")\n .run;\n\n assert!(!settings.agent.fabro_tools);\n }\n}\n```\n\n- [ ] **Step 2: Run the new tests and confirm they fail**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-config run_agent_fabro_tools\n```\n\nExpected: compile failure mentioning `fabro_tools` is not a field, or parse failure saying `fabro_tools` is unknown.\n\n- [ ] **Step 3: Add the resolved setting**\n\nUpdate `RunAgentSettings` in `lib/crates/fabro-types/src/settings/run.rs`:\n\n```rust\n#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]\npub struct RunAgentSettings {\n pub fabro_tools: bool,\n pub permissions: Option,\n pub mcps: HashMap,\n}\n```\n\n- [ ] **Step 4: Add the layered TOML field**\n\nUpdate `RunAgentLayer` in `lib/crates/fabro-config/src/layers/run.rs`:\n\n```rust\n/// `[run.agent]` — agent knobs only (Fabro tools, permissions, MCPs).\n#[derive(\n Debug,\n Clone,\n Default,\n PartialEq,\n Serialize,\n Deserialize,\n fabro_macros::Combine,\n fabro_macros::OptionsMetadata,\n)]\n#[serde(deny_unknown_fields)]\npub struct RunAgentLayer {\n /// Allow workflow agents to use Fabro run-management tools.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n #[option(default = \"false\", value_type = \"boolean\")]\n pub fabro_tools: Option,\n\n /// Default tool permission level for workflow agents.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n #[option(\n default = \"\\\"read-write\\\"\",\n value_type = \"\\\"read-only\\\" | \\\"read-write\\\" | \\\"full\\\"\"\n )]\n pub permissions: Option,\n\n /// Agent-scoped MCP server entries, keyed by name.\n #[serde(default, skip_serializing_if = \"StickyMap::is_empty\")]\n #[option(value_type = \"table\")]\n pub mcps: StickyMap,\n}\n```\n\n- [ ] **Step 5: Resolve the setting**\n\nUpdate `resolve_agent` in `lib/crates/fabro-config/src/resolve/run.rs`:\n\n```rust\nfn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings {\n let Some(agent) = agent else {\n return RunAgentSettings::default();\n };\n\n RunAgentSettings {\n fabro_tools: agent.fabro_tools.unwrap_or(false),\n permissions: agent.permissions,\n mcps: agent\n .mcps\n .iter()\n .map(|(name, entry)| (name.clone(), resolve_mcp_entry(name, entry)))\n .collect(),\n }\n}\n```\n\n- [ ] **Step 6: Run config tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-config run_agent_fabro_tools\n```\n\nExpected: PASS.\n\n- [ ] **Step 7: Commit**\n\n```bash\ngit add lib/crates/fabro-types/src/settings/run.rs lib/crates/fabro-config/src/layers/run.rs lib/crates/fabro-config/src/resolve/run.rs lib/crates/fabro-config/src/tests/resolve_run.rs\ngit commit -m \"feat: add run agent fabro tools setting\"\n```\n\n---\n\n### Task 2: Gate Worker JWT Scope and Worker Env\n\n**Files:**\n- Modify: `lib/crates/fabro-static/src/env_vars.rs`\n- Modify: `lib/crates/fabro-server/src/worker_token.rs`\n- Modify: `lib/crates/fabro-server/src/server.rs`\n- Test: `lib/crates/fabro-server/src/server/tests.rs`\n\n- [ ] **Step 1: Write server tests for default and opted-in runs**\n\nUpdate `worker_command_always_sets_worker_token_env` in `lib/crates/fabro-server/src/server/tests.rs` so the default case expects only `run:worker`. Add a second test that passes `agent_fabro_tools_enabled = true` and expects both scopes plus the worker env flag.\n\n```rust\n#[cfg(unix)]\n#[test]\nfn worker_command_default_token_omits_agent_run_tools_scope() {\n let storage_dir = tempfile::tempdir().unwrap();\n let state = worker_command_test_state(storage_dir.path(), &[\"dev-token\"], Some(TEST_DEV_TOKEN));\n let run_id = RunId::new();\n\n let cmd = worker_command(\n state.as_ref(),\n run_id,\n RunExecutionMode::Start,\n storage_dir.path(),\n false,\n )\n .unwrap();\n\n let EnvOverride::Set(token) = command_env_value(&cmd, EnvVars::FABRO_WORKER_TOKEN) else {\n panic!(\"worker token env should be set\");\n };\n let claims = jsonwebtoken::decode::(\n &token,\n state.worker_token_keys().decoding_key(),\n state.worker_token_keys().validation(),\n )\n .expect(\"worker token should decode\")\n .claims;\n\n assert_eq!(claims.scope.split_whitespace().collect::>(), vec![\"run:worker\"]);\n assert_eq!(\n command_env_value(&cmd, EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS),\n EnvOverride::Removed\n );\n}\n\n#[cfg(unix)]\n#[test]\nfn worker_command_opt_in_token_includes_agent_run_tools_scope() {\n let storage_dir = tempfile::tempdir().unwrap();\n let state = worker_command_test_state(storage_dir.path(), &[\"dev-token\"], Some(TEST_DEV_TOKEN));\n let run_id = RunId::new();\n\n let cmd = worker_command(\n state.as_ref(),\n run_id,\n RunExecutionMode::Start,\n storage_dir.path(),\n true,\n )\n .unwrap();\n\n let EnvOverride::Set(token) = command_env_value(&cmd, EnvVars::FABRO_WORKER_TOKEN) else {\n panic!(\"worker token env should be set\");\n };\n let claims = jsonwebtoken::decode::(\n &token,\n state.worker_token_keys().decoding_key(),\n state.worker_token_keys().validation(),\n )\n .expect(\"worker token should decode\")\n .claims;\n\n assert_eq!(\n claims.scope.split_whitespace().collect::>(),\n vec![\"run:worker\", \"agent:run_tools\"]\n );\n assert_eq!(\n command_env_value(&cmd, EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS),\n EnvOverride::Set(\"true\".to_string())\n );\n}\n```\n\n- [ ] **Step 2: Run the server tests and confirm they fail**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-server worker_command_\n```\n\nExpected: compile failure because `FABRO_WORKER_AGENT_RUN_TOOLS` and the new `worker_command` argument do not exist.\n\n- [ ] **Step 3: Add the internal env var constant**\n\nUpdate `lib/crates/fabro-static/src/env_vars.rs` near `FABRO_WORKER_TOKEN`:\n\n```rust\npub const FABRO_WORKER_AGENT_RUN_TOOLS: &'static str = \"FABRO_WORKER_AGENT_RUN_TOOLS\";\npub const FABRO_WORKER_TOKEN: &'static str = \"FABRO_WORKER_TOKEN\";\n```\n\nUpdate the EnvVars tests in the same file so the new constant is included in the alphabetized/core variable expectations.\n\n- [ ] **Step 4: Make the base worker scope constructor available**\n\nUpdate `lib/crates/fabro-server/src/worker_token.rs`:\n\n```rust\nimpl WorkerScopeSet {\n #[must_use]\n pub(crate) const fn run_worker() -> Self {\n Self {\n agent_run_tools: false,\n }\n }\n\n #[must_use]\n pub(crate) const fn run_worker_with_agent_run_tools() -> Self {\n Self {\n agent_run_tools: true,\n }\n }\n}\n```\n\nRemove only the `#[cfg(test)]` attribute from `run_worker`; leave the existing tests intact.\n\n- [ ] **Step 5: Add the worker command parameter and choose scopes**\n\nUpdate `worker_command` in `lib/crates/fabro-server/src/server.rs`:\n\n```rust\nfn worker_command(\n state: &AppState,\n run_id: RunId,\n mode: RunExecutionMode,\n run_dir: &std::path::Path,\n agent_fabro_tools_enabled: bool,\n) -> anyhow::Result {\n // existing setup...\n let scopes = if agent_fabro_tools_enabled {\n WorkerScopeSet::run_worker_with_agent_run_tools()\n } else {\n WorkerScopeSet::run_worker()\n };\n let worker_token = issue_worker_token_with_scopes(state.worker_token_keys(), &run_id, scopes)\n .map_err(|_| anyhow::anyhow!(\"failed to sign worker token\"))?;\n\n // existing Command construction...\n cmd.env_remove(EnvVars::FABRO_WORKER_TOKEN);\n cmd.env(EnvVars::FABRO_WORKER_TOKEN, worker_token);\n cmd.env_remove(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS);\n if agent_fabro_tools_enabled {\n cmd.env(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS, \"true\");\n }\n // existing GitHub key forwarding...\n}\n```\n\nUpdate all `worker_command(...)` test call sites to pass `false` unless the test is explicitly about Fabro tool opt-in.\n\n- [ ] **Step 6: Load the effective run setting before spawning the worker**\n\nUpdate `execute_run_subprocess` in `lib/crates/fabro-server/src/server.rs` after `open_run` succeeds and before `spawn_blocking`:\n\n```rust\nlet run_state = match run_store.state().await {\n Ok(run_state) => run_state,\n Err(err) => {\n tracing::error!(run_id = %run_id, error = %err, \"Failed to load run state\");\n fail_managed_run(\n &state,\n run_id,\n FailureReason::WorkflowError,\n format!(\"Failed to load run state: {err}\"),\n );\n state.scheduler_notify.notify_one();\n return;\n }\n};\nlet agent_fabro_tools_enabled = run_state.spec.settings.run.agent.fabro_tools;\n```\n\nPass the boolean into `worker_command` inside the existing `spawn_blocking` closure:\n\n```rust\nworker_command(\n state_for_build.as_ref(),\n run_id,\n execution_mode,\n &run_dir_for_build,\n agent_fabro_tools_enabled,\n)\n```\n\n- [ ] **Step 7: Run server tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-server worker_command\n```\n\nExpected: PASS.\n\n- [ ] **Step 8: Commit**\n\n```bash\ngit add lib/crates/fabro-static/src/env_vars.rs lib/crates/fabro-server/src/worker_token.rs lib/crates/fabro-server/src/server.rs lib/crates/fabro-server/src/server/tests.rs\ngit commit -m \"feat: gate worker run tool scope by run setting\"\n```\n\n---\n\n### Task 3: Gate CLI Worker Tool Registration\n\n**Files:**\n- Modify: `lib/crates/fabro-cli/src/commands/run/runner.rs`\n- Test: `lib/crates/fabro-cli/src/commands/run/runner.rs`\n\n- [ ] **Step 1: Add a focused test for the env gate**\n\nAdd a `#[cfg(test)]` module at the bottom of `lib/crates/fabro-cli/src/commands/run/runner.rs`:\n\n```rust\n#[cfg(test)]\nmod tests {\n use super::fabro_run_tools_enabled_from_env;\n\n #[test]\n fn fabro_run_tools_enabled_env_requires_true() {\n assert!(!fabro_run_tools_enabled_from_env(None));\n assert!(!fabro_run_tools_enabled_from_env(Some(\"\")));\n assert!(!fabro_run_tools_enabled_from_env(Some(\"false\")));\n assert!(!fabro_run_tools_enabled_from_env(Some(\"1\")));\n assert!(fabro_run_tools_enabled_from_env(Some(\"true\")));\n }\n}\n```\n\n- [ ] **Step 2: Run the new test and confirm it fails**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-cli fabro_run_tools_enabled_env_requires_true\n```\n\nExpected: compile failure because `fabro_run_tools_enabled_from_env` does not exist.\n\n- [ ] **Step 3: Add the env parsing helper**\n\nAdd this helper near `build_fabro_run_tool_services` in `lib/crates/fabro-cli/src/commands/run/runner.rs`:\n\n```rust\nfn fabro_run_tools_enabled_from_env(value: Option<&str>) -> bool {\n value == Some(\"true\")\n}\n```\n\n- [ ] **Step 4: Gate service construction in worker startup**\n\nReplace the unconditional `build_fabro_run_tool_services(...)` call in `execute` with:\n\n```rust\nlet fabro_run_tools = if fabro_run_tools_enabled_from_env(process_env_var(\n EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS,\n).as_deref()) {\n build_fabro_run_tool_services(\n worker_token,\n client.clone_for_reuse(),\n run_id,\n run_spec.source_directory.as_deref(),\n &run_dir,\n Arc::clone(&catalog),\n )\n} else {\n None\n};\n```\n\nKeep `build_fabro_run_tool_services` returning `None` for an empty token. That keeps token presence as a second local guard.\n\n- [ ] **Step 5: Run CLI tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-cli fabro_run_tools_enabled_env_requires_true\ncargo nextest run -p fabro-cli --test it runner\n```\n\nExpected: PASS.\n\n- [ ] **Step 6: Commit**\n\n```bash\ngit add lib/crates/fabro-cli/src/commands/run/runner.rs\ngit commit -m \"feat: register fabro run tools only when opted in\"\n```\n\n---\n\n### Task 4: Update Docs and Generated Reference Text\n\n**Files:**\n- Modify: `lib/crates/fabro-dev/src/commands/docs_options_reference.rs`\n- Modify: `docs/public/reference/user-configuration.mdx`\n- Modify: `docs/public/execution/run-configuration.mdx`\n\n- [ ] **Step 1: Update the docs generator sample**\n\nUpdate the `[run.agent]` sample in `docs_options_reference.rs`:\n\n```rust\nSection::of::(\n \"[run.agent]\",\n r#\"[run.agent]\nfabro_tools = true\npermissions = \"read-write\"\"#,\n),\n```\n\n- [ ] **Step 2: Update generated/reference docs**\n\nIn `docs/public/reference/user-configuration.mdx`, update the `[run.agent]` description, example, and options table:\n\n```mdx\n## `[run.agent]`\n\n`[run.agent]` — agent knobs only (Fabro tools, permissions, MCPs)\n\n```toml\n[run.agent]\nfabro_tools = true\npermissions = \"read-write\"\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `fabro_tools` | boolean | false | Allow workflow agents to use Fabro run-management tools. |\n| `mcps` | table | None | Agent-scoped MCP server entries, keyed by name. |\n| `permissions` | \"read-only\" \\| \"read-write\" \\| \"full\" | \"read-write\" | Default tool permission level for workflow agents. |\n```\n\n- [ ] **Step 3: Add user-facing run configuration docs**\n\nIn `docs/public/execution/run-configuration.mdx`, add a short section before the existing `[run.agent.mcps]` section:\n\n```mdx\n### `[run.agent]`\n\nConfigure workflow agent behavior that is not tied to a single stage.\n\n```toml\n[run.agent]\nfabro_tools = true\n```\n\n`fabro_tools` defaults to `false`. Set it to `true` only for runs whose agents should be able to create, search, inspect, and interact with Fabro runs through the built-in Fabro run tools. This setting is separate from normal agent `permissions` and from MCP server configuration.\n```\n\n- [ ] **Step 4: Run docs/reference checks**\n\nRun:\n\n```bash\ncargo dev docs check\n```\n\nExpected before regenerating docs: FAIL with `docs/public/reference/user-configuration.mdx is stale; run cargo dev docs refresh`.\n\nThen run:\n\n```bash\ncargo dev docs refresh\ncargo dev docs check\n```\n\nExpected: PASS.\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add lib/crates/fabro-dev/src/commands/docs_options_reference.rs docs/public/reference/user-configuration.mdx docs/public/execution/run-configuration.mdx\ngit commit -m \"docs: document run agent fabro tools opt in\"\n```\n\n---\n\n### Task 5: Full Verification\n\n**Files:**\n- No source edits unless verification finds a defect.\n\n- [ ] **Step 1: Run targeted package tests**\n\nRun:\n\n```bash\ncargo nextest run -p fabro-config\ncargo nextest run -p fabro-server\ncargo nextest run -p fabro-cli\n```\n\nExpected: all PASS.\n\n- [ ] **Step 2: Run formatting check**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 fmt --check --all\n```\n\nExpected: PASS.\n\n- [ ] **Step 3: Run clippy for touched Rust crates**\n\nRun:\n\n```bash\ncargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-config -p fabro-static -p fabro-server -p fabro-cli -p fabro-dev --all-targets -- -D warnings\n```\n\nExpected: PASS.\n\n- [ ] **Step 4: Manually review behavior**\n\nConfirm these invariants in the final diff:\n\n```text\nDefault run:\n- resolved run.agent.fabro_tools == false\n- worker JWT scope == \"run:worker\"\n- FABRO_WORKER_AGENT_RUN_TOOLS is absent from worker env\n- StartServices.fabro_run_tools == None\n\nOpted-in run:\n- resolved run.agent.fabro_tools == true\n- worker JWT scope == \"run:worker agent:run_tools\"\n- FABRO_WORKER_AGENT_RUN_TOOLS == \"true\"\n- StartServices.fabro_run_tools is Some(...)\n```\n\n- [ ] **Step 5: Commit verification fixes**\n\nWhen verification changes files, inspect the exact paths and commit them:\n\n```bash\ngit status --short\ngit add -u\ngit commit -m \"test: cover run agent fabro tools opt in\"\n```\n\n---\n\n## Assumptions and Defaults\n\n- `fabro_tools` is a per-run opt-in setting only; this plan does not add a separate server-wide allow/deny policy.\n- Defaulting to `false` intentionally changes existing behavior: runs that need Fabro run tools must set `[run.agent] fabro_tools = true`.\n- `run.agent.permissions` remains about ordinary agent tool permissions and does not imply Fabro API access.\n- `[run.agent.mcps]` remains independent; MCP tools are not enabled or disabled by `fabro_tools`.\n- `fabro mcp start` and standalone MCP exposure of Fabro tools are out of scope.\n- The private worker env var uses the exact string `\"true\"` as the only enabling value, so accidental values such as `\"1\"` or `\"yes\"` do not grant tools.\n- The hidden `__run-worker` CLI argument contract should not grow; use the env var rather than a new hidden CLI flag.\n", "thread.preflight_compile.current_node": "preflight_lint", + "internal.retry_count.implement": 0, "thread.start.current_node": "toolchain", "internal.node_visit_count": 1 }, @@ -726,6 +813,44 @@ "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", "usage": null }, + "implement": { + "status": "succeeded", + "context_updates": { + "last_stage": "implement", + "last_response": "All changes are in place. Summary of what was done:\n\n**Task 1** — Already completed in prior commits (types/layers/resolver + tests).\n\n**Task 2** — Completed the remaining gaps:\n- Added `FABRO_WOR", + "response.implement": "All changes are in place. Summary of what was done:\n\n**Task 1** — Already completed in prior commits (types/layers/resolver + tests).\n\n**Task 2** — Completed the remaining gaps:\n- Added `FABRO_WORKER_AGENT_RUN_TOOLS` constant to `fabro-static/src/env_vars.rs` (including the alphabetized test list).\n- Updated `worker_command` in `fabro-server/src/server.rs` to clear the env var and conditionally set it to `\"true\"` when `agent_fabro_tools_enabled` is set.\n- Extended both server tests (`worker_command_default_token_omits_agent_run_tools_scope` and `worker_command_opt_in_token_includes_agent_run_tools_scope`) to assert the env var behavior. The default-case assertion uses `EnvOverride::Unchanged` rather than `Removed` because `apply_worker_env` calls `Command::env_clear()` first, which makes a subsequent `env_remove` a true no-op in `get_envs()` — the conceptual invariant (\"absent from worker env\") still holds.\n\n**Task 3** — Switched the CLI worker to use the env var as the gate:\n- Added `use fabro_static::EnvVars;` import.\n- Replaced `if run_spec.settings.run.agent.fabro_tools` with `if fabro_run_tools_enabled_from_env(process_env_var(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS).as_deref())`.\n- Added the `fabro_run_tools_enabled_from_env` helper that only treats the exact string `\"true\"` as enabled.\n- Added the `fabro_run_tools_enabled_env_requires_true` unit test in the existing `mod tests`.\n\n**Task 4** — Docs already updated in prior commits.\n\n**Task 5** — Verification:\n- `cargo nextest run -p fabro-config` (197 tests pass)\n- `cargo nextest run -p fabro-server` (583 tests pass)\n- `cargo nextest run -p fabro-cli` (906 tests pass)\n- `cargo +nightly-2026-04-14 fmt --check --all` passes\n- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-config -p fabro-static -p fabro-server -p fabro-cli -p fabro-dev --all-targets -- -D warnings` passes\n- `cargo dev docs check` reports docs up to date\n\nFinal invariants are honored:\n- Default run → `run.agent.fabro_tools == false`, JWT scope is just `run:worker`, `FABRO_WORKER_AGENT_RUN_TOOLS` is absent from the worker env, and `StartServices.fabro_run_tools == None`.\n- Opted-in run → `run.agent.fabro_tools == true`, JWT scope is `run:worker agent:run_tools`, `FABRO_WORKER_AGENT_RUN_TOOLS == \"true\"` is set in the worker env, and the env-gated branch in `runner.rs` builds `FabroRunToolServices`." + }, + "notes": "Stage completed: implement", + "usage": { + "input": { + "usage": { + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "tokens": { + "input_tokens": 114351, + "output_tokens": 19745, + "reasoning_tokens": 0, + "cache_read_tokens": 6127529, + "cache_write_tokens": 122658 + } + }, + "facts": { + "algorithm": "anthropic", + "cache_write_5m_tokens": 122658, + "cache_write_1h_tokens": 0 + } + }, + "total_usd_micros": 4895756 + }, + "files_touched": [ + "/home/daytona/workspace/fabro/lib/crates/fabro-cli/src/commands/run/runner.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-server/src/server/tests.rs", + "/home/daytona/workspace/fabro/lib/crates/fabro-static/src/env_vars.rs" + ] + }, "start": { "status": "succeeded", "usage": null @@ -747,12 +872,13 @@ "usage": null } }, - "next_node_id": "implement", + "next_node_id": "simplify_opus", "node_visits": { "start": 1, "toolchain": 1, "preflight_compile": 1, - "preflight_lint": 1 + "preflight_lint": 1, + "implement": 1 } }, "diff": {} @@ -817,7 +943,12 @@ "first_event_seq": 40, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "failure_reason": null, + "timestamp": "2026-05-22T13:54:33.611733Z" + }, "provider_used": null, "diff": null, "script_invocation": { @@ -825,11 +956,27 @@ "command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", "language": "shell" }, - "script_timing": null, + "script_timing": { + "output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "exit_code": 0, + "duration_ms": 137152, + "termination": "exited", + "output_bytes": 0, + "live_streaming": false + }, "parallel_results": null, "output": null, + "output_bytes": 0, + "live_streaming": false, + "termination": "exited", "started_at": "2026-05-22T13:52:16.449629Z", "handler": "command", + "timing": { + "wall_time_ms": 137161, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 0, "output_tokens": 0, @@ -838,7 +985,7 @@ "cache_read_tokens": 0, "cache_write_tokens": 0 }, - "state": "running" + "state": "succeeded" }, "toolchain@1": { "first_event_seq": 20, @@ -935,6 +1082,38 @@ "cache_write_tokens": 0 }, "state": "succeeded" + }, + "implement@1": { + "first_event_seq": 50, + "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-22T13:54:37.714080Z", + "handler": "agent", + "usage": { + "input_tokens": 114351, + "output_tokens": 19745, + "total_tokens": 6384283, + "reasoning_tokens": 0, + "cache_read_tokens": 6127529, + "cache_write_tokens": 122658, + "total_usd_micros": 4895756 + }, + "model": { + "provider": "anthropic", + "model_id": "claude-opus-4-7" + }, + "state": "running" } } } \ No newline at end of file diff --git a/stages/004-preflight_lint@1/output.log b/stages/004-preflight_lint@1/output.log new file mode 100644 index 000000000..d87ba9545 --- /dev/null +++ b/stages/004-preflight_lint@1/output.log @@ -0,0 +1 @@ +blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126 \ No newline at end of file diff --git a/stages/004-preflight_lint@1/script_timing.json b/stages/004-preflight_lint@1/script_timing.json new file mode 100644 index 000000000..822a8a69a --- /dev/null +++ b/stages/004-preflight_lint@1/script_timing.json @@ -0,0 +1,8 @@ +{ + "output": "blob://sha256/12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "exit_code": 0, + "duration_ms": 137152, + "termination": "exited", + "output_bytes": 0, + "live_streaming": false +} \ No newline at end of file diff --git a/stages/004-preflight_lint@1/status.json b/stages/004-preflight_lint@1/status.json new file mode 100644 index 000000000..8f9b0b34c --- /dev/null +++ b/stages/004-preflight_lint@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Script completed: cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", + "failure_reason": null, + "timestamp": "2026-05-22T13:54:33.611733Z" +} \ No newline at end of file diff --git a/stages/005-implement@1/prompt.md b/stages/005-implement@1/prompt.md new file mode 100644 index 000000000..3a3f64f5d --- /dev/null +++ b/stages/005-implement@1/prompt.md @@ -0,0 +1,707 @@ +Goal: # Run Agent Fabro Tools Opt-In 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 `[run.agent] fabro_tools = true/false`, defaulting to `false`, so workflow agents only get Fabro run tools and the `agent:run_tools` worker JWT scope when a run opts in. + +**Architecture:** Treat `run.agent.fabro_tools` as the source of truth in resolved run settings. The server reads the effective run setting before spawning `__run-worker`, issues the worker token with or without `agent:run_tools`, and passes a private worker env flag so the CLI worker registers Fabro run tools only for opted-in runs. Server-side JWT scope checks remain the authorization backstop. + +**Tech Stack:** Rust, Serde TOML config layers, Fabro worker JWT scopes, Tokio subprocess spawning, `cargo nextest`. + +--- + +## File Map + +- Modify `lib/crates/fabro-types/src/settings/run.rs`: add the resolved `RunAgentSettings::fabro_tools` boolean. +- Modify `lib/crates/fabro-config/src/layers/run.rs`: add the optional layered `[run.agent] fabro_tools` field and options metadata. +- Modify `lib/crates/fabro-config/src/resolve/run.rs`: resolve missing config to `false`. +- Modify `lib/crates/fabro-config/src/tests/resolve_run.rs`: cover default, true, false, and layer override behavior. +- Modify `lib/crates/fabro-static/src/env_vars.rs`: add a typed internal worker env var name. +- Modify `lib/crates/fabro-server/src/worker_token.rs`: make `WorkerScopeSet::run_worker()` available to production code. +- Modify `lib/crates/fabro-server/src/server.rs`: compute the opt-in flag from the run spec, choose worker JWT scopes, and pass the worker env flag. +- Modify `lib/crates/fabro-server/src/server/tests.rs`: update worker command tests for default and opted-in scope/env behavior. +- Modify `lib/crates/fabro-cli/src/commands/run/runner.rs`: gate `FabroRunToolServices` construction on the worker env flag and add unit coverage for the env parser. +- Modify docs generator/reference docs: `lib/crates/fabro-dev/src/commands/docs_options_reference.rs`, `docs/public/reference/user-configuration.mdx`, and `docs/public/execution/run-configuration.mdx`. + +--- + +### Task 1: Add Resolved Run Config + +**Files:** +- Modify: `lib/crates/fabro-types/src/settings/run.rs` +- Modify: `lib/crates/fabro-config/src/layers/run.rs` +- Modify: `lib/crates/fabro-config/src/resolve/run.rs` +- Test: `lib/crates/fabro-config/src/tests/resolve_run.rs` + +- [ ] **Step 1: Write config resolver tests first** + +Add a `run_agent_fabro_tools` test module to `lib/crates/fabro-config/src/tests/resolve_run.rs` near the existing run settings tests. + +```rust +mod run_agent_fabro_tools { + use crate::layers::Combine; + use crate::{SettingsLayer, WorkflowSettingsBuilder}; + + fn parse_settings(source: &str) -> SettingsLayer { + source + .parse::() + .expect("fixture should parse via SettingsLayer") + } + + #[test] + fn defaults_to_false_when_run_agent_is_absent() { + let settings = WorkflowSettingsBuilder::from_layer(&SettingsLayer::default()) + .expect("empty settings should resolve") + .run; + + assert!(!settings.agent.fabro_tools); + } + + #[test] + fn resolves_true_from_run_agent_table() { + let settings = WorkflowSettingsBuilder::from_toml( + r#" +_version = 1 + +[run.agent] +fabro_tools = true +"#, + ) + .expect("run.agent.fabro_tools should resolve"); + + assert!(settings.run.agent.fabro_tools); + } + + #[test] + fn resolves_explicit_false_from_run_agent_table() { + let settings = WorkflowSettingsBuilder::from_toml( + r#" +_version = 1 + +[run.agent] +fabro_tools = false +"#, + ) + .expect("run.agent.fabro_tools false should resolve"); + + assert!(!settings.run.agent.fabro_tools); + } + + #[test] + fn higher_layer_false_overrides_lower_true() { + let workflow = parse_settings( + r#" +_version = 1 + +[run.agent] +fabro_tools = false +"#, + ); + let user = parse_settings( + r#" +_version = 1 + +[run.agent] +fabro_tools = true +"#, + ); + let merged = workflow.combine(user); + + let settings = WorkflowSettingsBuilder::from_layer(&merged) + .expect("merged settings should resolve") + .run; + + assert!(!settings.agent.fabro_tools); + } +} +``` + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo nextest run -p fabro-config run_agent_fabro_tools +``` + +Expected: compile failure mentioning `fabro_tools` is not a field, or parse failure saying `fabro_tools` is unknown. + +- [ ] **Step 3: Add the resolved setting** + +Update `RunAgentSettings` in `lib/crates/fabro-types/src/settings/run.rs`: + +```rust +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct RunAgentSettings { + pub fabro_tools: bool, + pub permissions: Option, + pub mcps: HashMap, +} +``` + +- [ ] **Step 4: Add the layered TOML field** + +Update `RunAgentLayer` in `lib/crates/fabro-config/src/layers/run.rs`: + +```rust +/// `[run.agent]` — agent knobs only (Fabro tools, permissions, MCPs). +#[derive( + Debug, + Clone, + Default, + PartialEq, + Serialize, + Deserialize, + fabro_macros::Combine, + fabro_macros::OptionsMetadata, +)] +#[serde(deny_unknown_fields)] +pub struct RunAgentLayer { + /// Allow workflow agents to use Fabro run-management tools. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[option(default = "false", value_type = "boolean")] + pub fabro_tools: Option, + + /// Default tool permission level for workflow agents. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[option( + default = "\"read-write\"", + value_type = "\"read-only\" | \"read-write\" | \"full\"" + )] + pub permissions: Option, + + /// Agent-scoped MCP server entries, keyed by name. + #[serde(default, skip_serializing_if = "StickyMap::is_empty")] + #[option(value_type = "table")] + pub mcps: StickyMap, +} +``` + +- [ ] **Step 5: Resolve the setting** + +Update `resolve_agent` in `lib/crates/fabro-config/src/resolve/run.rs`: + +```rust +fn resolve_agent(agent: Option<&RunAgentLayer>) -> RunAgentSettings { + let Some(agent) = agent else { + return RunAgentSettings::default(); + }; + + RunAgentSettings { + fabro_tools: agent.fabro_tools.unwrap_or(false), + permissions: agent.permissions, + mcps: agent + .mcps + .iter() + .map(|(name, entry)| (name.clone(), resolve_mcp_entry(name, entry))) + .collect(), + } +} +``` + +- [ ] **Step 6: Run config tests** + +Run: + +```bash +cargo nextest run -p fabro-config run_agent_fabro_tools +``` + +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add lib/crates/fabro-types/src/settings/run.rs lib/crates/fabro-config/src/layers/run.rs lib/crates/fabro-config/src/resolve/run.rs lib/crates/fabro-config/src/tests/resolve_run.rs +git commit -m "feat: add run agent fabro tools setting" +``` + +--- + +### Task 2: Gate Worker JWT Scope and Worker Env + +**Files:** +- Modify: `lib/crates/fabro-static/src/env_vars.rs` +- Modify: `lib/crates/fabro-server/src/worker_token.rs` +- Modify: `lib/crates/fabro-server/src/server.rs` +- Test: `lib/crates/fabro-server/src/server/tests.rs` + +- [ ] **Step 1: Write server tests for default and opted-in runs** + +Update `worker_command_always_sets_worker_token_env` in `lib/crates/fabro-server/src/server/tests.rs` so the default case expects only `run:worker`. Add a second test that passes `agent_fabro_tools_enabled = true` and expects both scopes plus the worker env flag. + +```rust +#[cfg(unix)] +#[test] +fn worker_command_default_token_omits_agent_run_tools_scope() { + let storage_dir = tempfile::tempdir().unwrap(); + let state = worker_command_test_state(storage_dir.path(), &["dev-token"], Some(TEST_DEV_TOKEN)); + let run_id = RunId::new(); + + let cmd = worker_command( + state.as_ref(), + run_id, + RunExecutionMode::Start, + storage_dir.path(), + false, + ) + .unwrap(); + + let EnvOverride::Set(token) = command_env_value(&cmd, EnvVars::FABRO_WORKER_TOKEN) else { + panic!("worker token env should be set"); + }; + let claims = jsonwebtoken::decode::( + &token, + state.worker_token_keys().decoding_key(), + state.worker_token_keys().validation(), + ) + .expect("worker token should decode") + .claims; + + assert_eq!(claims.scope.split_whitespace().collect::>(), vec!["run:worker"]); + assert_eq!( + command_env_value(&cmd, EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS), + EnvOverride::Removed + ); +} + +#[cfg(unix)] +#[test] +fn worker_command_opt_in_token_includes_agent_run_tools_scope() { + let storage_dir = tempfile::tempdir().unwrap(); + let state = worker_command_test_state(storage_dir.path(), &["dev-token"], Some(TEST_DEV_TOKEN)); + let run_id = RunId::new(); + + let cmd = worker_command( + state.as_ref(), + run_id, + RunExecutionMode::Start, + storage_dir.path(), + true, + ) + .unwrap(); + + let EnvOverride::Set(token) = command_env_value(&cmd, EnvVars::FABRO_WORKER_TOKEN) else { + panic!("worker token env should be set"); + }; + let claims = jsonwebtoken::decode::( + &token, + state.worker_token_keys().decoding_key(), + state.worker_token_keys().validation(), + ) + .expect("worker token should decode") + .claims; + + assert_eq!( + claims.scope.split_whitespace().collect::>(), + vec!["run:worker", "agent:run_tools"] + ); + assert_eq!( + command_env_value(&cmd, EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS), + EnvOverride::Set("true".to_string()) + ); +} +``` + +- [ ] **Step 2: Run the server tests and confirm they fail** + +Run: + +```bash +cargo nextest run -p fabro-server worker_command_ +``` + +Expected: compile failure because `FABRO_WORKER_AGENT_RUN_TOOLS` and the new `worker_command` argument do not exist. + +- [ ] **Step 3: Add the internal env var constant** + +Update `lib/crates/fabro-static/src/env_vars.rs` near `FABRO_WORKER_TOKEN`: + +```rust +pub const FABRO_WORKER_AGENT_RUN_TOOLS: &'static str = "FABRO_WORKER_AGENT_RUN_TOOLS"; +pub const FABRO_WORKER_TOKEN: &'static str = "FABRO_WORKER_TOKEN"; +``` + +Update the EnvVars tests in the same file so the new constant is included in the alphabetized/core variable expectations. + +- [ ] **Step 4: Make the base worker scope constructor available** + +Update `lib/crates/fabro-server/src/worker_token.rs`: + +```rust +impl WorkerScopeSet { + #[must_use] + pub(crate) const fn run_worker() -> Self { + Self { + agent_run_tools: false, + } + } + + #[must_use] + pub(crate) const fn run_worker_with_agent_run_tools() -> Self { + Self { + agent_run_tools: true, + } + } +} +``` + +Remove only the `#[cfg(test)]` attribute from `run_worker`; leave the existing tests intact. + +- [ ] **Step 5: Add the worker command parameter and choose scopes** + +Update `worker_command` in `lib/crates/fabro-server/src/server.rs`: + +```rust +fn worker_command( + state: &AppState, + run_id: RunId, + mode: RunExecutionMode, + run_dir: &std::path::Path, + agent_fabro_tools_enabled: bool, +) -> anyhow::Result { + // existing setup... + let scopes = if agent_fabro_tools_enabled { + WorkerScopeSet::run_worker_with_agent_run_tools() + } else { + WorkerScopeSet::run_worker() + }; + let worker_token = issue_worker_token_with_scopes(state.worker_token_keys(), &run_id, scopes) + .map_err(|_| anyhow::anyhow!("failed to sign worker token"))?; + + // existing Command construction... + cmd.env_remove(EnvVars::FABRO_WORKER_TOKEN); + cmd.env(EnvVars::FABRO_WORKER_TOKEN, worker_token); + cmd.env_remove(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS); + if agent_fabro_tools_enabled { + cmd.env(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS, "true"); + } + // existing GitHub key forwarding... +} +``` + +Update all `worker_command(...)` test call sites to pass `false` unless the test is explicitly about Fabro tool opt-in. + +- [ ] **Step 6: Load the effective run setting before spawning the worker** + +Update `execute_run_subprocess` in `lib/crates/fabro-server/src/server.rs` after `open_run` succeeds and before `spawn_blocking`: + +```rust +let run_state = match run_store.state().await { + Ok(run_state) => run_state, + Err(err) => { + tracing::error!(run_id = %run_id, error = %err, "Failed to load run state"); + fail_managed_run( + &state, + run_id, + FailureReason::WorkflowError, + format!("Failed to load run state: {err}"), + ); + state.scheduler_notify.notify_one(); + return; + } +}; +let agent_fabro_tools_enabled = run_state.spec.settings.run.agent.fabro_tools; +``` + +Pass the boolean into `worker_command` inside the existing `spawn_blocking` closure: + +```rust +worker_command( + state_for_build.as_ref(), + run_id, + execution_mode, + &run_dir_for_build, + agent_fabro_tools_enabled, +) +``` + +- [ ] **Step 7: Run server tests** + +Run: + +```bash +cargo nextest run -p fabro-server worker_command +``` + +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add lib/crates/fabro-static/src/env_vars.rs lib/crates/fabro-server/src/worker_token.rs lib/crates/fabro-server/src/server.rs lib/crates/fabro-server/src/server/tests.rs +git commit -m "feat: gate worker run tool scope by run setting" +``` + +--- + +### Task 3: Gate CLI Worker Tool Registration + +**Files:** +- Modify: `lib/crates/fabro-cli/src/commands/run/runner.rs` +- Test: `lib/crates/fabro-cli/src/commands/run/runner.rs` + +- [ ] **Step 1: Add a focused test for the env gate** + +Add a `#[cfg(test)]` module at the bottom of `lib/crates/fabro-cli/src/commands/run/runner.rs`: + +```rust +#[cfg(test)] +mod tests { + use super::fabro_run_tools_enabled_from_env; + + #[test] + fn fabro_run_tools_enabled_env_requires_true() { + assert!(!fabro_run_tools_enabled_from_env(None)); + assert!(!fabro_run_tools_enabled_from_env(Some(""))); + assert!(!fabro_run_tools_enabled_from_env(Some("false"))); + assert!(!fabro_run_tools_enabled_from_env(Some("1"))); + assert!(fabro_run_tools_enabled_from_env(Some("true"))); + } +} +``` + +- [ ] **Step 2: Run the new test and confirm it fails** + +Run: + +```bash +cargo nextest run -p fabro-cli fabro_run_tools_enabled_env_requires_true +``` + +Expected: compile failure because `fabro_run_tools_enabled_from_env` does not exist. + +- [ ] **Step 3: Add the env parsing helper** + +Add this helper near `build_fabro_run_tool_services` in `lib/crates/fabro-cli/src/commands/run/runner.rs`: + +```rust +fn fabro_run_tools_enabled_from_env(value: Option<&str>) -> bool { + value == Some("true") +} +``` + +- [ ] **Step 4: Gate service construction in worker startup** + +Replace the unconditional `build_fabro_run_tool_services(...)` call in `execute` with: + +```rust +let fabro_run_tools = if fabro_run_tools_enabled_from_env(process_env_var( + EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS, +).as_deref()) { + build_fabro_run_tool_services( + worker_token, + client.clone_for_reuse(), + run_id, + run_spec.source_directory.as_deref(), + &run_dir, + Arc::clone(&catalog), + ) +} else { + None +}; +``` + +Keep `build_fabro_run_tool_services` returning `None` for an empty token. That keeps token presence as a second local guard. + +- [ ] **Step 5: Run CLI tests** + +Run: + +```bash +cargo nextest run -p fabro-cli fabro_run_tools_enabled_env_requires_true +cargo nextest run -p fabro-cli --test it runner +``` + +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add lib/crates/fabro-cli/src/commands/run/runner.rs +git commit -m "feat: register fabro run tools only when opted in" +``` + +--- + +### Task 4: Update Docs and Generated Reference Text + +**Files:** +- Modify: `lib/crates/fabro-dev/src/commands/docs_options_reference.rs` +- Modify: `docs/public/reference/user-configuration.mdx` +- Modify: `docs/public/execution/run-configuration.mdx` + +- [ ] **Step 1: Update the docs generator sample** + +Update the `[run.agent]` sample in `docs_options_reference.rs`: + +```rust +Section::of::( + "[run.agent]", + r#"[run.agent] +fabro_tools = true +permissions = "read-write""#, +), +``` + +- [ ] **Step 2: Update generated/reference docs** + +In `docs/public/reference/user-configuration.mdx`, update the `[run.agent]` description, example, and options table: + +```mdx +## `[run.agent]` + +`[run.agent]` — agent knobs only (Fabro tools, permissions, MCPs) + +```toml +[run.agent] +fabro_tools = true +permissions = "read-write" +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `fabro_tools` | boolean | false | Allow workflow agents to use Fabro run-management tools. | +| `mcps` | table | None | Agent-scoped MCP server entries, keyed by name. | +| `permissions` | "read-only" \| "read-write" \| "full" | "read-write" | Default tool permission level for workflow agents. | +``` + +- [ ] **Step 3: Add user-facing run configuration docs** + +In `docs/public/execution/run-configuration.mdx`, add a short section before the existing `[run.agent.mcps]` section: + +```mdx +### `[run.agent]` + +Configure workflow agent behavior that is not tied to a single stage. + +```toml +[run.agent] +fabro_tools = true +``` + +`fabro_tools` defaults to `false`. Set it to `true` only for runs whose agents should be able to create, search, inspect, and interact with Fabro runs through the built-in Fabro run tools. This setting is separate from normal agent `permissions` and from MCP server configuration. +``` + +- [ ] **Step 4: Run docs/reference checks** + +Run: + +```bash +cargo dev docs check +``` + +Expected before regenerating docs: FAIL with `docs/public/reference/user-configuration.mdx is stale; run cargo dev docs refresh`. + +Then run: + +```bash +cargo dev docs refresh +cargo dev docs check +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add lib/crates/fabro-dev/src/commands/docs_options_reference.rs docs/public/reference/user-configuration.mdx docs/public/execution/run-configuration.mdx +git commit -m "docs: document run agent fabro tools opt in" +``` + +--- + +### Task 5: Full Verification + +**Files:** +- No source edits unless verification finds a defect. + +- [ ] **Step 1: Run targeted package tests** + +Run: + +```bash +cargo nextest run -p fabro-config +cargo nextest run -p fabro-server +cargo nextest run -p fabro-cli +``` + +Expected: all PASS. + +- [ ] **Step 2: Run formatting check** + +Run: + +```bash +cargo +nightly-2026-04-14 fmt --check --all +``` + +Expected: PASS. + +- [ ] **Step 3: Run clippy for touched Rust crates** + +Run: + +```bash +cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-config -p fabro-static -p fabro-server -p fabro-cli -p fabro-dev --all-targets -- -D warnings +``` + +Expected: PASS. + +- [ ] **Step 4: Manually review behavior** + +Confirm these invariants in the final diff: + +```text +Default run: +- resolved run.agent.fabro_tools == false +- worker JWT scope == "run:worker" +- FABRO_WORKER_AGENT_RUN_TOOLS is absent from worker env +- StartServices.fabro_run_tools == None + +Opted-in run: +- resolved run.agent.fabro_tools == true +- worker JWT scope == "run:worker agent:run_tools" +- FABRO_WORKER_AGENT_RUN_TOOLS == "true" +- StartServices.fabro_run_tools is Some(...) +``` + +- [ ] **Step 5: Commit verification fixes** + +When verification changes files, inspect the exact paths and commit them: + +```bash +git status --short +git add -u +git commit -m "test: cover run agent fabro tools opt in" +``` + +--- + +## Assumptions and Defaults + +- `fabro_tools` is a per-run opt-in setting only; this plan does not add a separate server-wide allow/deny policy. +- Defaulting to `false` intentionally changes existing behavior: runs that need Fabro run tools must set `[run.agent] fabro_tools = true`. +- `run.agent.permissions` remains about ordinary agent tool permissions and does not imply Fabro API access. +- `[run.agent.mcps]` remains independent; MCP tools are not enabled or disabled by `fabro_tools`. +- `fabro mcp start` and standalone MCP exposure of Fabro tools are out of scope. +- The private worker env var uses the exact string `"true"` as the only enabling value, so accidental values such as `"1"` or `"yes"` do not grant tools. +- The hidden `__run-worker` CLI argument contract should not grow; use the env var rather than a new hidden CLI flag. + + +## Completed stages +- **toolchain**: succeeded + - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1` + - Output: + ``` + cargo 1.95.0 (f2d3ce0bd 2026-03-21) + ``` +- **preflight_compile**: succeeded + - Script: `cargo check -q --workspace 2>&1` + - Output: (empty) +- **preflight_lint**: succeeded + - Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1` + - Output: (empty) + + +Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD. \ No newline at end of file diff --git a/stages/005-implement@1/provider_used.json b/stages/005-implement@1/provider_used.json new file mode 100644 index 000000000..672cc50e5 --- /dev/null +++ b/stages/005-implement@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/005-implement@1/response.md b/stages/005-implement@1/response.md new file mode 100644 index 000000000..4c59fb343 --- /dev/null +++ b/stages/005-implement@1/response.md @@ -0,0 +1,28 @@ +All changes are in place. Summary of what was done: + +**Task 1** — Already completed in prior commits (types/layers/resolver + tests). + +**Task 2** — Completed the remaining gaps: +- Added `FABRO_WORKER_AGENT_RUN_TOOLS` constant to `fabro-static/src/env_vars.rs` (including the alphabetized test list). +- Updated `worker_command` in `fabro-server/src/server.rs` to clear the env var and conditionally set it to `"true"` when `agent_fabro_tools_enabled` is set. +- Extended both server tests (`worker_command_default_token_omits_agent_run_tools_scope` and `worker_command_opt_in_token_includes_agent_run_tools_scope`) to assert the env var behavior. The default-case assertion uses `EnvOverride::Unchanged` rather than `Removed` because `apply_worker_env` calls `Command::env_clear()` first, which makes a subsequent `env_remove` a true no-op in `get_envs()` — the conceptual invariant ("absent from worker env") still holds. + +**Task 3** — Switched the CLI worker to use the env var as the gate: +- Added `use fabro_static::EnvVars;` import. +- Replaced `if run_spec.settings.run.agent.fabro_tools` with `if fabro_run_tools_enabled_from_env(process_env_var(EnvVars::FABRO_WORKER_AGENT_RUN_TOOLS).as_deref())`. +- Added the `fabro_run_tools_enabled_from_env` helper that only treats the exact string `"true"` as enabled. +- Added the `fabro_run_tools_enabled_env_requires_true` unit test in the existing `mod tests`. + +**Task 4** — Docs already updated in prior commits. + +**Task 5** — Verification: +- `cargo nextest run -p fabro-config` (197 tests pass) +- `cargo nextest run -p fabro-server` (583 tests pass) +- `cargo nextest run -p fabro-cli` (906 tests pass) +- `cargo +nightly-2026-04-14 fmt --check --all` passes +- `cargo +nightly-2026-04-14 clippy -p fabro-types -p fabro-config -p fabro-static -p fabro-server -p fabro-cli -p fabro-dev --all-targets -- -D warnings` passes +- `cargo dev docs check` reports docs up to date + +Final invariants are honored: +- Default run → `run.agent.fabro_tools == false`, JWT scope is just `run:worker`, `FABRO_WORKER_AGENT_RUN_TOOLS` is absent from the worker env, and `StartServices.fabro_run_tools == None`. +- Opted-in run → `run.agent.fabro_tools == true`, JWT scope is `run:worker agent:run_tools`, `FABRO_WORKER_AGENT_RUN_TOOLS == "true"` is set in the worker env, and the env-gated branch in `runner.rs` builds `FabroRunToolServices`. \ No newline at end of file