Treat bundled cleanup hooks as built-ins (#42110)

## What changed

- Centralize the allowlist for bundled MCP cleanup hooks and use it for both local and executor-discovered plugins, including `unified-computer-use` cleanup through `cua_repl`.
- Mark matching cleanup hooks as trusted built-ins so they run without saved hook trust and remain active when regular hooks or their per-hook state are disabled. Plugin enablement and managed-only policy still apply.
- Hide built-in cleanup hooks from hook listings and lifecycle notifications while retaining their metrics. Keep the built-in classification out of serialized protocol data.

## Testing

- Cover allowlist boundaries, trust and enablement behavior, inline and file-based hook declarations, MCP success and error responses, hook listing, lifecycle notifications, metrics, and protocol serialization.

GitOrigin-RevId: f93b7bc99f4ed9694f529def8ec383b45f31430e
This commit is contained in:
teddywyly-oai
2026-09-01 18:02:27 +00:00
committed by copyberry
parent 84aa75204a
commit c7c824dce4
23 changed files with 1058 additions and 239 deletions
+42
View File
@@ -1663,6 +1663,11 @@ pub struct HookOutputEntry {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub struct HookRunSummary {
/// Internal classification used to suppress lifecycle notifications without losing telemetry.
#[serde(skip)]
#[schemars(skip)]
#[ts(skip)]
pub builtin: bool,
pub id: String,
pub event_name: HookEventName,
pub handler_type: HookHandlerType,
@@ -4436,6 +4441,43 @@ mod tests {
Ok(())
}
#[test]
fn hook_builtin_classification_stays_internal() -> Result<()> {
let wire = json!({
"id": "cleanup-hook",
"event_name": "stop",
"handler_type": "mcp_tool",
"execution_mode": "sync",
"scope": "turn",
"source_path": test_path_buf("/tmp/hooks.json").abs(),
"source": "plugin",
"display_order": 0,
"status": "completed",
"status_message": null,
"started_at": 10,
"completed_at": 11,
"duration_ms": 1000,
"entries": [],
});
let mut run: HookRunSummary = serde_json::from_value(wire.clone())?;
assert!(!run.builtin);
run.builtin = true;
assert_eq!(serde_json::to_value(run)?, wire);
let mut untrusted_wire = wire;
untrusted_wire["builtin"] = json!(true);
assert!(!serde_json::from_value::<HookRunSummary>(untrusted_wire)?.builtin);
let schema = serde_json::to_value(schemars::schema_for!(HookRunSummary))?;
assert!(
!schema["properties"]
.as_object()
.expect("hook properties")
.contains_key("builtin")
);
assert!(!HookRunSummary::decl().contains("builtin:"));
Ok(())
}
#[test]
fn feature_thread_source_serializes_as_its_app_owned_label() -> Result<()> {
let source = ThreadSource::Feature("automation".to_string());