Skip to content

Commit d8e7854

Browse files
authored
Merge pull request #95 from 7df-lab/dev/websearch0610
fix web_search, fix anthropic message API 400 error, add hooks
2 parents 4253bc5 + fb74643 commit d8e7854

60 files changed

Lines changed: 3201 additions & 138 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"crates/safety",
88
"crates/tasks",
99
"crates/mcp",
10+
"crates/network-proxy",
1011
"crates/protocol",
1112
"crates/client",
1213
"crates/config",
@@ -58,6 +59,7 @@ devo-core = { path = "crates/core" }
5859
devo-file-search = { path = "crates/file-search" }
5960
devo-keyring-store = { path = "crates/keyring-store" }
6061
devo-mcp = { path = "crates/mcp" }
62+
devo-network-proxy = { path = "crates/network-proxy" }
6163
devo-protocol = { path = "crates/protocol" }
6264
devo-provider = { path = "crates/provider" }
6365
devo-rmcp-client = { path = "crates/rmcp-client" }
@@ -103,6 +105,7 @@ regex-lite = "0.1.8"
103105
reqwest = { version = "0.12", default-features = false, features = [
104106
"json",
105107
"rustls-tls",
108+
"socks",
106109
"stream",
107110
] }
108111
reqwest-eventsource = "0.6"

crates/cli/src/prompt_command.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ pub(crate) async fn run_prompt(
8282
collaboration_mode: devo_protocol::CollaborationMode::Build,
8383
agent_coordinator: None,
8484
local_web_search: None,
85+
hooks: (!app_config.hooks.is_empty()).then(|| devo_core::HookRuntimeContext {
86+
runner: devo_core::HookRunner::new(app_config.hooks.clone()),
87+
base: devo_core::HookBaseInput {
88+
session_id: session_state.id.clone(),
89+
transcript_path: String::new(),
90+
cwd: cwd.clone(),
91+
permission_mode: Some("auto-approve".to_string()),
92+
agent_id: None,
93+
agent_type: None,
94+
},
95+
}),
96+
network_proxy: None,
8597
},
8698
);
8799
let model_catalog = PresetModelCatalog::load_from_config(&home_dir, Some(&cwd))?;

crates/config/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ consumer crates.
1414
- `server.rs` defines server transport and connection defaults.
1515
- `logging.rs` defines logging and rolling file-log settings.
1616
- `skills.rs` defines skill discovery settings.
17+
- `hooks.rs` defines external hook event and command configuration.
1718
- `experimental.rs` defines opt-in experimental feature gates.
1819
- `error.rs` defines app and provider config error types.
1920
- `provider.rs` re-exports provider config APIs and contains provider-focused
@@ -88,6 +89,7 @@ without clearing every omitted provider field from user config.
8889
- `updates.enabled = true`
8990
- `updates.check_on_startup = true`
9091
- `updates.check_interval_hours = 24`
92+
- `hooks = {}`
9193
- `project_root_markers = [".git"]`
9294
- `projects = {}`
9395

@@ -154,6 +156,14 @@ check_interval_hours = 24
154156

155157
[projects."/path/to/project"]
156158
permission_preset = "default" # read-only, default, auto-review, or full-access
159+
160+
[[hooks.PreToolUse]]
161+
matcher = "exec_command"
162+
163+
[[hooks.PreToolUse.hooks]]
164+
type = "command"
165+
command = "hooks/pre-tool-use.sh"
166+
timeout = 30
157167
```
158168

159169
`logging.file.directory` is optional. Relative logging directories resolve under
@@ -176,6 +186,105 @@ permission_preset = "default" # read-only, default, auto-review, or full-access
176186
Provider-specific validation happens while resolving or mutating provider
177187
config.
178188

189+
## Hooks
190+
191+
External hooks are configured under the top-level `[hooks]` table. Each hook
192+
event contains matcher entries, and each matcher entry contains one or more hook
193+
commands:
194+
195+
```toml
196+
[[hooks.PostToolUse]]
197+
matcher = "exec_command|read_file"
198+
199+
[[hooks.PostToolUse.hooks]]
200+
type = "command"
201+
command = "hooks/post-tool-use.sh"
202+
shell = "bash"
203+
timeout = 30
204+
205+
[[hooks.UserPromptSubmit]]
206+
207+
[[hooks.UserPromptSubmit.hooks]]
208+
type = "command"
209+
command = "hooks/check-prompt.sh"
210+
async = false
211+
```
212+
213+
Command hooks receive one JSON object on stdin. The common fields are
214+
`hook_event_name`, `session_id`, `transcript_path`, and `cwd`. Runtime contexts
215+
may also include `permission_mode`, `agent_id`, and `agent_type`, followed by
216+
event-specific fields such as `tool_name`, `tool_input`, `tool_use_id`,
217+
`tool_response`, `prompt`, `source`, `trigger`, `reason`, `file_path`, `event`,
218+
`old_cwd`, and `new_cwd`.
219+
220+
Hook command results follow Claude Code-style blocking semantics:
221+
222+
- Exit status `0` succeeds unless stdout contains a blocking JSON decision.
223+
- Exit status `2` blocks the triggering action. The block reason is read from
224+
stdout JSON or stderr.
225+
- Stdout JSON shaped as `{"decision":"block","reason":"..."}` blocks even when
226+
the process exits successfully.
227+
- Claude-style `hookSpecificOutput` denial JSON blocks for `PreToolUse` and
228+
`PermissionRequest`.
229+
- Stdout JSON shaped as `{"continue":false,"stopReason":"..."}` is treated as a
230+
blocking stop for lifecycle events that consume blocking decisions.
231+
- Other non-zero exits are logged as non-blocking hook failures.
232+
233+
The `command` hook type is executed by the runtime. `prompt`, `agent`, and
234+
`http` hook definitions are parsed so config files remain forward compatible,
235+
but they are currently logged as unsupported and not executed. `shell` accepts
236+
`bash` and `powershell`. `timeout` is in seconds and defaults to `600`.
237+
`async = true` and `asyncRewake = true` spawn the command in the background and
238+
do not wait for a blocking decision. `if`, `status_message`, and `once` are
239+
preserved in config but are not interpreted by the current runtime.
240+
241+
All 27 hook event names are accepted by config:
242+
243+
- `PreToolUse`, `PostToolUse`, `PostToolUseFailure`
244+
- `Notification`, `UserPromptSubmit`
245+
- `SessionStart`, `SessionEnd`, `Stop`, `StopFailure`
246+
- `SubagentStart`, `SubagentStop`
247+
- `PreCompact`, `PostCompact`
248+
- `PermissionRequest`, `PermissionDenied`
249+
- `Setup`, `TeammateIdle`, `TaskCreated`, `TaskCompleted`
250+
- `Elicitation`, `ElicitationResult`
251+
- `ConfigChange`, `WorktreeCreate`, `WorktreeRemove`
252+
- `InstructionsLoaded`, `CwdChanged`, `FileChanged`
253+
254+
The current runtime triggers hooks where Devo has a matching lifecycle point:
255+
tool execution, prompt submission, server setup, session start and resume,
256+
session shutdown, turn stop and failure, subagent start and stop, manual
257+
compaction, permission request and denial, config writes through `provider/upsert`
258+
and `skills/set_enabled`, per-turn cwd changes, and file changes reported by
259+
`write`/`apply_patch` tool metadata.
260+
261+
Runtime-triggered events:
262+
263+
- `PreToolUse`, `PostToolUse`, `PostToolUseFailure`
264+
- `UserPromptSubmit`
265+
- `SessionStart`, `SessionEnd`
266+
- `Stop`, `StopFailure`
267+
- `SubagentStart`, `SubagentStop`
268+
- `PreCompact`, `PostCompact`
269+
- `PermissionRequest`, `PermissionDenied`
270+
- `Setup`
271+
- `ConfigChange`
272+
- `CwdChanged`, `FileChanged`
273+
274+
Config-ready but not currently triggered:
275+
276+
- `Notification`: Devo has protocol notifications, but no single user-facing
277+
notification lifecycle equivalent to Claude's external notification hook.
278+
- `TeammateIdle`, `TaskCreated`, `TaskCompleted`: the standalone `devo-tasks`
279+
crate is not wired into the server runtime task lifecycle.
280+
- `Elicitation`, `ElicitationResult`: MCP elicitation is currently handled
281+
inside the MCP manager with an automatic response and no server-session hook
282+
bridge.
283+
- `WorktreeCreate`, `WorktreeRemove`: Devo currently has no worktree lifecycle
284+
API.
285+
- `InstructionsLoaded`: Devo discovers AGENTS-style instructions during context
286+
assembly, but does not expose a hookable per-file instruction-load event.
287+
179288
## Provider Config
180289

181290
Provider config is part of `config.toml` and is modeled by `ProviderConfigSection`.
@@ -275,6 +384,26 @@ the provider default endpoint and result count. Compatibility aliases
275384
`websearch` and `web-search` route to `web_search`, but aliases are not exposed
276385
to the model.
277386

387+
## Web Fetch
388+
389+
`[tools.web_fetch]` controls whether a turn exposes URL fetching to the model.
390+
It resolves with the same priority as web search:
391+
392+
1. `[model_bindings.<id>.web_fetch]`
393+
2. `[providers.<id>.web_fetch]`
394+
3. `[tools.web_fetch]`
395+
396+
Supported modes:
397+
398+
- `disabled`: do not provide provider-hosted web fetch and do not expose the
399+
local `webfetch` function tool.
400+
- `provider`: let the active provider adapter inject provider-hosted fetch into
401+
the request. OpenAI Responses uses hosted tool `{"type":"web_fetch"}`;
402+
OpenAI Chat Completions uses `web_fetch_options`; Anthropic Messages uses
403+
server tool `{"type":"web_fetch_20250910","name":"web_fetch"}`.
404+
- `local`: expose the existing local `webfetch` function tool. This is the
405+
default to preserve the existing local fetch behavior.
406+
278407
## Provider Resolution
279408

280409
`resolve_provider_settings_from_config_and_auth` chooses the active model

crates/config/src/app.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use devo_util_paths::FileSystemConfigPathResolver;
1616
use crate::AUTH_CONFIG_FILE_NAME;
1717
use crate::AppConfigError;
1818
use crate::ExperimentalConfig;
19+
use crate::HooksConfig;
1920
use crate::LogRotation;
2021
use crate::LoggingConfig;
2122
use crate::LoggingFileConfig;
@@ -65,6 +66,9 @@ pub struct AppConfig {
6566
/// Tool-specific runtime configuration.
6667
#[serde(default, skip_serializing_if = "ToolsConfig::is_empty")]
6768
pub tools: ToolsConfig,
69+
/// External lifecycle hooks keyed by event name.
70+
#[serde(default, skip_serializing_if = "HooksConfig::is_empty")]
71+
pub hooks: HooksConfig,
6872
/// Provider, model, and active model defaults.
6973
#[serde(flatten)]
7074
pub provider: ProviderConfigSection,
@@ -154,6 +158,7 @@ impl Default for AppConfig {
154158
mcp_oauth_credentials_store: Some(OAuthCredentialsStoreMode::default()),
155159
mcp: McpConfig::default(),
156160
tools: ToolsConfig::default(),
161+
hooks: HooksConfig::default(),
157162
provider: ProviderConfigSection::default(),
158163
provider_http: ProviderHttpConfig::default(),
159164
updates: UpdatesConfig {
@@ -286,6 +291,7 @@ impl AppConfigStore {
286291
.as_deref()
287292
.and_then(non_empty_string),
288293
web_search: None,
294+
web_fetch: None,
289295
enabled: binding.enabled,
290296
},
291297
);

0 commit comments

Comments
 (0)