{"id":"fac8d656-415a-4237-8331-3b5e374bdbc8","slug":"dsh-session-bridge","kind":"mcp","name":"dsh-session-bridge","summary":"Hand one DeepSeek Harness session to another Agent (Codex, Trae, ...) over a loopback HTTP bridge: transcript, live status, pending question, option answers and slash commands - plus a one-click hando","owner":"Ote65","repoUrl":"https://github.com/Ote65/dsh-session-bridge","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-14T10:14:46.000Z","addedAt":"2026-10-21T09:52:03.737Z","logoUrl":"https://avatars.githubusercontent.com/u/245381087?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["utilities"],"tags":["agent","agent-bridge","codex","cordis","deepseek","deepseek-harness","dsh","handoff"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T07:36:26.920Z","sample":false,"url":"https://conduits.dev/tools/dsh-session-bridge","language":"JavaScript","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# dsh-session-bridge\n\n**Hand one DeepSeek Harness session to another Agent — and let it read, and act, from anywhere.**\n\n> 📽️ **Animated introduction:** open [`docs/index.html`](docs/index.html) in a browser.\n> No build, no dependencies — one self-contained file that loops through the whole\n> flow: open `⋯` → **Expose session** → the handoff prompt lands on your clipboard →\n> Codex / Trae connects over loopback HTTP → reads the transcript → sees the pending\n> question → answers it → **reports back and stops**.\n\n[![test](https://github.com/Ote65/dsh-session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/Ote65/dsh-session-bridge/actions/workflows/test.yml)\n![license](https://img.shields.io/badge/license-MIT-blue)\n![plugin](https://img.shields.io/badge/DeepSeek%20Harness-plugin-4176e6)\n\nA DeepSeek Harness (DSH) session usually lives behind the desktop UI. This plugin\npublishes **one** session over a loopback HTTP bridge and puts a paste-ready handoff\nprompt on your clipboard, so Codex, Trae, or any other Agent can:\n\n- **see the conversation** — the full event log, incrementally, from any `seq`\n- **see whether it is running** — live status plus the current event cursor\n- **see what it is blocked on** — the pending `ask_user_question` with its options\n- **choose an option** — the answer is steered back in as a user message, so the session resumes\n- **run a slash command** — `/compact`, `/goal`, and anything else the session accepts\n- **know when to stop** — the prompt tells the receiving Agent to read, report, and stop\n\n```\n        ┌─────────────── DSH (desktop) ───────────────┐\n        │  Session row   ⋯  →  \"Expose session\"       │\n        │        │                                    │\n        │        ▼  copies a handoff prompt           │\n        │   clipboard ────────────────────────────┐   │\n        │        │                               │   │\n        │        ▼                               ▼   │\n        │   ┌────────────────────────────────────────┐│\n        │   │  loopback bridge  127.0.0.1:19388      ││\n        │   │  /info /status /transcript /pending     ││\n        │   │  /answer /command            (POST)     ││\n        │   └────────────────────────────────────────┘│\n        └───────────────────┬─────────────────────────┘\n                            │  plain HTTP, no cookie, no header\n                            ▼\n                  ┌───────────────────┐\n                  │  Codex / Trae / … │  reads, decides, answers, stops\n                  └───────────────────┘\n```\n\n---\n\n## Install\n\n### One paste, and the Harness installs it for you\n\nCopy the prompt below into DSH. It finds this repository, installs the plugin into the\nactive profile, verifies the result, and **stars the repository**. The landing page ships\nthe same block behind a copy button.\n\n<sub>The prompt is in English on purpose: it is the instruction handed to an Agent.\nEverything else in this repository is English too.</sub>\n\n```text\nInstall the DeepSeek Harness plugin `dsh-session-bridge` from the repository below, then star it.\n\nrepository: https://github.com/Ote65/dsh-session-bridge\n\n1. Read the repository README first, then install it into the ACTIVE profile the same way\n   that profile installs third-party bundles. On the desktop profile that is the plugin\n   manager's bundle install - do not hand-edit the profile's config files.\n2. Verify: the \"...\" menu of a session row in the left sidebar gains one more row.\n   If the plugin manager reports \"did not activate\", stop and report the exact error\n   instead of guessing at the configuration.\n3. Once installed, star the repository on GitHub (for example\n   `gh api -X PUT /user/starred/Ote65/dsh-session-bridge`) and say whether the star succeeded.\n\nDo not push, publish, or open a pull request unless I ask.\n```\n\n### Manual (desktop profile)\n\n```bash\ngit clone https://github.com/Ote65/dsh-session-bridge.git\n```\n\nThen add the bundle through the profile's plugin manager, pointing at the checkout\n(or add a `link:` dependency plus the bundle name to the profile's `package.json`).\nThe plugin ships two halves — `lib/index.js` (Host) and `lib/client.js` (Client) —\nand `cordis.patch.yml` inserts its single row.\n\n**Restart DSH after the first install.** The Host half is a Node module and is not\nreliably hot-reloaded; the Client half is.\n\n---\n\n## Use\n\n1. Hover any session row in the left sidebar and open its **`⋯`** menu.\n2. Choose **Expose session** (the row between *Fork* and *Archive*).\n3. The row turns green — **Copied** — and a handoff prompt is on your clipboard.\n4. Paste it into the other Agent.\n\nThe prompt begins with the contract:\n\n```text\n⚠️ Read first, decide once, stop after reading.\n  · GET the discovery document. HTTP 200 means you are connected.\n  · GET the transcript and understand the context.\n  · GET the pending question; if there is one, answer it with POST.\n  · Then STOP and report back to your user — do not fire off commands on your own.\n  · On 401/404/unreachable: stop too, report the status code, and do not go looking\n    for the auth implementation, other URLs, or open ports. That path is a dead end.\n```\n\n---\n\n## API\n\nEverything is reachable with `curl` — **no cookie, no header, no token**. The bridge id\nin the URL *is* the credential.\n\n| Route | Method | Purpose |\n|---|---|---|\n| `/session-bridge/expose?sessionId=…` | GET | mint (or reuse) an exposure; returns the discovery document + prompt |\n| `/session-bridge/info?bridge=…` | GET | discovery document (cwd, model, `seq`, endpoints) |\n| `/session-bridge/status?bridge=…` | GET | live? running? current `seq` |\n| `/session-bridge/transcript?bridge=…&since=N` | GET | the conversation, incrementally |\n| `/session-bridge/pending?bridge=…` | GET | the pending question **with its options**, or `null` |\n| `/session-bridge/answer` | POST | `{ bridge, callId?, answers: [{ id, selected: [label], custom? }] }` |\n| `/session-bridge/command` | POST | `{ bridge, line: \"/compact\" }` |\n| `/session-bridge/revoke?bridge=…` | GET | drop one exposure |\n\n### The cursor contract\n\n`Session.snapshotEvents(fromSeq, toSeqExclusive)` is **half-open**, and `session.seq`\nis the **next** offset (one past the last event). So:\n\n> Send the `seq` from one response back as the next request's `since`.\n\nNo `+1`, no guessing.\n\n### Example\n\n```bash\ncurl \"http://127.0.0.1:19388/session-bridge/info?bridge=$BRIDGE\"\ncurl \"http://127.0.0.1:19388/session-bridge/transcript?bridge=$BRIDGE&since=0\"\ncurl \"http://127.0.0.1:19388/session-bridge/pending?bridge=$BRIDGE\"\ncurl -X POST \"http://127.0.0.1:19388/session-bridge/answer\" \\\n  -H 'content-type: application/json' \\\n  -d \"{\\\"bridge\\\":\\\"$BRIDGE\\\",\\\"answers\\\":[{\\\"id\\\":\\\"1\\\",\\\"selected\\\":[\\\"Submit first\\\"]}]}\"\n```\n\n---\n\n## How it works (and the traps it avoids)\n\n### Its own loopback listener, not DSH's web server\n\nDSH's web server gives a plugin exactly two outcomes, and neither is usable by an\noutside caller:\n\n| Registered at | What an external `curl` gets |\n|---|---|\n| outside `/api` | **404** — only the `/api` subtree is dispatched to plugin routes |\n| inside `/api` | **401 `unauthorized`** — behind the renderer's signed-cookie gate |\n\nSo the bridge runs **its own `node:http` listener** on `127.0.0.1:19388`, sets\n`access-control-allow-origin` (the renderer lives on `dsh-app://app/`, so its call is\ncross-origin), and treats the 128-bit bridge id as the capability.\n\n### The self-check is a real socket\n\nBefore a prompt is handed over, `/expose` fetches **its own URL over a real TCP\nconnection** and refuses to answer unless it gets `200`. An earlier build used the\nHost's global `fetch`, which can be served internally and reported `200` for a URL no\nexternal process could reach — a false green that shipped a dead link. The self-check\nis deliberately `node:http`, and the failure is surfaced as `HTTP <code>` in the row's\ntooltip instead of a broken handoff.\n\n### What is *not* here\n\n**Free-text prompts.** `subagents.prompt` looks like the door, but its contract is\nparent↔child only (`parentSessionId` + `childSessionId` + `mode: 'continuable'`), and\nan outside Agent is neither. The remaining path is the Client half's composer\n(`inputActions`), driven by a poll of this bridge. Until then, `/answer` is how an\nexternal Agent writes into a blocked session — the reply is steered in as a user\nmessage, which also resumes it.\n\n### Read/write surface\n\n| Write | Backed by |\n|---|---|\n| answer options | `userQuestions.answer(agent, callId, { answers })` |\n| run a slash command | `commands.execute(agent, line, [], signal)` |\n\nBoth take an arbitrary live **root agent**, obtained with `ctx.agents.get(sessionId)`.\n\n---\n\n## Security\n\n- Binds **`127.0.0.1` only**. Nothing is exposed to the LAN.\n- The **URL is the credential**: 16 random bytes (128 bits), never the raw session id.\n  An exposed URL cannot be used to enumerate or address other sessions.\n- **Exposure is in memory.** It dies with the plugin; nothing is persisted.\n- `/revoke` drops an exposure immediately.\n- While exposed, a caller who has the URL can read the whole transcript **and** answer\n  the session's questions. Treat a handoff prompt like a password, and revoke when the\n  handover is done.\n\n---\n\n## Configuration\n\nAdd a `config` block to the bundle row in the profile patch:\n\n```yaml\n- id: session-bridge\n  config:\n    port: 19388                                       # loopback listener\n    baseUrl: http://127.0.0.1:19388                   # what the handoff prompt advertises\n    sessionProjectionDir: ''                          # optional; read only for the session title\n```\n\n> Do **not** export a `Config` object from the plugin. Cordis' `resolveConfig` expects a\n> schema with `validate` (Zod-like); a plain object makes activation fail with an error\n> whose stack points only into Cordis. The shipped plugins read config defensively in\n> `apply(ctx, config = {})` instead — this one does the same.\n\n---\n\n## Tests\n\n```bash\nnpm test          # 31 checks: host 12 + client 19, no network\nnpm run test:live # mounts the real handlers on a real port and drives them over HTTP\n```\n\nCovered: the eight routes, the exposure lifecycle, the discovery document, the\nincremental transcript and its cursor contract, `pendingQuestion` derivation, the\nself-check rollback (a URL that is not `200` is never handed over), service-unavailable\nand not-live paths, the keyed menu registration and its `name` field, the radar glyph\n(four primitives, no emoji), the clipboard write and its fallback, and the\ndismiss-the-menu-only-after-confirming behaviour.\n\nCI runs both suites on Node 22 and 24.\n\n---\n\n## Repository layout\n\n```\ndsh-session-bridge/\n├── lib/\n│   ├── index.js        Host half — the loopback listener, the eight routes\n│   └── client.js       Client half — the \"⋯\" menu row, the radar glyph, the clipboard\n├── tests/\n│   ├── run.mjs         the suites\n│   ├── host.test.mjs   routes, exposure lifecycle, cursor contract, self-check\n│   ├── client.test.mjs menu seat, glyph, clipboard, menu dismissal\n│   └── live.mjs        mounts the real handlers on a real port and drives them over HTTP\n├── docs/\n│   └── index.html      the animated introduction (self-contained)\n├── cordis.patch.yml    the single bundle row\n└── package.json\n```\n\n---\n\n## License\n\n[MIT](LICENSE)\n"}