Files
JWeBot/docs/extension-host.md
T

5.9 KiB

Extension Host

Status: signed-release Host and local Edge routing integration implemented.

The Edge supports in-process, prefer-host, and host-only execution modes. The production default remains in-process; the Host is not installed or enabled on production host.

Process Model

One host process:

  1. receives an explicit verified release directory;
  2. re-verifies installed release content, record, and publisher signature;
  3. checks the publisher against an explicit OpenSSH allowed-signers file;
  4. re-validates manifest and source policy;
  5. loads one plugin entrypoint;
  6. freezes declared command registration;
  7. reads one bounded JSON request from stdin;
  8. invokes one command;
  9. exchanges bounded service requests with the Kernel when declared;
  10. writes one final JSON response to stdout;
  11. exits.

Plugin stdout is redirected away from the protocol stream. Plugin stdout and stderr are bounded before reaching the parent pipe.

Request

{
  "op": "invoke_command",
  "command": "/hello",
  "arguments": "world",
  "event": {
    "version": 1,
    "event_id": "event-1",
    "observed_at": "2026-10-14T10:00:00+08:00",
    "sender": "JWeBot",
    "text": "@JWeBot /hello world",
    "confidence": 1.0,
    "sender_confidence": 1.0,
    "body_confidence": 1.0,
    "side": "incoming",
    "mention_bot": true,
    "group_fingerprint": "opaque-fingerprint",
    "source": "rapidocr-local"
  }
}

The request contains no delivery target. The group fingerprint remains an opaque safety binding for the Kernel.

Response

{
  "status": "ok",
  "api_version": "1",
  "plugin_id": "hello-command",
  "result": {
    "api_version": "1",
    "reply": {"kind": "text", "text": "hello JWeBot world"},
    "facts": [],
    "metadata": {}
  }
}

Errors return a short error class and do not include plugin exception text.

Declared service calls use an intermediate JSON-line exchange:

{
  "status": "service_request",
  "call_id": "1",
  "profile": "coin",
  "operation": "execute",
  "payload": {"command": "/翻硬币"}
}

The parent validates the profile, performs the Kernel-owned operation, and returns a matching service_result. Calls, message sizes, and total wall time are bounded. An undeclared profile, mismatched call ID, malformed result, or unavailable broker fails closed.

Context Capabilities

The prototype provides:

  • manifest-bound command registration;
  • declared, bounded Kernel service profiles;
  • bounded fact collection;
  • hashed logger messages with field names only.

The following brokers intentionally fail closed until implemented:

  • storage;
  • approved HTTP;
  • scheduler.

No GUI sender, group selector, raw ledger, environment, shell, SSH, Docker, or arbitrary network client is supplied.

Environment Handling

Before loading extension code, the CLI removes environment variables except LANG, LC_ALL, and TZ.

This reduces accidental secret exposure but is not a security sandbox. The parent interpreter also sets PYTHONDONTWRITEBYTECODE=1, and the loader disables bytecode writes while importing a plugin, so verified release directories cannot acquire unrecorded __pycache__ files.

Required Production Isolation

Before Edge integration, the host still needs an OS-enforced launcher with:

  • dedicated dynamic or unprivileged identity;
  • read-only verified release directory;
  • private writable state directory;
  • no inherited secrets;
  • no network by default;
  • restricted address families;
  • no new privileges and empty capability set;
  • process, memory, CPU, and wall-time limits;
  • parent-enforced timeout and kill;
  • fixed Unix-socket or pipe protocol.

Static source checks and Python-level context restrictions do not replace these controls.

Repository templates:

deploy/systemd/system/jwebot-extension-host@.service
deploy/systemd/system/jwebot-extension-host@.socket

They specify a fixed unprivileged jwebot-ext identity, private AF_UNIX-only network namespace, empty capabilities, read-only system and release paths, private devices/tmp, syscall filtering, and CPU/memory/process/runtime limits. The service refuses to start unless /etc/jwebot-extension-host/allowed_signers is readable and passes that trust file to the Host. No private signing key belongs on the target.

The templates are not installed or enabled. A later provisioning phase must create the service identity, install the Host virtual environment, prepare release ownership and public-key trust file, run systemd-analyze verify, and test the socket protocol on the target Debian version.

Parent Client

jwebot_kernel.ExtensionHostClient starts one Host in a new process group with:

  • an explicit verified release directory;
  • python -I by default;
  • only LANG, LC_ALL, and TZ inherited;
  • closed unrelated file descriptors;
  • bounded request/response/stderr sizes;
  • a parent-enforced wall-time timeout;
  • process-group kill on timeout.

The parent discards Host stderr from protocol results and returns generic transport errors. It services only reviewed broker callbacks supplied by the Kernel.

At Edge startup, signed manifests are loaded from <state-root>/<plugin-id>/current, checked against the fixed ownership inventory, registered into one catalog, and frozen. Host health responses must exactly match the signed author, commands, service profiles, plugin ID, and API version.

prefer-host may use the in-process adapter only when a signed release is missing or its Host fails startup health. Once a Host invocation starts, the Kernel never replays that command through another path because the broker may already have committed state. host-only requires every reviewed plugin to be signed, installed, and healthy before Edge startup succeeds.

jwebot_kernel.UnixExtensionHostClient matches the systemd socket template. It uses the same bounded JSON-line service exchange and applies one end-to-end socket deadline. Socket absence, timeout, malformed JSON, service protocol violations, and Host error status all fail closed.