diff --git a/lib/crates/fabro-config/src/project.rs b/lib/crates/fabro-config/src/project.rs index 677902ede..eb7e569a6 100644 --- a/lib/crates/fabro-config/src/project.rs +++ b/lib/crates/fabro-config/src/project.rs @@ -12,8 +12,7 @@ use serde::Serialize; use crate::config::ConfigLayer; use crate::run; -pub use fabro_types::settings::project::ProjectSettings; -use fabro_types::settings::v2::{InterpString, SettingsFile}; +use fabro_types::settings::{InterpString, SettingsFile}; const CONFIG_FILENAME: &str = "fabro.toml"; const RUN_GRAPH_FILE: &str = "workflow.fabro"; diff --git a/lib/crates/fabro-hooks/src/config.rs b/lib/crates/fabro-hooks/src/config.rs index 8d055984f..09ee98960 100644 --- a/lib/crates/fabro-hooks/src/config.rs +++ b/lib/crates/fabro-hooks/src/config.rs @@ -1 +1,350 @@ -pub use fabro_types::settings::hook::{HookDefinition, HookEvent, HookSettings, HookType, TlsMode}; +//! Hook configuration runtime types. +//! +//! These types are the runtime shape that the hook executor consumes. The +//! v2 parse tree under `fabro_types::settings::v2::run::HookEntry` is the +//! *config-file* shape; this module lives in `fabro-hooks` because the +//! behavior methods (`is_blocking`, `timeout`, `resolved_hook_type`, +//! `runs_in_sandbox`, `effective_name`) are runtime concerns owned by the +//! executor. +//! +//! [`bridge_hook`] converts a v2 `HookEntry` into the runtime +//! [`HookDefinition`] and lives here (not in `fabro-types`) so the runtime +//! shape stays owned by this crate. + +use std::borrow::Cow; + +use fabro_types::settings::v2::InterpString; +use fabro_types::settings::v2::run::{ + HookAgentMarker, HookEntry, HookEvent as V2HookEvent, HookTlsMode as V2HookTlsMode, +}; +use serde::{Deserialize, Serialize}; + +/// Lifecycle events that can trigger user-defined hooks. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HookEvent { + RunStart, + RunComplete, + RunFailed, + StageStart, + StageComplete, + StageFailed, + StageRetrying, + EdgeSelected, + ParallelStart, + ParallelComplete, + /// Reserved: hooks for this event are not yet invoked by the engine. + SandboxReady, + /// Reserved: hooks for this event are not yet invoked by the engine. + SandboxCleanup, + CheckpointSaved, + PreToolUse, + PostToolUse, + PostToolUseFailure, +} + +impl HookEvent { + /// Whether hooks for this event block execution by default. + #[must_use] + pub fn is_blocking_by_default(self) -> bool { + matches!( + self, + Self::RunStart + | Self::StageStart + | Self::EdgeSelected + | Self::PreToolUse + | Self::SandboxReady + ) + } +} + +impl std::fmt::Display for HookEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::RunStart => "run_start", + Self::RunComplete => "run_complete", + Self::RunFailed => "run_failed", + Self::StageStart => "stage_start", + Self::StageComplete => "stage_complete", + Self::StageFailed => "stage_failed", + Self::StageRetrying => "stage_retrying", + Self::EdgeSelected => "edge_selected", + Self::ParallelStart => "parallel_start", + Self::ParallelComplete => "parallel_complete", + Self::SandboxReady => "sandbox_ready", + Self::SandboxCleanup => "sandbox_cleanup", + Self::CheckpointSaved => "checkpoint_saved", + Self::PreToolUse => "pre_tool_use", + Self::PostToolUse => "post_tool_use", + Self::PostToolUseFailure => "post_tool_use_failure", + }) + } +} + +/// TLS verification mode for HTTP hooks. +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TlsMode { + /// Require `https://` and verify certificates (default). + #[default] + Verify, + /// Require `https://` but skip certificate verification. + NoVerify, + /// Allow `http://`; skip certificate verification for `https://`. + Off, +} + +/// How a hook is executed. +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum HookType { + Command { + command: String, + }, + Http { + url: String, + headers: Option>, + #[serde(default)] + allowed_env_vars: Vec, + #[serde(default)] + tls: TlsMode, + }, + Prompt { + prompt: String, + model: Option, + }, + Agent { + prompt: String, + model: Option, + max_tool_rounds: Option, + }, +} + +/// A single hook definition. +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct HookDefinition { + pub name: Option, + pub event: HookEvent, + /// Inline command shorthand — if set, implies `type = "command"`. + #[serde(default)] + pub command: Option, + /// Explicit hook type (command or http). If omitted and `command` is set, + /// defaults to `Command`. + #[serde(flatten)] + pub hook_type: Option, + /// Regex matched against node_id, handler_type, or event-specific fields. + pub matcher: Option, + /// Override the event's default blocking behavior. + pub blocking: Option, + /// Timeout in milliseconds (default: 60_000). + pub timeout_ms: Option, + /// Run inside the sandbox (true, default) or on the host (false). + pub sandbox: Option, +} + +impl HookDefinition { + /// Resolve the effective hook type: explicit `hook_type` wins, then `command` + /// shorthand, then error. + pub fn resolved_hook_type(&self) -> Option> { + if let Some(ref ht) = self.hook_type { + return Some(Cow::Borrowed(ht)); + } + self.command.as_ref().map(|cmd| { + Cow::Owned(HookType::Command { + command: cmd.clone(), + }) + }) + } + + /// Whether this hook is blocking for its event. + #[must_use] + pub fn is_blocking(&self) -> bool { + self.blocking + .unwrap_or_else(|| self.event.is_blocking_by_default()) + } + + /// Timeout duration for this hook. + /// + /// Defaults: 30s for prompt hooks, 60s for all others. + #[must_use] + pub fn timeout(&self) -> std::time::Duration { + if let Some(ms) = self.timeout_ms { + return std::time::Duration::from_millis(ms); + } + let default_ms = match self.resolved_hook_type().as_deref() { + Some(HookType::Prompt { .. }) => 30_000, + _ => 60_000, + }; + std::time::Duration::from_millis(default_ms) + } + + /// Whether this hook runs in the sandbox. + #[must_use] + pub fn runs_in_sandbox(&self) -> bool { + self.sandbox.unwrap_or(true) + } + + /// The effective name: explicit name or a generated one. + #[must_use] + pub fn effective_name(&self) -> String { + if let Some(ref n) = self.name { + return n.clone(); + } + let event_str = self.event.to_string(); + match self.resolved_hook_type().as_deref() { + Some(HookType::Command { ref command }) => { + let short = &command[..command.floor_char_boundary(20)]; + format!("{event_str}:{short}") + } + Some(HookType::Http { ref url, .. }) => format!("{event_str}:{url}"), + Some(HookType::Prompt { ref prompt, .. } | HookType::Agent { ref prompt, .. }) => { + let short = &prompt[..prompt.floor_char_boundary(20)]; + format!("{event_str}:{short}") + } + None => event_str, + } + } +} + +/// Top-level hook configuration: a list of hook definitions. +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +pub struct HookSettings { + #[serde(default)] + pub hooks: Vec, +} + +impl HookSettings { + /// Merge with another config. Concatenates lists; on name collisions, `other` wins. + #[must_use] + pub fn merge(self, other: Self) -> Self { + let mut by_name: std::collections::HashMap = + std::collections::HashMap::new(); + let mut order: Vec = Vec::new(); + + for hook in self.hooks { + let name = hook.effective_name(); + if !by_name.contains_key(&name) { + order.push(name.clone()); + } + by_name.insert(name, hook); + } + for hook in other.hooks { + let name = hook.effective_name(); + if !by_name.contains_key(&name) { + order.push(name.clone()); + } + by_name.insert(name, hook); + } + + let hooks = order + .into_iter() + .filter_map(|name| by_name.remove(&name)) + .collect(); + + Self { hooks } + } +} + +/// Convert a v2 [`HookEntry`] into the runtime [`HookDefinition`] shape +/// this crate's executor consumes. +#[must_use] +pub fn bridge_hook(hook: &HookEntry) -> HookDefinition { + let hook_type = resolve_hook_type(hook); + // If the hook is a script/command form, emit via the shorthand so + // HookDefinition.command holds the full command and + // HookDefinition.hook_type stays None. This avoids the duplicate + // `command` key that would otherwise appear under `#[serde(flatten)]`. + let command = if let Some(script) = &hook.script { + Some(interp_to_string(script)) + } else { + hook.command.as_ref().map(|command| { + command + .iter() + .map(interp_to_string) + .collect::>() + .join(" ") + }) + }; + HookDefinition { + name: hook.name.clone().or_else(|| hook.id.clone()), + event: bridge_hook_event(hook.event), + command, + hook_type, + matcher: hook.matcher.clone(), + blocking: hook.blocking, + timeout_ms: hook + .timeout + .map(|d| u64::try_from(d.as_std().as_millis()).unwrap_or(u64::MAX)), + sandbox: hook.sandbox, + } +} + +fn resolve_hook_type(hook: &HookEntry) -> Option { + if hook.script.is_some() || hook.command.is_some() { + return None; + } + if let Some(url) = &hook.url { + let headers = if hook.headers.is_empty() { + None + } else { + Some( + hook.headers + .iter() + .map(|(k, v)| (k.clone(), interp_to_string(v))) + .collect(), + ) + }; + let tls = match hook.tls { + Some(V2HookTlsMode::Verify) => TlsMode::Verify, + Some(V2HookTlsMode::NoVerify) => TlsMode::NoVerify, + Some(V2HookTlsMode::Off) => TlsMode::Off, + None => TlsMode::default(), + }; + return Some(HookType::Http { + url: interp_to_string(url), + headers, + allowed_env_vars: hook.allowed_env_vars.clone(), + tls, + }); + } + if matches!(hook.agent, Some(HookAgentMarker::Enabled)) { + return Some(HookType::Agent { + prompt: hook + .prompt + .as_ref() + .map(interp_to_string) + .unwrap_or_default(), + model: hook.model.as_ref().map(interp_to_string), + max_tool_rounds: hook.max_tool_rounds, + }); + } + hook.prompt.as_ref().map(|prompt| HookType::Prompt { + prompt: interp_to_string(prompt), + model: hook.model.as_ref().map(interp_to_string), + }) +} + +fn bridge_hook_event(event: V2HookEvent) -> HookEvent { + match event { + V2HookEvent::RunStart => HookEvent::RunStart, + V2HookEvent::RunComplete => HookEvent::RunComplete, + V2HookEvent::RunFailed => HookEvent::RunFailed, + V2HookEvent::StageStart => HookEvent::StageStart, + V2HookEvent::StageComplete => HookEvent::StageComplete, + V2HookEvent::StageFailed => HookEvent::StageFailed, + V2HookEvent::StageRetrying => HookEvent::StageRetrying, + V2HookEvent::EdgeSelected => HookEvent::EdgeSelected, + V2HookEvent::ParallelStart => HookEvent::ParallelStart, + V2HookEvent::ParallelComplete => HookEvent::ParallelComplete, + V2HookEvent::SandboxReady => HookEvent::SandboxReady, + V2HookEvent::SandboxCleanup => HookEvent::SandboxCleanup, + V2HookEvent::CheckpointSaved => HookEvent::CheckpointSaved, + V2HookEvent::PreToolUse => HookEvent::PreToolUse, + V2HookEvent::PostToolUse => HookEvent::PostToolUse, + V2HookEvent::PostToolUseFailure => HookEvent::PostToolUseFailure, + } +} + +fn interp_to_string(value: &InterpString) -> String { + value.as_source() +} diff --git a/lib/crates/fabro-hooks/src/types.rs b/lib/crates/fabro-hooks/src/types.rs index ca7eeaa30..65ae5001b 100644 --- a/lib/crates/fabro-hooks/src/types.rs +++ b/lib/crates/fabro-hooks/src/types.rs @@ -1,4 +1,4 @@ -pub use fabro_types::settings::hook::HookEvent; +pub use crate::config::HookEvent; use fabro_types::RunId; use serde::{Deserialize, Serialize}; diff --git a/lib/crates/fabro-types/src/settings/hook.rs b/lib/crates/fabro-types/src/settings/hook.rs deleted file mode 100644 index 69309b9c5..000000000 --- a/lib/crates/fabro-types/src/settings/hook.rs +++ /dev/null @@ -1,229 +0,0 @@ -use std::borrow::Cow; - -use serde::{Deserialize, Serialize}; - -/// Lifecycle events that can trigger user-defined hooks. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum HookEvent { - RunStart, - RunComplete, - RunFailed, - StageStart, - StageComplete, - StageFailed, - StageRetrying, - EdgeSelected, - ParallelStart, - ParallelComplete, - /// Reserved: hooks for this event are not yet invoked by the engine. - SandboxReady, - /// Reserved: hooks for this event are not yet invoked by the engine. - SandboxCleanup, - CheckpointSaved, - PreToolUse, - PostToolUse, - PostToolUseFailure, -} - -impl HookEvent { - /// Whether hooks for this event block execution by default. - #[must_use] - pub fn is_blocking_by_default(self) -> bool { - matches!( - self, - Self::RunStart - | Self::StageStart - | Self::EdgeSelected - | Self::PreToolUse - | Self::SandboxReady - ) - } -} - -impl std::fmt::Display for HookEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::RunStart => "run_start", - Self::RunComplete => "run_complete", - Self::RunFailed => "run_failed", - Self::StageStart => "stage_start", - Self::StageComplete => "stage_complete", - Self::StageFailed => "stage_failed", - Self::StageRetrying => "stage_retrying", - Self::EdgeSelected => "edge_selected", - Self::ParallelStart => "parallel_start", - Self::ParallelComplete => "parallel_complete", - Self::SandboxReady => "sandbox_ready", - Self::SandboxCleanup => "sandbox_cleanup", - Self::CheckpointSaved => "checkpoint_saved", - Self::PreToolUse => "pre_tool_use", - Self::PostToolUse => "post_tool_use", - Self::PostToolUseFailure => "post_tool_use_failure", - }) - } -} - -/// TLS verification mode for HTTP hooks. -#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum TlsMode { - /// Require `https://` and verify certificates (default). - #[default] - Verify, - /// Require `https://` but skip certificate verification. - NoVerify, - /// Allow `http://`; skip certificate verification for `https://`. - Off, -} - -/// How a hook is executed. -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum HookType { - Command { - command: String, - }, - Http { - url: String, - headers: Option>, - #[serde(default)] - allowed_env_vars: Vec, - #[serde(default)] - tls: TlsMode, - }, - Prompt { - prompt: String, - model: Option, - }, - Agent { - prompt: String, - model: Option, - max_tool_rounds: Option, - }, -} - -/// A single hook definition. -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -pub struct HookDefinition { - pub name: Option, - pub event: HookEvent, - /// Inline command shorthand — if set, implies `type = "command"`. - #[serde(default)] - pub command: Option, - /// Explicit hook type (command or http). If omitted and `command` is set, - /// defaults to `Command`. - #[serde(flatten)] - pub hook_type: Option, - /// Regex matched against node_id, handler_type, or event-specific fields. - pub matcher: Option, - /// Override the event's default blocking behavior. - pub blocking: Option, - /// Timeout in milliseconds (default: 60_000). - pub timeout_ms: Option, - /// Run inside the sandbox (true, default) or on the host (false). - pub sandbox: Option, -} - -impl HookDefinition { - /// Resolve the effective hook type: explicit `hook_type` wins, then `command` - /// shorthand, then error. - pub fn resolved_hook_type(&self) -> Option> { - if let Some(ref ht) = self.hook_type { - return Some(Cow::Borrowed(ht)); - } - self.command.as_ref().map(|cmd| { - Cow::Owned(HookType::Command { - command: cmd.clone(), - }) - }) - } - - /// Whether this hook is blocking for its event. - #[must_use] - pub fn is_blocking(&self) -> bool { - self.blocking - .unwrap_or_else(|| self.event.is_blocking_by_default()) - } - - /// Timeout duration for this hook. - /// - /// Defaults: 30s for prompt hooks, 60s for all others. - #[must_use] - pub fn timeout(&self) -> std::time::Duration { - if let Some(ms) = self.timeout_ms { - return std::time::Duration::from_millis(ms); - } - let default_ms = match self.resolved_hook_type().as_deref() { - Some(HookType::Prompt { .. }) => 30_000, - _ => 60_000, - }; - std::time::Duration::from_millis(default_ms) - } - - /// Whether this hook runs in the sandbox. - #[must_use] - pub fn runs_in_sandbox(&self) -> bool { - self.sandbox.unwrap_or(true) - } - - /// The effective name: explicit name or a generated one. - #[must_use] - pub fn effective_name(&self) -> String { - if let Some(ref n) = self.name { - return n.clone(); - } - let event_str = self.event.to_string(); - match self.resolved_hook_type().as_deref() { - Some(HookType::Command { ref command }) => { - let short = &command[..command.floor_char_boundary(20)]; - format!("{event_str}:{short}") - } - Some(HookType::Http { ref url, .. }) => format!("{event_str}:{url}"), - Some(HookType::Prompt { ref prompt, .. } | HookType::Agent { ref prompt, .. }) => { - let short = &prompt[..prompt.floor_char_boundary(20)]; - format!("{event_str}:{short}") - } - None => event_str, - } - } -} - -/// Top-level hook configuration: a list of hook definitions. -#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] -pub struct HookSettings { - #[serde(default)] - pub hooks: Vec, -} - -impl HookSettings { - /// Merge with another config. Concatenates lists; on name collisions, `other` wins. - #[must_use] - pub fn merge(self, other: Self) -> Self { - let mut by_name: std::collections::HashMap = - std::collections::HashMap::new(); - let mut order: Vec = Vec::new(); - - for hook in self.hooks { - let name = hook.effective_name(); - if !by_name.contains_key(&name) { - order.push(name.clone()); - } - by_name.insert(name, hook); - } - for hook in other.hooks { - let name = hook.effective_name(); - if !by_name.contains_key(&name) { - order.push(name.clone()); - } - by_name.insert(name, hook); - } - - let hooks = order - .into_iter() - .filter_map(|name| by_name.remove(&name)) - .collect(); - - Self { hooks } - } -} diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index a6830b050..721371d01 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -19,20 +19,16 @@ //! owning consumer crates or replace their call sites with v2-native //! accessors, at which point this module goes away. -pub mod hook; pub mod mcp; -pub mod project; pub mod run; pub mod sandbox; pub mod server; pub mod v2; -pub use hook::{HookDefinition, HookEvent, HookSettings, HookType, TlsMode}; pub use mcp::{ McpServerEntry, McpServerSettings, McpTransport, default_startup_timeout_secs, default_tool_timeout_secs, }; -pub use project::ProjectSettings; pub use run::{ ArtifactsSettings, CheckpointSettings, GitHubSettings, LlmSettings, MergeStrategy, PullRequestSettings, SetupSettings, diff --git a/lib/crates/fabro-types/src/settings/project.rs b/lib/crates/fabro-types/src/settings/project.rs deleted file mode 100644 index 9f79df5e8..000000000 --- a/lib/crates/fabro-types/src/settings/project.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Deserialize, Serialize}; - -fn default_root() -> String { - ".".to_string() -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct ProjectSettings { - #[serde(default = "default_root")] - pub root: String, -} - -impl Default for ProjectSettings { - fn default() -> Self { - Self { - root: default_root(), - } - } -} diff --git a/lib/crates/fabro-types/src/settings/v2/to_runtime.rs b/lib/crates/fabro-types/src/settings/v2/to_runtime.rs index 5ba403cba..c3e3705f5 100644 --- a/lib/crates/fabro-types/src/settings/v2/to_runtime.rs +++ b/lib/crates/fabro-types/src/settings/v2/to_runtime.rs @@ -1,28 +1,20 @@ //! v2 → runtime-type conversion helpers. //! -//! The runtime types in `fabro_types::settings::{hook,mcp,run,sandbox}` are -//! the shapes that downstream crates (fabro-workflow, fabro-mcp, -//! fabro-sandbox, fabro-hooks) still consume at runtime. Each helper here -//! reads the v2 parse tree and builds the equivalent runtime value. +//! The runtime types in `fabro_types::settings::{mcp,run,sandbox}` are the +//! shapes that downstream crates (fabro-workflow, fabro-mcp, fabro-sandbox) +//! still consume at runtime. Each helper here reads the v2 parse tree and +//! builds the equivalent runtime value. //! -//! These helpers replace the deleted `bridge_to_old` seam from Stage 6.2. -//! They are narrower: each builds a single runtime type from a single v2 -//! subtree, rather than assembling a full legacy [`Settings`] struct. -//! -//! Stage 6.3 deletes the legacy runtime types themselves. At that point -//! these helpers either disappear or get rewritten against the v2-native -//! replacements. +//! Hook bridging has moved to `fabro_hooks::config::bridge_hook`, which +//! owns its runtime shape. Consumer crates will pull the rest of these +//! helpers into their own crates in follow-up 6.3b passes. use std::collections::HashMap; use super::interp::InterpString; use super::run::{ - HookEntry as V2HookEntry, HookEvent as V2HookEvent, McpEntryLayer, - MergeStrategy as V2MergeStrategy, RunArtifactsLayer, RunPullRequestLayer, RunSandboxLayer, - WorktreeMode as V2WorktreeMode, -}; -use crate::settings::hook::{ - HookDefinition, HookEvent as OldHookEvent, HookType as OldHookType, TlsMode as OldTlsMode, + McpEntryLayer, MergeStrategy as V2MergeStrategy, RunArtifactsLayer, RunPullRequestLayer, + RunSandboxLayer, WorktreeMode as V2WorktreeMode, }; use crate::settings::mcp::{McpServerEntry, McpTransport}; use crate::settings::run::{ @@ -213,108 +205,6 @@ pub fn bridge_mcp_entry(entry: &McpEntryLayer) -> McpServerEntry { } } -pub fn bridge_hook(hook: &V2HookEntry) -> HookDefinition { - let hook_type = resolve_hook_type(hook); - // If the hook is a script/command form, emit via the shorthand so the - // old HookDefinition.command field holds the full command and - // HookDefinition.hook_type stays None. This avoids the duplicate - // `command` key that would otherwise appear under `#[serde(flatten)]`. - let command = if let Some(script) = &hook.script { - Some(interp_to_string(script)) - } else { - hook.command.as_ref().map(|command| { - command - .iter() - .map(interp_to_string) - .collect::>() - .join(" ") - }) - }; - HookDefinition { - name: hook.name.clone().or_else(|| hook.id.clone()), - event: bridge_hook_event(hook.event), - command, - hook_type, - matcher: hook.matcher.clone(), - blocking: hook.blocking, - timeout_ms: hook - .timeout - .map(|d| u64::try_from(d.as_std().as_millis()).unwrap_or(u64::MAX)), - sandbox: hook.sandbox, - } -} - -fn resolve_hook_type(hook: &V2HookEntry) -> Option { - // Script/command-shorthand hooks are emitted via the top-level - // HookDefinition.command field in bridge_hook, not here, to avoid - // the `#[serde(flatten)]` duplicate-field collision between the - // outer HookDefinition.command shorthand and the inner - // HookType::Command.command in the legacy old Settings shape. - if hook.script.is_some() || hook.command.is_some() { - return None; - } - if let Some(url) = &hook.url { - let headers = if hook.headers.is_empty() { - None - } else { - Some( - hook.headers - .iter() - .map(|(k, v)| (k.clone(), interp_to_string(v))) - .collect(), - ) - }; - let tls = match hook.tls { - Some(super::run::HookTlsMode::Verify) => OldTlsMode::Verify, - Some(super::run::HookTlsMode::NoVerify) => OldTlsMode::NoVerify, - Some(super::run::HookTlsMode::Off) => OldTlsMode::Off, - None => OldTlsMode::default(), - }; - return Some(OldHookType::Http { - url: interp_to_string(url), - headers, - allowed_env_vars: hook.allowed_env_vars.clone(), - tls, - }); - } - if hook.agent.is_some() { - return Some(OldHookType::Agent { - prompt: hook - .prompt - .as_ref() - .map(interp_to_string) - .unwrap_or_default(), - model: hook.model.as_ref().map(interp_to_string), - max_tool_rounds: hook.max_tool_rounds, - }); - } - hook.prompt.as_ref().map(|prompt| OldHookType::Prompt { - prompt: interp_to_string(prompt), - model: hook.model.as_ref().map(interp_to_string), - }) -} - -fn bridge_hook_event(event: V2HookEvent) -> OldHookEvent { - match event { - V2HookEvent::RunStart => OldHookEvent::RunStart, - V2HookEvent::RunComplete => OldHookEvent::RunComplete, - V2HookEvent::RunFailed => OldHookEvent::RunFailed, - V2HookEvent::StageStart => OldHookEvent::StageStart, - V2HookEvent::StageComplete => OldHookEvent::StageComplete, - V2HookEvent::StageFailed => OldHookEvent::StageFailed, - V2HookEvent::StageRetrying => OldHookEvent::StageRetrying, - V2HookEvent::EdgeSelected => OldHookEvent::EdgeSelected, - V2HookEvent::ParallelStart => OldHookEvent::ParallelStart, - V2HookEvent::ParallelComplete => OldHookEvent::ParallelComplete, - V2HookEvent::SandboxReady => OldHookEvent::SandboxReady, - V2HookEvent::SandboxCleanup => OldHookEvent::SandboxCleanup, - V2HookEvent::CheckpointSaved => OldHookEvent::CheckpointSaved, - V2HookEvent::PreToolUse => OldHookEvent::PreToolUse, - V2HookEvent::PostToolUse => OldHookEvent::PostToolUse, - V2HookEvent::PostToolUseFailure => OldHookEvent::PostToolUseFailure, - } -} - fn interp_to_string(value: &InterpString) -> String { value.as_source() } diff --git a/lib/crates/fabro-workflow/src/operations/start.rs b/lib/crates/fabro-workflow/src/operations/start.rs index 7944fb576..d59168d3b 100644 --- a/lib/crates/fabro-workflow/src/operations/start.rs +++ b/lib/crates/fabro-workflow/src/operations/start.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use fabro_config::project as project_config; +use fabro_hooks::config::bridge_hook; use fabro_interview::{AutoApproveInterviewer, Interviewer}; use fabro_model::{Catalog, FallbackTarget, Provider}; use fabro_sandbox::{SandboxProvider, SandboxSpec}; @@ -12,7 +13,7 @@ use fabro_types::RunId; use fabro_types::settings::sandbox::{self as sandbox_config, WorktreeMode}; use fabro_types::settings::v2::run::ModelRefOrSplice; use fabro_types::settings::v2::to_runtime::{ - bridge_hook, bridge_mcp_entry, bridge_pull_request, bridge_sandbox, bridge_worktree_mode, + bridge_mcp_entry, bridge_pull_request, bridge_sandbox, bridge_worktree_mode, }; use fabro_types::settings::v2::{InterpString, SettingsFile};