Audience: Chisl / AionUi operators who want a remote OpenCode server to report tool calls, inject dynamic system context, stream shell output, and route permissions through their local AionCore instance. Plugin package:
@chisl/chisl-opencode-plugin(AionUi/packages/opencode-plugin/, pinned at@opencode-ai/plugin@1.16.2). Wire protocol: v1 — see AionCorePROTOCOL.md(Chisl Plugin Channel section, last verified 2026-06-10).
The plugin is the control plane for a remote OpenCode server. The MCP
bridge (local_fs_mcp) stays the data plane — file reads, file writes,
and the synchronous shell tool. The plugin adds:
- Context injection. AionCore pushes system-prompt strings to the
plugin over SSE; the plugin injects them into the chat
(
experimental.chat.system.transform, with a defensivechat.messagesynthetic-part fallback that disables itself the first time the system hook fires). - Tool audit. Every tool call (
tool.execute.before/tool.execute.after) and a curated subset of OpenCode lifecycle events (file.watcher.updated,session.idle,message.part.updated) are forwarded to AionCore and stored in a per-agent 500-entry ring buffer for the renderer-side status panel. Rawargsandoutputare capped in transit and never persisted; only a redactedsummaryreaches storage. - Permission routing. OpenCode's
permission.askhook dials AionCore for a decision. The MVP policy is"ask"passthrough — AionCore returnsstatus: "ask"and OpenCode's native flow continues. A richer policy engine is a documented follow-up. - Streaming shell. A custom
run_shell_streamingtool streams command output (stdout + stderr) from the local AionCore back to the remote OpenCode host over SSE, gated through the sameShellApproverthe local fs MCP uses. Default timeout 120 s (hard cap 1 h); output capped at 4 MiB.
The plugin runs in-process in the OpenCode server. It executes
nothing locally destructive — all shell execution happens on the AionCore
side, behind the ShellApprover and audit ring buffer.
- OpenCode ≥ 1.16.x on the remote server (the plugin is pinned to
@opencode-ai/plugin@1.16.2; the SDK version pin lives in<chisl-root>/opencode-sdk-version.json). - A network path from the remote server back to the local machine.
The remote plugin dials into your local AionCore, not the other way
around — same direction the local fs MCP uses. AionCore publishes its
plugin webserver on the first reachability candidate of the host's
local IP discovery (loopback for the
AIONUI_LOCAL_FS_MCP_PUBLIC_URLoverride path). - Stable plugin port. The dial-back port must not change when you
restart Chisl. Set
AIONUI_PLUGIN_PORTon the Chisl / AionCore process to the port already in your remoteAIONCORE_URL(Docker, systemd) — you do not reconfigure the remote server. After the first successful bind, AionCore also persists the port under{data_dir}/plugin-listen-portand reuses it automatically. AIONUI_LOCAL_FS_MCP_PUBLIC_URLoverride (only when the auto- discovered URL is wrong): set this env var on the AionCore process to pin the advertised URL — typical when running AionCore behind a tunnel (Tailscale, Cloudflare), inside a container with a published port, or on a host whose primary interface isn't reachable from the remote server.
# Example: AionCore runs in a container with port 4111 published as
# https://chislcore.tail-net.ts.net on the LAN.
export AIONUI_LOCAL_FS_MCP_PUBLIC_URL=https://chislcore.tail-net.ts.netThe AionUi "Install Plugin" button in the remote-agent card reads
this same override when it builds the endpoint_url shown in the
install card.
- In AionUi, open the Remote Agents panel and pick the OpenCode agent you want to wire up.
- Click Install Plugin. AionCore mints (or returns) a per-agent
bearer token and shows an install card with:
- The endpoint URL (e.g.
http://192.168.1.10:4111/). - A config snippet (a single
{"plugin": ["@chisl/chisl-opencode-plugin"]}JSON object — copy into~/.config/opencode/config.jsonunder thepluginkey). - An env snippet:
AIONCORE_URL=http://192.168.1.10:4111/ AIONCORE_TOKEN=<token> - A live status indicator (green = connected within the last 60 s or the SSE stream is open; amber = helloed but not currently streaming; grey = no contact).
- The endpoint URL (e.g.
- Paste the env vars into the OpenCode process's environment (or
wrap the command with
env $(cat chisl.env | xargs)). The plugin readsAIONCORE_URLandAIONCORE_TOKENat load time — no config- file edit is required if you set the env vars. - Restart
opencode serve(or whatever supervises the OpenCode process). Watch the status indicator — it should turn green within 10 s of restart. If it stays grey/amber for more than a minute, see the Troubleshooting section.
# 1. Install the plugin alongside opencode.
npm i -g @chisl/chisl-opencode-plugin@1.16.2
# 2. Set the env vars AionCore gave you. Persist them in your shell
# rc or systemd Environment= so they survive a restart.
export AIONCORE_URL=http://192.168.1.10:4111/
export AIONCORE_TOKEN=00000000-0000-4000-8000-000000000001
# 3. Restart opencode. The plugin loads on next opencode start.
sudo systemctl restart opencode # or however you supervise itVerify:
# opencode serve logs should show the plugin loading with no errors.
journalctl -u opencode -n 50 | grep -i chisldocker run -d --name opencode \
-e AIONCORE_URL=http://host.docker.internal:4111/ \
-e AIONCORE_TOKEN=00000000-0000-4000-8000-000000000001 \
-p 4096:4096 \
opencode/opencode:latesthost.docker.internal resolves to the Docker host's loopback on Docker
Desktop and to the gateway IP on Linux. If the OpenCode container runs
on a different host than AionCore, replace host.docker.internal with
the AionCore host's LAN IP, or publish the AionCore port with
docker run -p 4111:4111 … and point the plugin at the published host.
If AionCore runs in a container too, put both containers on the same
user-defined bridge network and use AionCore's container name as the
hostname. The AIONUI_LOCAL_FS_MCP_PUBLIC_URL override is the safest
way to pin the URL the plugin should dial in either case.
# /etc/systemd/system/opencode.service
[Unit]
Description=OpenCode
After=network.target
[Service]
Type=simple
User=opencode
Environment=AIONCORE_URL=http://192.168.1.10:4111/
Environment=AIONCORE_TOKEN=00000000-0000-4000-8000-000000000001
ExecStart=/usr/local/bin/opencode serve
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now opencode
sudo journalctl -u opencode -f # watch the plugin loadThe token is per-workspace (per remote_agent_id row). Rotate it from
the AionUi remote-agent card or by hitting the AionCore REST endpoint
directly:
curl -X POST http://localhost:4111/api/remote-agents/<agent-id>/plugin/rotate-tokenThe response carries the new token in full exactly once — copy it
into the OpenCode process's environment and restart. The old token sees
401 Unauthorized on its next request; the plugin's SSE loop
reconnects with exponential backoff as soon as the new env vars are
loaded. The webserver's validator reads the same SQLite row, so the new
token is visible to the next inbound request without a restart.
401 Unauthorized on the plugin webserver.
The token is stale (rotated, or pasted wrong). Re-open the AionUi
install card, copy the current AIONCORE_TOKEN, restart OpenCode.
Status indicator stays grey forever; no hello lands in AionCore.
The remote OpenCode server cannot reach AIONCORE_URL from its
network. Check:
curl $AIONCORE_URL/global/healthfrom the OpenCode host returns{"healthy": true, …}. If it doesn't, AionCore is not reachable — fix the URL or the firewall.- The
AIONUI_LOCAL_FS_MCP_PUBLIC_URLoverride is set on the AionCore process (not the OpenCode process). The override is for advertising AionCore's URL; the plugin is the dialer. - The OpenCode host can resolve AionCore's hostname (try the IP instead if DNS is suspect).
- The port is open in both directions (the webserver binds to
0.0.0.0:4111in the default path; the firewall must allow inbound from the OpenCode host).
Plugin not loading — opencode serve logs show no chisl line.
The plugin package isn't on the OpenCode host's load path. Confirm:
ls $(npm root -g)/@chisl/chisl-opencode-plugin/dist/index.jsRe-run npm i -g @chisl/chisl-opencode-plugin@1.16.2 and restart
opencode serve.
Status indicator is amber (helloed but no SSE stream).
The plugin sent a hello (so URL + token are right) but the SSE
stream isn't currently open. The most common cause is the OpenCode
host's outbound HTTP/1.1 keep-alive getting closed by a stateful
firewall between the two. The plugin auto-reconnects with exponential
backoff; if the indicator stays amber for more than a minute, check
the opencode logs for SSE error events.
Permission.ask prompts appear in OpenCode's native UI, not in
AionUi's Approvals queue.
That is the expected MVP behaviour — AionCore responds with
status: "ask" and OpenCode's native flow takes over. AionUi shows
the same prompt via the existing permission.asked SSE event
handler (see AionCore PROTOCOL.md → Permission / question). A
AionCore-side policy engine is a follow-up plan.
- Unset
AIONCORE_URL/AIONCORE_TOKENfrom the OpenCode process environment. - Remove the
{"plugin": ["@chisl/chisl-opencode-plugin"]}entry from~/.config/opencode/config.json(if you added it). - Restart
opencode serve. - (Optional) Rotate the token in AionUi so a stale install can't re-attach.