wiki-plugin-terminal
v0.9.24
Published
Federated Wiki plugin — a live shell terminal in the page; successor to wiki-plugin-shell
Maintainers
Readme
Federated Wiki - Terminal Plugin
A live shell terminal in a wiki page — the successor to
wiki-plugin-shell,
keeping its item text semantics and run-and-capture model, adding an
interactive xterm attached to named pty sessions.
Design: Terminal Plugin
How it works
- Client (
src/client/terminal.js): bundles @xterm/xterm with the fit addon, styled for the 400px wiki column. Probes the local pty service; unreachable → code-style display only (public servers), reachable → run / terminal / send / zoom / tab toolbar. - Service (
service/terminal_service.py): FastAPI router owning ptys keyed by session name. Sessions survive detach (tmux semantics) so the column view, the zoom overlay, and the fullscreen tab share one shell. Spawned zsh emits OSC 133 shell-integration markers; the client scanner turns them into per-command{output, exit}results fired as theterminal-resultevent on the item. - Wiki server (
server/server.js): intentionally a no-op — live capabilities are local-first only.
Build
npm install
npm run build # clean + test + esbuild → client/terminal.{js,css}
npm run dev # watch modeInstall into a local wiki farm
ln -s $(pwd) <wiki>/node_modules/wiki-plugin-terminal
# add "wiki-plugin-terminal": "^0.1.0" to <wiki>/package.json dependencies
# restart the wiki serverWire the pty service into the local FastAPI app
cp service/terminal_service.py <fastapi-app-dir>/
# in main.py:
# from terminal_service import router as terminal_router
# app.include_router(terminal_router)Or standalone: uvicorn terminal_service:app --host 127.0.0.1 --port 4288.
The client first looks for the service at http://terminal.localhost (and https://) — a Caddy route on the default ports, per the hitchhiker 42xx port policy. When nothing answers there it tries http://terminal.localhost:4288 directly (since 0.9.5), which is where the Dojo installer runs the service on a newcomer's laptop with no proxy in front. Whichever answers is used for every later call. Browsers resolve *.localhost to loopback on their own, so no hosts-file entry is needed. Port 8000 was abandoned: Bitfocus Companion owns it; 4248 is avoided because other local services use it.
Safari may refuse to let an https page reach plain-http loopback; a page forked into your own local wiki is a local page and works in every browser.
Succession
Legacy shell items can be redirected to this plugin via wiki-client's
window.pluginSuccessor table (shell: 'terminal' — one line, precedent
mathjax: 'math'). factory.json carries a forward-looking
"supersedes": ["shell"] field proposing that the server aggregate
succession declarations from plugins into /system/factories.json.
PAGE: a run whose output is a page
A PAGE directive (it implies RUN) treats the run's stdout as page JSON — {title, story, journal?} — and opens it as a ghost page beside the page the item is on, the way the Voice Tool and the Plan Tool open theirs. Fork the ghost to save it. A ghost whose title names a page already on the site is written over that page in place; a script that refreshes a report sends the page's existing journal plus one create action carrying the new story, so the previous report stays one click back in the journal (wiki-client's revision replay restores a whole story from a create), rather than one step per paragraph. ~/bin/pi5-disk-report --page is the first such script.
TIMEOUT: a longer one-shot run
A run from the toolbar (or a RUN/PAGE item, or a workflow step) is a one-shot capture, and the service stops it after 30 seconds. An item that needs longer says so in a leading directive: TIMEOUT: 300, TIMEOUT: 300s, TIMEOUT: 5m or TIMEOUT: 2 min. The service clamps the ask to its own ceiling, 600 seconds unless WIKI_TERMINAL_RUN_TIMEOUT_MAX says otherwise, and the reply names the limit it used and whether the ask was cut down. The client caps the ask at 600 seconds as well, so a page never asks a service older than 0.9.14, which accepts any timeout, for more than a new one would give. A page can ask for time; it cannot grant itself any. Work longer than the ceiling belongs in a live session (BUTTON with SESSION), which has no limit. When a run does time out, the message says which of the two to reach for.
Security
The pty service is remote code execution by design. It binds to
127.0.0.1; Caddy proxies terminal.localhost to it from loopback only,
and it is simply absent on public servers, where the plugin renders
display-only. Two trusts decide whether a script runs, and they are kept
apart.
Page trust — whose text may run. A page of a local site (localhost,
*.localhost, or the local mirror farm, where the wiki-security-author
client sets window.isLocalMirror) runs as before. A page from any other
site runs only if that site is listed in
~/.config/wiki-plugin-terminal/trust.json ({"sites": ["plan.ide.earth"]},
mode 600, reloaded on change) AND the service, fetching the page from that
site itself, finds the item text byte-equal to what the browser sent.
Publication is the signature: only the site's owner can publish there. A
trusted page's toolbar wears a tick and the site's name (a shield for a
signed item, a cross for a copy that does not match); a stale or edited copy
is refused. Directives travel with the text, so a page cannot strip or alter a
GUARD, and service: is honoured on local pages only.
Farm trust — the farm default. Listing every site by hand does not
scale on a farm where almost every wiki carries a plan page, so trust.json
also takes farms: {"farms": ["https://fab.fish"]}. A farm anchor is a
site of your own farm; the service reads the farm's site list from the
anchor's /system/api.json (x-fedwiki-farm.sites, Farm plugin ≥ 0.10) and
trusts every site on it as if it were in sites. The list is your box's word,
so a stranger's site cannot claim its way in; it is refreshed every ten
minutes, and an anchor that does not answer contributes nothing. Sites of
other people on your farm are still verified by publication, but they are
trusted — name the ones you do not want under except:
{"farms": ["https://fab.fish"], "except": ["lena.solidarity.quest"]}. Or trust
only the sites one login owns there — {"farms": [{"anchor": "https://fab.fish",
"owner": "david"}]} — which the service reads from the farm's
/system/api/farm/sites.json?owner=david (Farm plugin 0.10.2), so a new site
of yours is trusted the moment its owner.json says so and nobody else's ever
is. CORS and the consent popup follow the same list, per request, so no restart.
Keyboard trust — the person consents. A request from a non-local
origin (the public page itself, opened in a browser on this machine) also
needs a grant: the toolbar shows a closed padlock and no run button. The
person clicks the padlock, a popup on terminal.localhost asks whether that
origin may run verified scripts for 30 minutes, and hands the page an
origin-bound HMAC token by postMessage. The padlock opens and the run
button appears; clicking the open padlock forgets the grant for that tab.
The token lives in that tab's sessionStorage, is sent as a bearer and as
the first websocket frame, and dies with the service (the secret is minted
at start). Never a cookie. Nothing runs on view: a GUARD on a non-local page is a button, not
a poll.
Standing Unlock — your own sites need no padlock (0.9.24). An owner
entry under farms may add "standing": true:
{"farms": [{"anchor": "https://fab.fish", "owner": "david", "standing": true}]}.
Every site that owner has on that farm then passes the keyboard trust with no
grant, in every tab and across restarts; the padlock shows open and says so.
Publication is still checked on every run, except still removes a site, a
whole-farm entry (no owner) never stands, and Lock everything on the
Unlock page pauses the standing Unlock until you press Resume there or the
service restarts. Turn it off for good by removing the flag.
Claude sessions, read-only. GET /terminal/agents runs claude agents
--json on this machine and returns the sessions plus, for each pid the wiki
started, the name of its terminal session. It never returns a process
environment, and no text from the page runs, so it needs only an allowed
origin and the Unlock (or a standing one). The Plan Tool's Claude Sessions
face uses it.
Signed items — trust that follows the author. Since 0.9.0 an item may
carry a signature ({alg: "bip340", pubkey, created_at, sig}): the item's
text, directives included, signed as a nostr event (NIP-01, kind 30078, tags
d=fedwiki-item and type=terminal) with a BIP-340 key shown as an npub. A
key listed under keys in trust.json ({"npub1…": {"id": "david"}})
passes offline on any site, with no page fetch, and the toolbar reads
signed by david; an edited text reads signature stale. The signer is a
person: wiki-sign-item keygen --trust makes a key into the login Keychain,
wiki-sign-item sign <site> <slug> signs a page's terminal items,
wiki-sign-item verify checks them. service/bip340.py is the verifier,
pure Python, proven against the official vectors.
The health probe remains the backstop — no service, no toolbar — and the browser adds its own consent when a public https page first reaches a loopback address (Chrome's Local Network Access prompt). The Terminal Trust Plan on the private security wiki records the audit this answers.
License
MIT
