Skip to content

TypeScript/QuickJS extension runtime for user-defined tools, commands, and hooks #3

@quangdang46

Description

@quangdang46

TypeScript/QuickJS extension runtime for user-defined tools, commands, and hooks

Summary

Add a first-class extension runtime so users can extend jcode with TypeScript/JavaScript modules without forking the codebase. Extensions register custom tools, slash commands, keybindings, event hooks, providers, status-line widgets, and UI components. The runtime is embedded (QuickJS) — no Node, no Bun, no V8 — so it stays single-binary and starts in <100 ms.

References:

Why

Current state in jcode

  • No QuickJS / JS / TS runtime is embedded. Grep finds zero matches for QuickJS, quickjs, plugin_runtime, extension_runtime in src/.
  • src/tool/skill.rs loads Markdown skills only.
  • MCP exists for tools, but MCP can't register slash commands, hotkeys, or UI hooks — only tools.

Implementation checklist

1. Pick a runtime

  • Use rquickjs (Rust bindings to QuickJS), feature-gated behind extensions. Rationale: single-file embed, ~700 KB binary cost, no system Node dependency, mature ECMAScript 2020 support. Native AOT is out of scope.
  • Add to a new crate crates/jcode-extensions/ (lib + runtime + hostcall mux).

2. Capability-gated hostcall protocol

  • Define a hostcall namespace mirroring pi_agent_rust: tool, exec, http, session, ui, events.
  • Each extension declares required capabilities in its manifest (pi.json / jcode.json). Missing caps = the host returns EPERM synchronously.
  • Profiles: safe (default), balanced, permissive (mirror pi_agent_rust's --extension-policy).

3. Extension manifest + discovery

  • Discovery order (highest-priority first):
    1. $JCODE_EXT_PATH (colon-separated)
    2. .jcode/extensions/ (project)
    3. ~/.jcode/extensions/ (user)
    4. Bundled in jcode install npm:... / jcode install git:... packages (see separate issue)
  • Manifest schema (TypeScript-friendly):
    {
      "name": "my-ext",
      "version": "0.1.0",
      "main": "./dist/extension.js",
      "jcode": {
        "capabilities": ["tool", "session", "ui"],
        "minJcodeVersion": "0.13"
      }
    }
  • Resolve main relative to the manifest. Reject paths that escape the extension root.

4. Public TypeScript API

  • Ship crates/jcode-extensions/types/jcode.d.ts so users get autocomplete:
    export interface ExtensionAPI {
      registerTool(spec: ToolSpec): void;
      registerCommand(name: string, spec: CommandSpec): void;
      registerKeybinding(combo: string, handler: () => void | Promise<void>): void;
      registerProvider(spec: ProviderSpec): void;
      on(event: "tool_call" | "tool_result" | "compaction" | "stream_start" | "stream_end", handler: EventHandler): void;
      session: SessionAPI;
      ui: UIAPI;
      exec(argv: string[], opts?: ExecOpts): Promise<ExecResult>;
      http(req: HttpReq): Promise<HttpResp>;
    }
    export default function (jcode: ExtensionAPI): void | Promise<void>;
  • Allow default export to be async — host awaits it before the first prompt is accepted.

5. Lifecycle and reactor

  • Lazy load: extensions are loaded on first use, not at startup. Cold load target: P95 < 100 ms (matches pi_agent_rust).
  • Warm pool: reuse one isolate per extension across calls. Hot reload on file change in dev mode (gated by JCODE_EXT_DEV=1).
  • Bounded queue for hostcalls with backpressure (mirror pi_agent_rust's reactor mesh, but simpler v1: single shard).

6. Trust lifecycle (see separate issue)

  • Track per-extension trust state (pending / acknowledged / trusted / killed). First-run extensions sit in pending and ask the user to acknowledge capabilities. Killed extensions never load.
  • Audit log under ~/.jcode/logs/extensions.jsonl.

7. Built-in extension examples

8. CLI surface

  • jcode ext list, jcode ext info <name>, jcode ext run <name> --help, jcode ext disable <name>, jcode ext enable <name>.
  • jcode doctor (see separate issue) reports loaded extensions, capability profile, and trust state.

Testing

Conformance fixtures

  • Build a fixture suite (tests/extensions/fixtures/*.json) mirroring pi_agent_rust's conformance harness. Each fixture: setup files → load extension → assert tool/command/hook outcomes → assert no capability escapes.
  • Cover the negative cases: capability denial, infinite loop kill via timeout, memory cap exceeded.

Cold/warm load benchmarks

  • Add benches/extension_load.rs (criterion). Gate on P95 cold load < 100 ms, P99 warm load < 1 ms.

Manual smoke

  • Drop the example plan-mode/ extension into ~/.jcode/extensions/; verify /plan works.
  • Drop a malicious try-to-read-/etc/shadow.js; verify safe profile blocks the read and emits an audit log entry.

Acceptance criteria

  • cargo build --release --features extensions produces a binary under the existing 22 MiB ceiling reported in AGENTS.md (or document the new size budget in the same file).
  • Two in-tree example extensions load and register tools/commands without warnings.
  • jcode ext list shows their state.
  • Capability denial returns a structured error to the extension and an audit log line.
  • All new unit and conformance tests pass under cargo test --features extensions.
  • cargo clippy --all-targets --features extensions -- -D warnings is clean.

References



Tracking: physically split into sub-issues

This issue is now an umbrella. Concrete work lives in:

Land in order. Close this issue when all three sub-issues are closed.


Implementation notes addendum (Devin gap-analysis pass, 2026-05-21)

Verified jcode code paths

  • No QuickJS/JS runtime in tree. rg -i 'quickjs|rquickjs|extension_runtime' src crates returns no matches.
  • Closest existing extensibility: src/tool/skill.rs (Markdown skills, semantic-vector-loaded) and src/tool/mcp.rs + src/mcp/ (MCP client for tools).
  • Slash commands are currently registered manually in src/tui/ui_overlays.rs and dispatched in src/tui/app/commands.rs — the extension API must thread through both.
  • Provider registry is in src/provider/ and crates/jcode-provider-* — extension-provided providers will need a stream-simple bridge similar to pi_agent_rust's src/providers/mod.rs.

Scope expansion (must-have before merging this issue)

The original checklist covers the runtime + manifest + capability bucket. Devin's review flags four additional must-have-before-shipping items that pi_agent_rust treats as part of the extension runtime, not as separate features:

  1. Secret-aware env filter for pi.env() / jcode.env() — block any key matching *KEY*, *TOKEN*, *SECRET*, *PASSWORD*, *COOKIE* from the extension-visible env by default. Add an --ext-env-allow=NAME flag for opt-in unblocking. Reference: pi_agent_rust Feature Superset Highlights → "Secret-aware extension env filtering".
  2. Hostcall taxonomy with stable error codes — every hostcall must return one of ok | timeout | denied | io | invalid_request | internal. This makes extension errors actionable and lets jcode doctor (Add general-purpose jcode doctor diagnostic command #8) diagnose them. Reference: pi_agent_rust → "Unified hostcall dispatcher with typed taxonomy mapping".
  3. Preflight static analysis — before loading an extension for the first time, scan its entrypoint for forbidden imports (fs, child_process, net, crypto.subtle.importKey) that should be hostcalls instead, and reject obvious red flags (eval, Function(...), postMessage to unknown targets). Reference: pi_agent_rust → "Extension preflight static analysis".
  4. Hostcall compatibility lane forced-compat switch — a single CLI flag and per-extension setting to force the extension into a slower "compat" lane for emergency rollback if a fast-path bug surfaces. Reference: pi_agent_rust → "Hostcall compatibility-lane emergency controls".

If shipping all four in one PR is too large, consider splitting #3 into:

  • #3a Runtime + manifest + capability declarations
  • #3b Hostcall taxonomy + secret-aware env filter + preflight analysis
  • #3c Forced-compat switch + reactor mesh tuning

Cross-references

Reference URLs

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions