feat(plugin-pty): PTY_SERVICE — real interactive eliza-code CLI (on cerebras) in the cockpit terminal#10668
feat(plugin-pty): PTY_SERVICE — real interactive eliza-code CLI (on cerebras) in the cockpit terminal#10668NubsCarson wants to merge 15 commits into
Conversation
…the web terminal
The app already ships the whole web terminal — the xterm UI (PtyTerminalPane),
the typed client (spawnShellSession/subscribePtyOutput/sendPtyInput/resizePty),
and the agent-server WS handlers (pty-subscribe/pty-input/pty-output/pty-resize).
Those handlers resolve `runtime.getService("PTY_SERVICE")?.consoleBridge`, which
was always null because nothing registered a PTY_SERVICE. This adds that missing
keystone.
New opt-in plugin @elizaos/plugin-pty:
- PtyService (serviceType "PTY_SERVICE") exposing the consoleBridge the agent
server drives, plus session lifecycle (start/stop/list).
- Routes: POST/GET /api/pty/sessions, DELETE /api/pty/sessions/:id (authenticated).
- buildElizaCodeCerebrasSpec: launches the interactive eliza-code CLI (a real
slash-command TUI we own) pointed at Eliza Cloud's OpenAI-compatible endpoint
so inference runs on cerebras (gpt-oss-120b fast / zai-glm-4.7 smart) — a real
CLI with all slash commands, zero TOS exposure.
Runtime-aware PTY engine: node-pty's write path is broken under Bun
(this._socket.write is not a function), and the agent runs under Bun in dev, so
defaultSpawnResolver uses Bun's native truePty (Bun.spawn({terminal})) under Bun
and @lydell/node-pty under Node — both behind one PtyHandle. (The Bun terminal
`exit` callback reports the PTY-teardown status, always 1; the real exit code is
taken from proc.exited.)
Wiring:
- client: spawnPtySession() -> POST /api/pty/sessions (mirrors spawnShellSession),
then reuse the existing subscribe/input/resize path.
- agent: register @elizaos/plugin-pty as a deferred, opt-in optional plugin
(no autoEnable -> dormant unless a character lists it; disabled on store builds).
Tests: 36 unit tests (bridge routing, streaming, lifecycle, cwd confinement,
session cap, spec builder, bin resolver, route gates/errors) against an injected
fake PTY, plus a gated pty-real.e2e.test.ts. Real-runtime path verified under Bun
(truePty) and Node (node-pty): real process output, keystroke round-trip, exit
code 0, clean session teardown.
Wire the cockpit's "tap-in, drive it directly" pillar to the new PTY_SERVICE:
launch a REAL interactive eliza-code CLI on Eliza Cloud/cerebras from the cockpit
and drive it in the live xterm pane — all slash commands, an agent we own, zero
TOS exposure.
- CockpitInteractiveTerminal: spawns via client.spawnPtySession({kind:"eliza-code",
tier}), mounts the existing self-contained PtyTerminalPane on the returned
sessionId, surfaces spawn errors with a retry, and kills the session on
unmount/close so the REPL never orphans.
- CockpitRoute: a Fast/Smart launch control over the deck opens the terminal as a
full-panel overlay (leaves the presentational CockpitView untouched).
- client: stopPtySession() (DELETE /api/pty/sessions/:id) alongside spawnPtySession.
Tests: 5 new component tests (spawn→attach at tier, cwd passthrough, error+retry,
unmount-kills-session, close→stop+onClose) via the real component path, mocking
only the client boundary + xterm pane. Full plugin suite green (192 tests);
typecheck + view-bundle build clean (xterm stays lazy-split).
Device/pixel verification (real /help on cerebras on-device) is the handoff.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Validation/update for #10668 after reviewing the PTY lane. I found and pushed a cleanup follow-up, then refreshed the branch on current
Security/runtime notes from review:
Focused validation on head
GitHub checks are still queued/UNSTABLE on the refreshed head; no failed check was visible when I posted this. |
|
Review status for current head Blocking findings:
Validation I ran:
|
|
Follow-up after rechecking head The validation cleanup commit did fix the local Biome/typecheck class of issues I called out earlier:
Remaining blockers are still the substantive ones:
One validation note: |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Update on #10668 after Shaw/lalalune review blockers. Pushed head: What changed:
Local validation after merging latest
I also tried the gated real PTY e2e explicitly with a temporary Vitest config. The file was discovered, but it skipped because native |
|
Follow-up: Shaw merged #10658 into Validation after this second develop refresh:
PR remains draft pending live/native/device evidence for the real PTY path. |
|
Follow-up after another Validation after this latest refresh:
Additional real PTY evidence from this machine, using the actual
bun --eval '... store.start({ command: "sh", args: ["-c", "printf BUNPTYHELLO"], ... }) ...'Output: {"ok":true,"engine":"bunTruePty","output":"BUNPTYHELLO","exitCode":0,"sessionId":"f70837ea-0e54-4297-9874-30aa5ef02b0c"}
bun --eval '... store.start({ command: "cat", args: [], ... }); bridge.writeRaw(sessionId, "ROUNDTRIP42\r") ...'Output: {"ok":true,"engine":"bunTruePty","saw":"ROUNDTRIP42","output":"ROUNDTRIP42\\r\\nROUNDTRIP42\\r\\n","sessionId":"58072cb1-9712-416a-bd65-195afa9dfe63"}This now gives local real Bun-native PTY evidence for spawn/output/exit and input round-trip. I am keeping the PR draft for now because the remaining highest-confidence proof is still frontend/device/on-phone evidence for the full cockpit/eliza-code path. |
|
Additional frontend/plugin evidence for the cockpit terminal side:
This includes the cockpit terminal panel tests that drive the pretty/CLI toggle, xterm pane mount, PTY websocket output into the rendered Current head remains |
Deep review — 4-agent sweep (security · correctness/leak · intent/2nd-order · architecture)Reviewed at tip 🔴 Blockers
🟠 Should-fix before merge
🟡 Architecture / duplication (design call)
🧪 Test gapThe "real e2e" ( Is it fixing a surface or the deeper problem?Registering Reviewed by an 8-agent sweep (this PR + the launcher #10669). Happy to open a follow-up PR with the concrete safety patches (blockers 2/3, items 4/7/10) once the auth model (blocker 1) is decided — that decision is yours/the author's, and the fixes need a real device+cluster run to verify, which is why I'm not force-landing them. |
|
Pushed follow-up head What changed:
Validation on this worktree after merging current
Notes:
|
|
Additional browser-origin PTY smoke for current head |
|
Refreshed #10668 onto current Focused validation after refresh:
This intentionally restarts CI but removes the branch-behind-develop state. |
Env-leak HIGH hold — LIFTED ✅Re-reviewed the current committed head (
Footgun before un-drafting: Remaining before ready: (1) squash to drop the stale diff, (2) follow-up to enforce the |
|
Refreshed #10668 onto current Pulled in:
Focused validation after refresh:
Current GitHub state after push: mergeable against |
Re-verified at current head
|
|
Refreshed #10668 again onto current Focused validation after this larger refresh:
No PTY/cockpit conflicts from the merged launcher/icon/cloud-app/app-control changes. CI restarted on the new head. |
What & why
The elizaOS app already ships the entire web terminal — the xterm UI
(
PtyTerminalPane), the typed client (spawnShellSession/subscribePtyOutput/sendPtyInput/resizePty), and the agent-serverWebSocket handlers (
pty-subscribe/pty-input/pty-output/pty-resize). Those handlers resolveruntime.getService("PTY_SERVICE")?.consoleBridge— which was always null,because nothing registered a
PTY_SERVICE. So the terminal was inert.This PR adds that missing keystone and wires it into the coding cockpit: a real
interactive
eliza-codeCLI on Eliza Cloud/cerebras, driveable from the webterminal on any device — all slash commands, an agent we own, zero TOS
exposure (running a real CLI on a subscription is inherently the TOS-unsafe
tier; eliza-code-on-cerebras avoids that entirely).
Changes
New plugin
@elizaos/plugin-pty(opt-in; noautoEnable, so dormantunless a character lists it; disabled on store builds):
PtyService(serviceType = "PTY_SERVICE") exposing theconsoleBridgetheagent server drives + session lifecycle.
POST/GET /api/pty/sessions,DELETE /api/pty/sessions/:id(authenticated).
buildElizaCodeCerebrasSpec: launches the interactiveeliza-codeCLI(
packages/examples/code) pointed at Eliza Cloud's OpenAI-compatible endpoint→ inference on cerebras (
gpt-oss-120bfast /zai-glm-4.7smart).Runtime-aware PTY engine.
@lydell/node-pty's write path is broken underBun (
this._socket.write is not a function— output streams, keystrokes throw),and the agent runs under Bun in dev. So
defaultSpawnResolveruses Bun's nativetruePty (
Bun.spawn({ terminal }), the same engine the Electrobun host uses)under Bun and
@lydell/node-ptyunder Node — both behind onePtyHandle.(Gotcha handled: Bun's terminal
exitcallback reports the PTY-teardown status,always
1; the real exit code is taken fromproc.exited.)Wiring.
spawnPtySession()/stopPtySession()(mirrorspawnShellSession).@elizaos/plugin-ptyas a deferred, opt-in optional plugin.CockpitInteractiveTerminalspawns viaspawnPtySessionand mountsthe existing
PtyTerminalPane;CockpitRoutegets a Fast/Smart launch controlthat opens it as a full-panel overlay (presentational
CockpitViewuntouched).Testing / evidence
plugin-pty(36): bridge routing, output streaming,session lifecycle, cwd confinement, session cap, the cerebras spec builder, the
bin resolver, and every route gate/error, all against an injected fake PTY;
cockpit (5): spawn→attach at tier, cwd passthrough, error+retry,
unmount-kills-session, close→stop, via the real component path (mock only at the
client boundary + xterm pane). Full
plugin-task-coordinatorsuite green.exercised end to end: spawn a real process, stream its output through the
bridge, round-trip a keystroke, exit code
0, clean session teardown.plugin-pty,agent,ui,plugin-task-coordinator(xterm stays lazy code-split)./helpon cerebras, on a phone" devicecapture requires a built
eliza-codebundle + a live Eliza Cloud key on thedevice; captured as the device handoff rather than in CI.
Notes for reviewers
services/pty-contract.tsintentionally re-declaresConsoleBridge/PTYServiceinstead of importing from@elizaos/agent(which depends on theplugin set — importing it would create a cycle); the runtime binds them
structurally at the
getServicecast. A comment flags the sync requirement.PTY_SERVICElater —deferred as the explicitly-gated experimental tier.