mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
## Summary
Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.
## What changed
**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.
**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.
**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).
**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.
**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.
**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.
**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.
### Plan Summary
- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.
### Fabro Details
<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, 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", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="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.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
|
||
|---|---|---|
| .. | ||
| src | ||
| tests/it | ||
| Cargo.toml | ||
| README.md | ||
agent
A programmable agentic loop for building coding agents. This crate provides the core session management, tool execution, and LLM interaction loop used to power interactive coding assistants.
Architecture
The crate is organized around a central Session that drives an agentic loop:
- User input is appended to a conversation
History - The session builds a
Requestwith system prompt, history, and tools - An LLM generates a response (text and/or tool calls) via
unified-llm - Tool calls are executed through a
ToolRegistryagainst aSandbox - Results are recorded and the loop continues until the LLM responds with text only (natural completion), a turn limit is reached, or the session is interrupted
User Input
|
v
[Session::process_input]
|
v
+-------------------+
| Build Request | <-- system prompt + history + tools
+-------------------+
|
v
+-------------------+
| LLM Call | <-- via unified-llm Client
+-------------------+
|
v
+-------------------+ +-------------------+
| Tool Calls? -----+-yes-| Execute Tools |
+-------------------+ | (parallel or seq) |
| no +-------------------+
v |
[Done] +---> loop back to Build Request
Key Components
Session-- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, interrupt handling, and event emission.AgentProfile(trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships withAnthropicProfile,OpenAiProfile, andGeminiProfile.Sandbox(trait) -- Abstracts filesystem, shell, grep, and glob operations.LocalSandboxprovides a real implementation; the trait enables sandboxing and testing.ToolRegistry-- Maps tool names to definitions and async executor functions. Tools are registered per-profile.History-- Ordered list ofTurnvariants (User,Assistant,ToolResults,System,Steering) that converts to LLM messages.Emitter-- BroadcastsSessionEvents (tool calls, text, errors, warnings) over atokio::sync::broadcastchannel for UI or logging.SubAgentManager-- Spawns childSessions on background tasks for delegated work, with depth limits.SessionConfig-- Tunable parameters: max turns, tool round limits, command timeouts, loop detection, output truncation limits, and user instructions.
Key Types and Traits
Session
The main entry point. Created with an LLM client, a provider profile, a sandbox, and a config.
AgentProfile
pub trait AgentProfile: Send + Sync {
fn id(&self) -> String;
fn model(&self) -> String;
fn tool_registry(&self) -> &ToolRegistry;
fn build_system_prompt(
&self,
env: &dyn Sandbox,
env_context: &EnvContext,
project_docs: &[String],
user_instructions: Option<&str>,
) -> String;
// ... default methods for tools(), knowledge_cutoff(), context_window_size()
}
Built-in profiles:
AnthropicProfile-- 200K context, extended thinking beta headers, tools:read_file,write_file,edit_file,shell,grep,globOpenAiProfile-- 128K context, reasoning effort support, tools:read_file,write_file,shell,grep,glob,apply_patch(Codex apply_patch format)GeminiProfile-- 1M context, safety settings, tools: all Anthropic tools plusread_many_files,list_dir,web_search,web_fetch
Sandbox
pub trait Sandbox: Send + Sync {
async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>, String>;
async fn read_file_text(&self, path: &str) -> Result<String, String>;
async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String>; // line-numbered display
async fn write_file(&self, path: &str, content: &str) -> Result<(), String>;
async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result<ExecResult, String>;
async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result<Vec<String>, String>;
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String>;
// ... plus delete_file, file_exists, list_directory, initialize, cleanup, platform info
}
LocalSandbox is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback.
SessionConfig
pub struct SessionConfig {
pub max_turns: usize, // 0 = unlimited
pub max_tool_rounds_per_input: usize, // default: 200
pub default_command_timeout_ms: u64, // default: 10s
pub max_command_timeout_ms: u64, // default: 600s
pub enable_loop_detection: bool, // default: true
pub loop_detection_window: usize, // default: 10
pub max_subagent_depth: usize, // default: 1
pub user_instructions: Option<String>,
pub reasoning_effort: Option<String>,
// ... plus tool_output_limits, tool_line_limits, git_root
}
Usage
use agent::{
AnthropicProfile, LocalSandbox, Session, SessionConfig,
};
use std::path::PathBuf;
use std::sync::Arc;
use unified_llm::client::Client;
// 1. Create an LLM client (via unified-llm)
let client: Client = /* configure unified-llm client */;
// 2. Choose a provider profile
let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-20250514"));
// 3. Create a sandbox
let env = Arc::new(LocalSandbox::new(
PathBuf::from("/path/to/project"),
));
// 4. Configure the session
let config = SessionConfig {
max_tool_rounds_per_input: 50,
enable_loop_detection: true,
user_instructions: Some("Always write tests first".into()),
..SessionConfig::default()
};
// 5. Create and initialize the session
let mut session = Session::new(client, profile, env, config, None);
session.initialize().await?;
// 6. Subscribe to events (for UI rendering)
let mut rx = session.subscribe();
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
// Handle SessionEvent: tool calls, text, errors, etc.
}
});
// 7. Process user input
session.process_input("Fix the failing test in src/lib.rs").await?;
Steering and Follow-ups
Inject guidance mid-conversation or queue follow-up messages:
// Inject a steering message before the next LLM call
session.steer("Focus on the root cause, not symptoms".into());
// Queue a follow-up that runs after the current input completes
session.follow_up("Now run the test suite to verify".into());
Interrupt
Cancel a running session from another thread:
let cancel_token = session.cancel_token();
// From another task:
cancel_token.cancel();
Custom Tools
Register additional tools via the profile's ToolRegistry:
use agent::tool_registry::{RegisteredTool, ToolExecutor};
use unified_llm::types::ToolDefinition;
use std::sync::Arc;
let custom_tool = RegisteredTool {
definition: ToolDefinition {
name: "my_tool".into(),
description: "Does something useful".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"input": {"type": "string"}
},
"required": ["input"]
}),
},
executor: Arc::new(|args, env| {
Box::pin(async move {
let input = args["input"].as_str().unwrap_or("");
Ok(format!("Processed: {input}"))
})
}),
};
// Register on a mutable profile before creating the session
profile.tool_registry_mut().register(custom_tool);
Subagents
Spawn child sessions for delegated tasks:
use agent::subagent::SubAgentManager;
let mut profile = AnthropicProfile::new("claude-sonnet-4-20250514");
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3)));
let factory = Arc::new(|| { /* create a new Session */ });
// Registers spawn_agent, send_input, wait, close_agent tools
profile.register_subagent_tools(manager, factory, 0);
Safety Features
- Loop detection -- Detects repeating tool call patterns (period 1, 2, or 3) and injects a steering warning
- Context window monitoring -- Emits
Warningevents (kind"context_window") when estimated usage exceeds 80% - Tool argument validation -- Validates arguments against JSON Schema before execution
- Tool output truncation -- Per-tool character and line limits with head/tail or tail-only truncation modes
- Environment variable filtering --
LocalSandboxstrips secrets (*_API_KEY,*_SECRET,*_TOKEN,*_PASSWORD,*_CREDENTIAL) from subprocess environments - Command timeouts -- Configurable per-command with process group cleanup (SIGTERM then SIGKILL)
- Project doc discovery -- Automatically discovers
AGENTS.md,CLAUDE.md,GEMINI.md, or.codex/instructions.mdbased on provider, with a 32KB budget