feat: DAP mirror — read-only IDE attach to agent-owned debug sessions (#217) - #301
Merged
Conversation
Move MinimalDapClient's decode loop verbatim into DapFrameDecoder and add encodeDapMessage, so the upcoming DAP mirror server reuses the property-fuzzed framing instead of hand-rolling a second parser. handleData/cleanup delegate to the decoder; the existing framing property suite now doubles as a regression net over the extraction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ents (#217) Loopback-only, token-gated TCP DAP server with per-client seq spaces, default-deny dispatch (forward reads, soft-succeed breakpoint config, reject control/mutation with quiet errors), a capability mask that hides control affordances, and late-join stopped synthesis so an IDE attaching to a paused session lands directly on the frame. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mirrorExpose/mirrorUnexpose pseudo-commands ride the existing dap envelope, intercepted before the connectivity bail and policy queueing. The worker now retains adapter capabilities and the last stopped-event body (post threadId-backfill) for the mirror's initialize response and late-join replay, fans stopped/continued out from its own choke points, infers resumption from successful continue/step responses, and closes the mirror with client notification during shutdown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parent-side plumbing for the DAP mirror: exposeSession/unexposeSession on SessionManagerOperations riding the mirrorExpose/mirrorUnexpose pseudo-commands, a SessionExposure record on ManagedSession projected into list_debug_sessions as host/port only (gated on a running proxy), eager exposure cleanup on relaunch and proxy stop, and token redaction — the shared payload sanitizer now redacts token/mirrorToken strings and ProxyManager's raw received-message debug log is sanitized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Server tool dispatch and session-manager expose/unexpose units, sanitizer token-redaction cases, a live-socket check of the production net wiring, and a mock-adapter e2e driving a scripted TCP DAP client (shared TcpDapClient helper) through the mirror: late-join stop replay, the full inspection surface, control rejection, token rejection, two concurrent clients, idempotency, and lifecycle teardown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tool counts 26→28, README table + comparison row flip, a full IDE Mirror section in tool-reference (VS Code debugServer+mirrorToken recipe, language→debug-type table, request dispositions, container note), the SECURITY.md trust-model call-out that the mirror token is a debuggee-execution capability, server-instruction/prompt/skill guidance, api-reference entries, and the CHANGELOG entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
… redaction regression (#217) Four gaps found by review of what the suites actually drive: - Worker stopped/continued fan-out now tested through the REAL setupDapEventHandlers closures (captured via a connection-manager stub) including the threadId-backfill path, replacing a hand-rolled simulation that would have survived deletion of the production insertions. - New e2e: live mirroring — an attached IDE client observes MCP-side stepping (synthesized continued + fresh stopped) and debuggee exit (exactly one terminated via the dedupe, then socket close + port refused), covering the shared-pause-state story end to end. - e2e lifecycle now proves restart_debugging kills the old endpoint, clears the exposure projection, and a fresh expose gets a new port. - Regression test that ProxyManager's received-message debug log redacts the mirror token (the confirmed leak fixed in this PR). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Added a coverage pass before merge (commit c068714) closing four gaps a review of the suites turned up:
Full suite green locally via the pre-push hook; CI re-running. |
…oot cause #143) The full-suite failure of the js-attach smoke was the documented #143 mode — the default 5s thread-verification window hard-failing a healthy attach while js-debug's child session adopts the inspector under heavy machine load — not a mirror regression: the attach path is untouched by this branch and the test passed 10/10 reproduction attempts (isolation, under CPU load, and paired with the new mirror e2e in both orders). Use the caller-configurable verifyTimeout that #147 added for exactly this; the poll exits as soon as threads appear, so a responsive machine pays nothing. Co-Authored-By: Claude Opus 5 (1M context) <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.
Implements the v1 (read-only) DAP mirror from the design spike in #217: an agent debugs a process no IDE owns — CI, a container, a plain terminal — parks it at an interesting point, and a human attaches VS Code (or any DAP client) to the live, paused session and inspects real state. Execution control stays with the MCP session.
What's in the box
Two new MCP tools (26 → 28)
expose_session {sessionId}→{host: "127.0.0.1", port, token}— starts a loopback-only, token-gated DAP server endpoint for the session; idempotent (same endpoint + token on re-call). The resultmessagecarries a ready-to-paste VS Code config ("debugServer": <port>+"mirrorToken"), with a container-networking note appended in container mode.unexpose_session {sessionId}— closes the endpoint and disconnects IDE clients (terminatedevent); no-op success when not exposed.Worker-hosted mirror (
src/proxy/dap-mirror-server.ts) — lives in the proxy worker, the sole owner of the live adapter connection (one worker = one session = one mirror):initializeanswered from the adapter's cached capabilities with control affordances masked off (restart/step-back/setVariable/etc. forced false,exceptionBreakpointFilters: []), so IDE UIs degrade before the user can click.attach/launch(crypto.randomBytes(24)base64url); bad token → user-visible error + socket close; 30s auth timeout; max 4 clients.IDapClient.sendRequest, so js-debug child routing comes free):threads,stackTrace,scopes,variables,source,evaluate,exceptionInfo,loadedSources,modules.set*Breakpoints(reportedverified:falsewith an explanatory message),setExceptionBreakpoints.showUser:falseerrors):continue,next,stepIn/Out,pause,setVariable,restart,terminate, and everything unlisted.request_seqcorrelation under out-of-order completion.stoppedevent from the worker's recorded last stop, so the IDE lands directly on the paused frame (with a 1s fallback for clients that skipconfigurationDone).Event plumbing —
stopped/continuedfan out from the worker's own handlers (after the threadId backfill for Delve/JDI; the generic'event'channel would race the async backfill and ship a threadId-less body), everything else ridesMinimalDapClient's generic'event'feed. A successfulcontinue/step response also synthesizescontinuedfor mirror clients when no event arrives — DAP clients are entitled to infer resumption from the response, and stale paused frames in the IDE were the alternative.Transport: zero new IPC surface —
mirrorExpose/mirrorUnexposeride the existing correlateddapenvelope (precedent:redefineClasses), intercepted at the top ofhandleDapCommandbefore the connectivity bail and policy queueing.Shared framing codec —
MinimalDapClient's battle-tested Content-Length decoder is extracted verbatim intosrc/proxy/dap-framing.ts(DapFrameDecoder+encodeDapMessage) and reused by client and mirror; the existing framing property suite now runs against the extraction as a regression net, plus a new property suite drives the decoder directly.Security
127.0.0.1; token required on every client.token/mirrorTokenvalues, and ProxyManager's raw received-message debug log is sanitized (it previously dumped every IPC message verbatim — thedapResponsecarrying the token would have hit debug logs).SECURITY.md+ tool-reference call out that the token is a debuggee-execution capability (evaluate is forwarded), not a view-only credential.list_debug_sessionsshowsexposure: {host, port}— never the token — gated on a running proxy so stale records can't surface after teardown paths that skip cleanup.Testing
tests/proxy/dap-mirror-server.test.ts— 46 hermetic cases (fake sockets feeding the real decoder): handshake + capability mask, token gate, auth timeout, full reject-table sweep, allowlist forwarding with seq correlation, out-of-order completions, two-client fan-out with independent seq spaces, late-join synthesis + fallback timer, terminated dedupe, max clients, malformed frames.tests/proxy/dap-proxy-worker.test.ts— 14 new worker cases: interception before queueing, idempotent expose, host reflects live worker state, resume inference, shutdown notification, capability retention through the [FEATURE] Capture adapter initialize capabilities + best-effort exceptionInfo enrichment on exception stops #243 once-only guard.tests/core/unit/{server,session}— tool dispatch/result shaping (incl. container note) and exposeSession/unexposeSession logic (stale-record cleanup, relaunch reset, token absent from projections).tests/integration/dap-mirror/— live-socket check of the productionnetwiring.tests/e2e/mcp-server-dap-mirror.test.ts— mock-adapter full stack with a scripted TCP DAP client: full inspection surface, control rejection leaves the session paused, token rejection, two concurrent clients, idempotency, endpoint closed after unexpose/close.stopped, real locals,evaluateforwarded,continuerejected with the session still agent-controlled, unexpose →ECONNREFUSED, and the token appears in zero log files (proxy log, DAP trace, debugpy logs).Out of scope (per the spike's phasing)
v2 breakpoint union and opt-in execution control; per-language polish (js-debug child-session guarantees, JDI bridge notes) — the mirror routes through
sendRequest, so js-debug child routing already works, but v1 makes no per-adapter guarantees beyond the tested mock + Python paths.Closes #217
🤖 Generated with Claude Code