Skip to content

[native] jcode: three-mode alignment (proxy / launcher / native plugin) + runtime-info #962

Closed as not planned

Description

@ranxianglei

Goal

Bring jcode to full three-mode alignment like pi / opencode / dsh already have:

  1. Mode 1 — proxy (URL prefix, exists today via launcher)
  2. Mode 2 — launcher (bili jcode — exists)
  3. Mode 3 — native plugin (bili plugin install jcode — the gap): plugin loaded by the CLIENT itself, spawns/attaches its own proxy, rewrites model traffic, registers ACP tools natively, stamps plugin-mode headers, implements the runtime-info protocol.

Feasibility pre-assessment

UNKNOWN — research first: plugin/extension surface (config rewrite, request headers, native tool registration)? Follow the dsh playbook if yes.

Reference implementations (read these first)

Requirements if feasible

  • No launcher, no env vars, no fixed port for the end user; opt-out env BILI_NATIVE_JCODE=0
  • Plugin-mode gating: stamp x-bili-plugin ONLY after native tools are registered (round 1 rides wire mode)
  • Parent-pid watchdog proxy teardown; attach to a healthy proxy when present
  • Runtime-info stamps (model + contextWindow + maxOutput whenever the client exposes them)
  • npm run typecheck + npm test + npm run build green; new tests for the plugin module (see tests/dsh-native.test.ts for the harness style)
  • README (en + zh) client table + install section updated

If NOT feasible

Post the evidence (what extension points exist, why wire interception / native tools are impossible) and close as wontfix — a documented no is a deliverable too.

For ework

This issue is directly implementable. Follow the dsh playbook; ask in this thread before inventing new protocol surface. PR must reference this issue (Fixes #N).

Activity

  1. ranxianglei commented on Sep 18, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 On it. Kicking off the feasibility pre-assessment: (1) what extension/plugin surface jcode actually exposes (config rewrite, request headers, native tool registration), (2) how close it is to the dsh/opencode/pi playbooks. Will report findings here before writing any plugin code — if the surface is insufficient I'll post the evidence and close as wontfix per the issue's own contract.

  2. ranxianglei commented on Sep 18, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Feasibility assessment in progress. First-order findings:

    • jcode (github.com/1jehuang/jcode) is a compiled Rust binary — unlike pi/opencode/dsh (all Node hosts with in-process extension APIs), there is no JS/TS seam to load a plugin into.
    • Its external surfaces so far: ~/.jcode/config.toml (provider profiles incl. base_url, per-provider static headers table, per-model context_window, extra_body body injection) and MCP client support (~/.jcode/mcp.json, plus Claude Code-compatible .mcp.json).

    That means the two load-bearing pieces of the dsh playbook — in-process traffic rewrite (fetch patch / registerProvider) and per-request dynamic header stamping (conversation id, toolsReady gate, runtime-info stamps) — have no obvious counterpart. Cloning the source now to verify there is no plugin/hook/event system hiding behind that, and to check whether MCP server processes receive session-scoped context (the one channel that could bridge to POST /__bili/plugin/register). Will post the verdict with full evidence shortly.

  3. ranxianglei commented on Sep 18, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Verdict: Mode 3 (native plugin) is NOT feasible for jcode. Per the issue spec, here is the evidence. I cloned 1jehuang/jcode master (Rust workspace, ~80 crates) and audited its entire extension surface at source level.

    Complete inventory of jcode's extension surface

    1. No plugin system — by design. jcode's README states it plainly under "Customizability": "Jcode is inventing a new form of customizability. One that doesn't limit you to what a plugin or extension can do" — its answer is self-dev mode (edit your own fork of the source). It is a compiled Rust binary: no dynamic loading, no JS/TS seam, nothing a Node plugin can hook into.
    2. Lifecycle hooks ([hooks] in ~/.jcode/config.toml, docs/HOOKS.md): turn_end / session_start / session_end / pre_tool (sync gate) / post_tool. External commands receiving env payloads (JCODE_HOOK_SESSION_ID, JCODE_HOOK_PAYLOAD…). Observers/gates only — they can see what happened, but cannot touch request/response bytes.
    3. Spawn hook ([terminal] spawn_hook, docs/SPAWN_HOOK.md): controls where headed sessions open (tmux/kitty pane). Terminal placement only.
    4. MCP client, stdio-only (~/.jcode/mcp.json, .jcode/mcp.json, Claude-compat files): this is the one channel that could register native tools. But three source-level facts kill the native-plugin model:
      • crates/jcode-base/src/mcp/pool.rs: "Manages a global pool of MCP server processes that are shared across all jcode sessions… Sessions get lightweight McpHandle clones" — MCP servers are deliberately de-sessionized: one process pool serves every session (their issue fix: guard side requests against over-window payloads (#554) #557). No per-session lifecycle, so "parent-pid watchdog teardown when the client exits" has no mapping.
      • crates/jcode-base/src/mcp/client.rs (connect_in_dir): child env = inherited daemon env + static config.env from mcp.json. No session-scoped variables are injected (unlike Claude Code's CLAUDE_CODE_SESSION_ID). The MCP shell can never learn which conversation is talking.
      • Env-expansion in config is not implemented anywhere (the unused HTTP-MCP headers field says "retained so environment expansion is ready when those transports are").
    5. Provider profiles (~/.jcode/config.toml): base_url, a static headers table applied verbatim to every request to that provider (crates/jcode-config-types/src/lib.rs:464 — BTreeMap<String,String>, no interpolation; applied at crates/jcode-provider-anthropic-runtime/src/lib.rs:2011), extra_body (static JSON merged into every body), per-model context_window. All of it constant-per-provider, read once at daemon start.
    6. Env vars (HTTPS_PROXY honored by reqwest — exactly how bili jcode's cert-MITM launcher already works; ANTHROPIC_BASE_URL etc.) — process-level, set at launch = launcher territory (Mode 2).
    7. Harness API (NDJSON over unix socket): the client API for UIs/scripts to drive sessions. Not an in-process plugin seam; it cannot intercept model traffic.

    Why each load-bearing piece of native mode fails

    • Traffic rewrite needs one of: in-process fetch patching (impossible — compiled binary, no seam); base_url config rewrite (needs a proxy origin known before jcode's long-lived shared daemon starts — ephemeral ports can't work, fixed ports are excluded by the issue, and there's a startup race: the daemon reads config.toml before any concurrently-spawned "plugin" could bring up a proxy; additionally OAuth providers hard-code official endpoints — "Claude OAuth traffic always continues to use Anthropic's official endpoints" — so subscription users can't be rerouted at all); HTTPS_PROXY MITM (env vars → launcher, i.e. Mode 2). The only candidate "plugin" process (an MCP shell) is a child of the very process whose traffic must be rewritten — it can't patch its parent or set its parent's environment.
    • Per-request dynamic header stamping (x-bili-plugin + x-bili-plugin-conversation, gated on toolsReady): the only header surface is the static per-provider TOML table. A static conversation id would collapse all concurrent jcode sessions into one conversation — cross-session context corruption, and swarm mode (many parallel sessions by design) makes it catastrophic. No gating possible; round 1 could never ride wire mode. Worse, jcode sends no stable per-session identifier on model requests at all (verified: zero session-id headers in the provider runtimes; only a process-level static User-Agent), so even identity-binding (feat: launcher-first UX — native-plugin experience with zero persistent host config (plugin-in-launcher, 内外呼应) #162) has nothing to latch onto.
    • Root structural cause: one jcode daemon process hosts many concurrent sessions, yet its entire extension surface operates at process level (env vars, hooks) or provider level (static config). Native mode requires per-session, per-request control. That mismatch is not bridgeable with the current surface.
    • Runtime-info protocol: the per-request channel inherits the header-stamping impossibility; only the bootstrap POST /__bili/plugin/runtime-info half would be reachable (from the MCP shell), reporting data that correlates to zero routed requests. Mid-session /model switches are invisible to static config.
    • Native tools alone don't cohere: even if the MCP shell registered compress/decompress/acp_status, their calls POST to conversations that no model request ever bound (no x-bili-plugin-conversation on the wire) → 404. And in proxy mode bili injects same-named wire tools itself and drops client-side ones. This is precisely why claude/codex/omp's MCP-shell companions are classified "not native mode" in the README.

    What would make it feasible (upstream jcode)

    Either (a) a request-middleware hook that can mutate outgoing model-request headers/body per session, or (b) per-session context injected into model requests (a stable session id header) plus per-session MCP spawn context. Absent either, native mode cannot be built honestly.

    Disposition

    • Closing [native] jcode: three-mode alignment (proxy / launcher / native plugin) + runtime-info #962 as wontfix per the issue's own spec ("a documented no is a deliverable too").
    • Modes 1 & 2 remain fully intact: /bili/ URL prefix and bili jcode (cert-MITM launcher, api.z.ai whitelisted) — the launcher is the right fit for a binary host with no plugin seam.
    • I'm opening a small docs PR recording this conclusion in the README client table + native-mode notes (en + zh) so future readers/agents don't re-run this investigation. Link in the next comment.
  4. added
    wontfixThis will not be worked on
    and removed
    enhancementNew feature or request
    on Sep 18, 2026
  5. ranxianglei commented on Sep 18, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Done — two housekeeping items on top of the verdict above:

    1. Docs PR: docs: record jcode native-mode infeasibility (#962) #967 — records the conclusion in the README (en + zh): a jcode row in the client table (bili jcode launcher / /bili/ prefix, no native plugin) and a bullet in the native-mode notes explaining why (compiled Rust binary, no plugin seam, static per-provider headers, session-agnostic MCP pool). 11 insertions, docs-only; typecheck + 1881 tests + build all green locally.
    2. Issue closed as wontfix per the spec in the issue body (GitHub's API has no wont_fix state reason, so it's closed not planned + the repo's existing wontfix label). Reopen anytime if jcode upstream lands a request-middleware hook or per-session request context — that would change the verdict.

    Modes 1 & 2 are untouched and remain the supported paths for jcode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    wontfixThis will not be worked on

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions