refactor(mcp): serve hook runtime through its owners - #2362
Conversation
…owner # Conflicts: # crates/tracedecay/src/daemon/projectless.rs
…owner # Conflicts: # crates/tracedecay-api/src/http/application_operation_owner.rs # crates/tracedecay-contracts/src/graph_tool.rs # crates/tracedecay-contracts/src/policy.rs # crates/tracedecay-contracts/src/retrieval/catalog.rs # crates/tracedecay-contracts/src/sdk_catalog.rs # crates/tracedecay-daemon-protocol/src/application_surface.rs # crates/tracedecay-daemon-protocol/src/application_surface/invocation.rs # crates/tracedecay-daemon-protocol/src/contract/mod.rs # crates/tracedecay-mcp/src/tools/binding.rs # crates/tracedecay-tool-catalog/src/manifest.rs # crates/tracedecay-tool-catalog/src/operation.rs # crates/tracedecay-tool-catalog/src/owner_side_effect.rs # crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs # dashboard/codegen/schemas/dashboard-contracts.schema.json # dashboard/src/contracts/generated.ts # sdks/typescript/src/operations.ts
…owner # Conflicts: # crates/tracedecay-api/src/http/application_operation_owner.rs # crates/tracedecay-contracts/src/policy.rs # crates/tracedecay-contracts/src/retrieval/catalog.rs # crates/tracedecay-contracts/src/sdk_catalog.rs # crates/tracedecay-daemon-protocol/src/application_surface.rs # crates/tracedecay-daemon-protocol/src/contract/mod.rs # crates/tracedecay-mcp/src/tools/binding.rs # crates/tracedecay-tool-catalog/src/manifest.rs # crates/tracedecay-tool-catalog/src/operation.rs # crates/tracedecay/src/daemon/profile_owner.rs # crates/tracedecay/src/daemon/projectless.rs # crates/tracedecay/src/mcp/server/graph_tool_owner.rs # crates/tracedecay/src/mcp/tools/handlers/dispatch_groups.rs # dashboard/codegen/schemas/dashboard-contracts.schema.json # dashboard/src/contracts/generated.ts # sdks/typescript/src/operations.ts
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad7d089240
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Self::HookRuntime => Some(OwnerSideEffectEntryV1 { | ||
| effect: EffectClass::RecordsHostEvidence, | ||
| ceiling_millis: INTERACTIVE_CEILING_MILLIS, | ||
| identical_calls: IdenticalCallPolicyV1::RunEach, | ||
| }), |
There was a problem hiding this comment.
Treat hook runtime calls as authoritative effects
When a hook call is cancelled or reaches its deadline after daemon admission, application_surface_cancellation_policy still classifies HookRuntime as ReadOnly, even though this entry makes it a non-cancellable owner side effect. The controlled client therefore returns Cancelled/TimedOut immediately instead of waiting for authoritative settlement, while the owner ignores cancellation and may finish committing the transcript, admission, or receipt; a host retry can then run the RunEach effect again. Add HookRuntime to the authoritative-effect policy (or derive that policy from this owner-side-effect metadata) so the transport behavior matches the published not_cancellable contract.
Useful? React with 👍 / 👎.
tracedecay_hook_runtime, the internal tool every agent-host hook calls, is now a typed owner operation (ApplicationSurfaceOperation::HookRuntime, wire name unchanged). The project's graph-tool owner answers project hooks. Hooks with no project route go to the daemon's profile owner, through theis_profile_owner_requestgeneralization from #2351. It is listed inGRAPH_TOOL_OPERATIONSandINTERNAL_OPERATIONS, so it is served by name and never advertised:tools/liststays at 230.Typed request, result, and effect
HookRuntimeSurfaceRequestV1is tagged byactionwithdeny_unknown_fields. It takes exactly what the shipped hosts send (derived fromhooks/mod.rs,daemon_ports.rs,dispatch.rs,codex.rs,claude.rs,cursor_compact.rs, and the Hermessync_turn):reset_counter,hook_v2_admit,hook_v2_delivery_receipt,hook_v2_feedback_notice_delivery,opencode_lsp_updated,ingest_transcript,codex_compact,claude_compact,cursor_compact,user_review,hermes_receipt, andhook_v2_profile_admit. Hook-native payloads (envelope, receipt, event, messages) stay JSON in the contract and are decoded by the hook domain types that own them.HookRuntimeResultV1is tagged byaction. Each action has its typed outcome, with per-statusvariants for admissions and notice delivery and one compaction outcome per field set. It serializes to the JSON the handler emitted, with one deliberate change: the three outputs that lackedactionnow carry it (hook_v2_delivery_receipt, the boundhook_v2_feedback_notice_delivery, and the skippedcursor_compact). I checked every host decoder; none rejects the extra key. The hand-writtenDaemonAdmissionResponseWireV1indaemon_ports.rsis gone; the host decodes the contract type. Every host sender now builds the typed request, so a sender cannot drift from what the daemon accepts.EffectClass::RecordsHostEvidence: it records a host's session evidence (transcripts, hook admissions, receipts) in daemon-owned session stores. Owner entry: 120 s ceiling (unchanged from today's interactive ceiling),RunEach, not interruptible.Refused now, where they used to be ignored or defaulted:
project_root, andtimeout_budget_ms, which the Cursor/Kiro/Pi senders sent and the daemon ignored (the senders no longer send it);providerandsession_idon compaction (removed from the Codex and Claude senders);storage_scopeon hook ingest (Hermes now routes onuser_scope);user_scope;accounting_receipt,hook_v2_guidance_lookup,hook_v2_scout_prepare,hook_v2_feedback,hook_v2_cancel,hook_v2_status, the fourhook_v2_scout_*reads, andcodex_stop. Their handlers are deleted.What moved with the tool
complete_tool_callpath, keyed by tool name. They now run in the owner, so the CLI (tracedecay tool hook_runtime, which Hermes uses) gets them too.live_transcript_refresh.rskeeps only thelcm_preflightrule and gainsjoin_hook_ingest_refresh.RegisteredHostIngestserver, notCore, just as the MCP connection route already did. Both use onehook_runtime_requirement, built on the contract'shook_runtime_needs_session_stores(every action exceptreset_counter).project_authority_unbound(seen inpackaged_host_ingest_delivers_a_registered_advisory_cycleafter the refactor(mcp): serve admin cli through its owners #2354 merge). The owner port now answersapplication.runtime.mountingthere. Both the CLI loop andcall_default_toolalready re-send on that refusal.graph_tool_error_problem). Before, they fell through tograph_tool.failed.runtime_ports::daemon_tool_json) reads anisErrorresult whose payload is a problem envelope as the owner's typed error, so hooks still see failures asErr.sync_turnnow requestsformat: json. Without it, this internal tool answered markdown,call_tracedecay_jsonreported "invalid nested JSON", and the turn-completed/ingested receipts never fired. The same happens on master.Deleted
dispatch_admin_tools' hook_runtime arm; hook_runtime'sINTERNAL_DAEMON_TOOL_NAMESentry and itsBINDING_GROUPSAdmin-row name.internal_daemon_tool_definition(its only entry), its CLI use, and its test.projectless_hook_runtime_response, and hook_runtime'sprojectless_tool_is_discoverableentry and dispatch arm.tool_errors::tool_error_responsehook_runtime branch;structured_hook_error_datais now test-only.handlers/hook_runtime/terminal.rs(codex_stop), the dead scout actions,accounting_receipt, the stubrun_user_review, and its unreachable review tail.hook_runtime_behavior_tests.rs. It drove a standalone server with no owner route; its behavior is rewritten as themcp_suiteproduction test below.McpToolDispatchGroup::Admin,dispatch_admin_tools, their binding row and arms, and the projectless connection's now-unreadclient_identityfield are deleted. I leftLegacyToolCompatibilityOwner,resolve_catalog_tool_binding,dispatch_compatibility_tool, and the generic fallback for the coordinator.git diff --stat origin/master...HEAD: 76 files, +2198/−2297. The handler, projectless, catalog-definition, error, refresh, host-port, binding, and dispatch-group files alone are +838/−1746.Fail-before / pass-after
Test:
mcp_suitemcp_handler_test::hook_runtime_request_test::hook_runtime_answers_typed_results_and_refuses_what_no_host_sends, over MCPtools/callon the production composition fixture.On
origin/mastersource (6ae6461) with only this test added:Master ignored
{"action":"reset_counter","project_root":"/elsewhere"}and reset the counter.On this branch:
ok. It asserts these literals:{"action":"reset_counter","reset":true};claude_compactrecord;invalid_requestrefusals forproject_root("unknown fieldproject_root, there are no fields"), fortimeout_budget_ms(with the full expected-field list), and forcodex_stop("unknown variantcodex_stop, expected one of …");user_reviewanswers "projectless Hermes review is unavailable: …", a user-scope ingest answers "missing required parametersession_id", and a malformed Hermes event answers "invalid Hermes receipt event: missing fieldevent".The CLI proof is
core_cli_suiteuser_scoped_transcript_ingest_handshakes_projectless_from_filesystem_root_cwd. It asserts thattracedecay tool hook_runtimefrom/handshakes projectless and sends aProfileGraphTool { HookRuntime }invocation carrying the exact arguments.Runtime journey
Setup: debug
tracedecayfrom this branch; isolated HOME,TRACEDECAY_DATA_DIR,TRACEDECAY_GLOBAL_DB, andXDG_CONFIG_HOMEunder the worktree; one daemon undersystemd-run --user --scope --unit=hook-runtime-owner-journey -p MemoryMax=6G -p MemorySwapMax=1G;tracedecay initof a scratch git project.Real hook callbacks over the daemon MCP route (
hook-claude-post-compactwith and without a project cwd,hook-codex-stop, andhook-hermes-terminal-receipt) all exited 0 with{}. Their analytics recorded the daemon calls, and Hermes' native event was admitted by the profile owner ("disposition": {"class":"application","reason_code":"hook_v2_accepted"}). The daemon log's only refusal is the deliberatecodex_stopcall.tracedecay serveover stdio:The same journey was repeated on the final binary, after the #2354 merge and the ownership-window fix, with a fresh isolated profile:
systemctl --user stop hook-runtime-owner-journey.scope: inactive; no daemon of mine remains.Checks
These counts come from after merging #2351 and #2354.
hook_runtime_surface4 of 4), mcp-catalog 28, tracedecay-mcp 380, daemon-protocol 65, daemon-service 322 (includingadoption_observationwithcapability.application.primitive.hook-runtime), api 53, agent-hosts 485.mcp_suite -- hook_runtime_request_test session_search_test schema_test protocol_test admin_test info_health_request_test: 61 passed, 1 failed. The failure,protocol_test::test_tools_call_search, sawstaleness_state: verifyingunder load; it passed on rerun and in both earlier runs.--lib -- mcp:: daemon::: 733 passed, 1 failed. The failure isruntime_identity::concurrent_same_identity_worktrees…, master-red (test: three suites still expect JSON-RPC errors for owner refusals now rendered as isError #2344).replay::client_identity_startup_replays_retained_profile_receipts, which asserted the old JSON-RPC error. It now asserts the owner'scanonical_admission_failedrefusal.daemon::tests::bootstrap daemon::tests::replay daemon::projectless mcp::: 257 passed, 1 failed. The failure,routing::many_slow_initialize_roots_share_one_discovery_budget, took 3.011 s against its 3 s bound under shared-host load; it passed in both earlier root runs.tracedecay-cli --bins: 334 passed.core_cli_suite: 144 passed, 2 failed. Both failures aretool_diagnostics_reads_the_typescript_producer_publication(master-red, test: three suites still expect JSON-RPC errors for owner refusals now rendered as isError #2344) and its siblingtool_diagnostics_reads_a_monorepo_package_finding, with the same host TypeScript producer cause (application.diagnostics.unavailable).hermes_suitelcm_bridge: 25 passed. The fullhermes_suitepassed 26 of 26 before the Hermes format fix.transcript_ingest_suite:hermes::23 passed, plusprojectless_hermes_turn_sync…andrepeated_codex_compactions….session_suitehook_ingest_join_runs_the_bound_refresh_worker: passed.hooks_lsp_suitenative_hook_captures_only_bound_transport_spool_records: passed.runtime_acceptance_suitepackaged_host_ingest_delivers_a_registered_advisory_cycle: 3 of 3 on the rebuilt binary.scripts/hermes_plugin_unit_check.py: 45 of 45.transport_acceptance_suite killed_daemon_retries…is not compiled in this configuration (0 matched), so it is not counted.cargo clippyon every touched crate,--all-targets -D warnings, with and withouttracedecay/test-transport,tracedecay/test-helpers: clean.cargo fmt --all -- --check: clean.contracts:check: up to date.mcp_suite hook_runtime_request_test session_search_test: 9 passed.daemon::tests::replay daemon::projectless daemon::tests::bootstrap mcp::tools::handlers: 152 passed.Left open
statusno longer reaches the wire; its reason code and retry verdict do, and no host read the status. After this change,TraceDecayError::hook_runtime_statushas only test readers.hint_outcomes: profile_root_unavailable. This predates the change: the project path has never passed a profile root, so hook hints never settle. Filed as hook ingest never settles hint outcomes: project route passes no profile root #2355.handle_tool_call_with_registry_optionsrefuseshermes_homefor every tool before dispatch, as a JSON-RPC error. That guard belongs to the generic fallback, which the coordinator deletes.