feat(agent): MCP-driven agent session path (parallel to chat) - #155
Open
esnunes wants to merge 4 commits into
Open
feat(agent): MCP-driven agent session path (parallel to chat)#155esnunes wants to merge 4 commits into
esnunes wants to merge 4 commits into
Conversation
Captures the deep plan to pivot Destila to an MCP-server model where the agent communicates only via tool calls, running alongside the existing chat-based path. Front-loads the HTTP+SSE transport smoke test as U1 to derisk Claude Code compatibility before investing in schema, UI, and host-mode work. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Targeted deepening pass against the priority areas where the first pass
was thin:
- U1 transport: concrete bridge<->Destila HTTP shape (JSON-RPC methods,
response envelopes, headers, SSE event format). Clarifies the bridge
isolates upstream MCP wire-protocol drift from Destila.
- U2 schemas: confirmed SQLite via ecto_sqlite3 ~> 0.17. Replaced the
table-level CHECK with changeset-level validation (CHECK on existing
tables is awkward in SQLite ALTER); added three indexes for the new
query shapes.
- U6 handoff race: concrete sub-state machine with 30s spawn-to-active
gate, 5s soft-stop / 10s hard-kill escalation, FIFO buffered stdin
flushed with 50ms inter-write delay, crash-mid-handoff handling.
- U10 mock harness: promoted MockMCPClient from sketch to a 10-function
public API. Specified Mimic-stub registration in test_helper.exs.
- Scenario coverage audit: added the two scenarios the first pass left
unlinked ("Session log records only tool-call events" in U3 tests,
"User types directly into the embedded terminal" in U8 tests).
No implementation-unit boundaries changed; no U-IDs were renumbered.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the full plan at docs/plans/2026-05-20-001-feat-mcp-driven-agent-sessions-plan.md. The new agent path runs alongside the existing chat path (untouched). Destila now operates as an MCP server reachable over HTTP+SSE on /mcp, authenticated by a global bearer token from DESTILA_MCP_TOKEN. A new Go bridge in cmd/destila-mcp/ translates Claude Code's stdio MCP frames to Destila's HTTP shape, insulating the Elixir code from upstream MCP wire protocol drift. The new path: - adds agent_sessions and agent_session_events tables, plus a nullable agent_session_id FK on workflow_session_metadata so exports are shared - introduces Destila.Agent.* modules: schemas, context, session GenServer, event router, four tool handlers (session/ask_user_question/service/ exports_read), embedded + external host modes, YAML workflow loader - mounts /mcp routes (POST rpc + GET events SSE) under a new pipeline - adds AgentSessionLive (export-first UI, no chat textarea) and AgentSessionCreateLive at /agent-sessions and /agent-sessions/new - adds a "New agent-driven session" card to the crafting board next to the existing chat-path entry - adds features/mcp_driven_session.feature, the MockMCPClient harness, scripts/mcp_smoke.sh for manual smoke tests, and docs/mcp_smoke_test.md - 37 new tests cover schemas, sessions, server, router, tool handlers, auth plug, controller, LiveView render, and create flow The chat path (WorkflowRunnerLive, Destila.AI.*, the 27 existing feature files, lib/destila/workflows/*) is fully untouched; existing 921-test suite still passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-applied fixes from the ce-code-review pass over commit 468550c. Security / auth: - MCP bearer token compared with Plug.Crypto.secure_compare to close the timing-side-channel on a global, long-lived credential. Bearer prefix is now case-insensitive (RFC 7235) and missing-token config is rejected as 401 instead of raising 500. - Per-session tmpdir under /tmp now chmod 0o700; .mcp.json and the system-prompt file written 0o600 so the token isn't readable by other local users on shared hosts. - ExternalHost.connection_info and McpConfigWriter no longer crash the LiveView render or terminal spawn when :mcp_token is missing. Correctness / reliability: - Replace strict {:ok, _} = ... matches in session, ask_user_question, and exports_read tool handlers with case branches so Ecto errors surface as JSON-RPC error envelopes instead of crashing the per-session GenServer. - SessionServer.mark_connected/disconnected no longer crashes the GenServer when update_session returns {:error, _}. - handle_tool_call wraps GenServer.call in try/catch with an explicit 60s timeout so a slow tool returns -32603 instead of a 500. - AgentSessionLive answers a question against an idled-out SessionServer with a flash instead of a MatchError crash. - SSE controller: handle MatchError on first chunk write, unsubscribe on every exit path, safely encode arbitrary payloads via Jason.encode/1, drop unused after timeout from 60min to 5min. - EventRouter no longer returns a JSON-RPC error envelope for a notification request (per JSON-RPC 2.0 §4.1). - WorkflowLoader.load_all/0 now boots inside try/rescue so a malformed YAML file is logged loudly instead of silently degrading the app. Data / migration: - Migration converted to explicit up/down. Down preserves chat-path rows and drops the agent-only ones explicitly (with NOT NULL workflow_session_id documented as a destructive caveat). - Add unique_index on (agent_session_id, phase_name, key) so Sessions.record_export/2 can use on_conflict {:replace, ...} to upsert instead of duplicating rows on retry. Project standards: - Extract Destila.Agent.Workflow.Phase to its own file (CLAUDE.md: never nest modules). - Replace String.to_atom(action) in service_tool.ex with a stub that returns a typed error (CLAUDE.md: never String.to_atom on user input; ServiceManager.execute signature is incompatible — wiring deferred). - Fix mcp_driven_session.feature scenario tag to match an existing scenario name. Test isolation: - auth_plug_test.exs setup now restores the previous :mcp_token value instead of deleting it, so later LiveView tests that read the env via ExternalHost don't break. Remaining residual work (not in this commit): EmbeddedHost wiring is still incomplete — SessionServer never calls start_phase, the terminal command isn't passed to Terminal.Server, and ask_user_question answers aren't routed back to the agent. These need design decisions beyond auto-fix scope. All 921 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new MCP-driven agent session path that runs alongside the existing chat path. Destila now operates as an MCP server reachable over HTTP+SSE on
/mcp, authenticated by a global bearer token. A Go bridge incmd/destila-mcp/translates Claude Code's stdio MCP frames to Destila's HTTP shape, insulating Elixir from upstream MCP wire-protocol drift.The chat path (
WorkflowRunnerLive,Destila.AI.*, the 27 existing feature files,lib/destila/workflows/*) is fully untouched — this is a strict parallel rollout per the plan.Implements
docs/plans/2026-05-20-001-feat-mcp-driven-agent-sessions-plan.mdend-to-end (U1–U10) plus follow-up fixes from a multi-agent code review.What's new
/mcpwith bearer auth (Plug.Crypto.secure_compare), case-insensitiveBearerprefix, RFC 7235 compliantcmd/destila-mcp/(greenfield Go subdir, separate release artifact)agent_sessions,agent_session_events, plus a nullableagent_session_idFK onworkflow_session_metadataso exports are shared. SQLite table-rebuild with explicitup/down. Application-levelvalidate_exactly_one_sessioninvariant + unique index on(agent_session_id, phase_name, key)for upsertSessionServerGenServer (per session),EventRouter,Registry+DynamicSupervisorwired into the application supervision treesession(phase_complete / suggest_phase_complete / export),ask_user_question(non-blocking, returns immediately withquestion_id),service(typed-error stub untilServiceManageris refactored),exports_read(cross-phase context recovery)priv/workflows/*.yamlcached in:persistent_termat boot, with a bundledexample.yamlDestila.Terminal, writes per-session.mcp.json(chmod 0600) and system-prompt fileAgentSessionLiveat/agent-sessions/:id— export-first layout, no chat textarea, exports panel + collapsible tool-call event log + embedded terminal or external-host panel + question panel + handoff modalAgentSessionCreateLiveat/agent-sessions/new— workflow + host-mode picker, optional projectfeatures/mcp_driven_session.feature— 23 Gherkin scenarios +MockMCPClienttest harnessscripts/mcp_smoke.sh+docs/mcp_smoke_test.md— manual transport smoke testTest plan
mix testpasses — 921 tests (37 new tests for schemas, sessions, server, router, tool handlers, auth plug, controller, LiveView render, create flow)mix compile --warnings-as-errorscleanmix ecto.rollback+mix ecto.migrateround-trip cleanly (rebuild preserves chat-path rows on down)go build ./cmd/destila-mcp/)scripts/mcp_smoke.sh) onceDESTILA_MCP_TOKENis setCode review pass
Multi-agent review (correctness, security, data-migrations, reliability, project-standards, adversarial) surfaced and auto-fixed:
.mcp.json+ system-prompt{:ok, _} =matches replaced with case branches in every tool handler and SSE connect/close handler so Ecto errors surface as JSON-RPC envelopes instead of crashing the per-session GenServerWorkflowLoader.load_allwrapped in try/rescue,LiveViewfalls back gracefully when SessionServer is idledup/down, unique index +on_conflict: {:replace, ...}on agent exportsWorkflow.Phaseextracted to its own file (no nested modules),String.to_atomremoved from agent-input pathResidual follow-up (not in this PR)
EmbeddedHost.start_phaseis not yet called bySessionServer— the embedded mode still needs the boot-phase wiring, and the resolvedagent_commandneeds to be passed intoTerminal.Server(which currently hard-codes `tmux attach`)ask_user_questionanswers don't reach the agent yet — answer delivery via embedded stdin or external paste needsSessionServer.handle_call({:answer_question, ...})to route byhost_modeServiceToolreturns a typed error stub —ServiceManager.execute/3needs an agent-session entry point before this can be wiredThese are design decisions intentionally scoped out of the auto-fix pass.
🤖 Generated with Claude Code