Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

10 KiB

@elizaos/plugin-pty

Interactive PTY terminal service for elizaOS. Registers PTY_SERVICE — the one piece the app's already-built web terminal needs to drive a real interactive CLI (most importantly the interactive eliza-code CLI running on Eliza Cloud/cerebras).

Purpose / role

The elizaOS app ships the full front half of a web terminal: the xterm UI (PtyTerminalPane), the typed client methods (spawnShellSession, subscribePtyOutput, sendPtyInput, resizePty), and the agent-server WebSocket handlers (pty-subscribe / pty-input / pty-output / pty-resize). Those handlers call getPtyConsoleBridge(state), which resolves runtime.getService("PTY_SERVICE")?.consoleBridge. Without a registered PTY_SERVICE, that bridge is null and the terminal is inert.

This plugin supplies that service. It is the missing keystone — everything else already exists, so this connects three finished pieces (xterm UI, WS keystroke path, interactive CLI) rather than building them.

It is opt-in: add @elizaos/plugin-pty to an agent's plugin list. It has no autoEnable, so it stays dormant fleet-wide unless a character explicitly loads it (intended for the developer-gated cockpit). It disables interactive spawning automatically on store builds.

Why eliza-code on cerebras (not the Claude/Codex CLIs)

Running a real interactive CLI on a subscription inherently means impersonating that vendor's CLI — the TOS-unsafe tier. eliza-code (packages/examples/code) is a real interactive slash-command TUI we own: it already implements /help, /clear, /task, etc., and selects its model provider purely from env. Pointing it at Eliza Cloud's OpenAI-compatible endpoint routes inference to cerebras (gemma-4-31b for both fast and smart) — a real CLI with all slash commands, on any device, with zero TOS exposure.

Experimental vendor-CLI tier (kind claude / codex, #10832 Phase 2)

The same PTY_SERVICE can spawn the real interactive Claude Code / Codex CLIs on the user's own subscription — the TOS-unsafe tier. It is gated by PTY_VENDOR_CLI_ENABLED (a separate gate from PTY_INTERACTIVE_ENABLED, default off, exact truthy allowlist only, never on store builds); when off, spawning kind: "claude" | "codex" is rejected with a 403 even if interactive spawning is on.

Spec builders live in lib/vendor-cli-spec.ts (buildClaudeCliSpec / buildCodexCliSpec + resolveClaudeCliBin / resolveCodexCliBin). Both CLIs launch PLAIN (no args) — the interactive TUI, not the claude --print / codex exec one-shot paths. Credentials reuse the existing subscription-plugin conventions: claude gets CLAUDE_CODE_OAUTH_TOKEN passed through when configured (plugin-anthropic-proxy's env credential path), else reads ~/.claude/.credentials.json itself via the inherited HOME; codex gets CODEX_HOME passed through when configured (the coding-account-bridge per-account convention), else reads ~/.codex/auth.json. The tokens are opaque passthroughs — never parsed, logged, or persisted here.

Plugin surface

  • Service PtyService (serviceType = "PTY_SERVICE", services/pty-service.ts) — exposes consoleBridge (the ConsoleBridge the agent server drives) plus startSession / stopSession / listSessions / hasSession.
  • Routes (routes/pty-routes.ts, authenticated + terminal-token gated for HTTP callers, rawPath):
    • POST /api/pty/sessions — spawn (kind: "eliza-code" default, or the gated "claude" / "codex"; returns { session }).
    • GET /api/pty/sessions — list live sessions.
    • DELETE /api/pty/sessions/:id — kill a session.

Runtime-aware PTY engine

node-pty's write path is broken under Bun (this._socket.write is not a function — output streams, keystrokes throw), and the agent runs under Bun in dev. So defaultSpawnResolver (services/pty-session-store.ts) picks the engine by runtime:

  • Bun → Bun's native truePty (Bun.spawn({ terminal })), the same engine the Electrobun host uses (services/bun-pty-spawn.ts). One gotcha handled here: the terminal exit callback reports the PTY-teardown status (always 1), so the real exit code is taken from proc.exited.
  • Node@lydell/node-pty (optional native dependency), which works end to end.

Both are adapted to one PtyHandle interface, so PtySessionStore is engine-agnostic and unit-testable with an injected fake PTY (test/fake-pty.ts).

Layout

index.ts                       Plugin def (services + routes + dispose) and public exports
services/
  pty-service.ts               PtyService — the PTY_SERVICE registration
  pty-session-store.ts         PtyConsoleBridge + PtySessionStore + defaultSpawnResolver
  bun-pty-spawn.ts             Bun native truePty adapter (isBunRuntime, bunTruePtySpawn)
  pty-contract.ts              ConsoleBridge/event contract (mirror of packages/agent)
  pty-types.ts                 PtyHandle / PtySpawn / PtySpawnSpec / PtySessionInfo
lib/
  eliza-code-spec.ts           buildElizaCodeCerebrasSpec + resolveElizaCodeBin (pure)
  vendor-cli-spec.ts           buildClaudeCliSpec / buildCodexCliSpec + bin resolvers (pure)
routes/
  pty-routes.ts                spawn / list / stop route handlers
test/
  fake-pty.ts                  Controllable in-memory PTY double + fake spawn
  eliza-code-spec.test.ts      Spec builder + bin resolver
  vendor-cli-spec.test.ts      Vendor spec builders + bin resolvers
  pty-session-store.test.ts    Bridge routing, streaming, lifecycle, confinement, cap
  pty-service.test.ts          Service wiring
  pty-routes.test.ts           Route handlers (gates, errors, spawn/list/stop)
  pty.real.test.ts             Gated real PTY coverage (excluded from the normal lane)

Commands

bun run --cwd plugins/plugin-pty build      # tsup ESM + declarations
bun run --cwd plugins/plugin-pty test       # vitest unit suite
bun run --cwd plugins/plugin-pty typecheck  # tsgo --noEmit
bun run --cwd plugins/plugin-pty lint

Config / env vars

Variable Default Purpose
PTY_INTERACTIVE_ENABLED true Explicit values enable only on true, 1, on, or yes; any other non-empty value disables spawning. Store builds also disable spawning.
PTY_ALLOWED_DIRECTORY process cwd Directory sessions are confined to.
ELIZA_TERMINAL_RUN_TOKEN Required step-up token for remote HTTP access to PTY spawn/list/stop routes. Trusted loopback cockpit traffic is allowed server-side without exposing the token to browser JavaScript.
PTY_ELIZA_CLOUD_API_KEY Dedicated Eliza Cloud key eliza-code authenticates with. Do not use the agent server's primary OPENAI_API_KEY for PTY sessions.
PTY_ELIZA_CLOUD_FAST_MODEL / PTY_ELIZA_CLOUD_SMART_MODEL gemma-4-31b Optional deployment pins for the fast/smart eliza-code model ids. Request body model values still take precedence.
PTY_ALLOWED_BASE_URLS Eliza Cloud API Comma-separated operator allowlist for non-default OpenAI-compatible base URLs.
PTY_IDLE_TIMEOUT_MS 900000 Idle live-session timeout. Set 0 to disable the fallback reaper.
ELIZA_CODE_BIN auto-resolved Absolute path to built eliza-code dist/index.js.
PTY_VENDOR_CLI_ENABLED false Experimental vendor-CLI tier (kind: "claude" | "codex" — the real vendor CLI on the user's own subscription). Enables only on true, 1, on, or yes; never on store builds.
PTY_CLAUDE_BIN / PTY_CODEX_BIN PATH lookup Absolute path to the claude / codex launcher.
CLAUDE_CODE_OAUTH_TOKEN Optional Claude Code OAuth token passed through to kind: "claude" sessions; without it the CLI reads ~/.claude/.credentials.json.
CODEX_HOME Optional codex auth dir passed through to kind: "codex" sessions; without it the CLI reads ~/.codex/auth.json.
ELIZA_BUILD_VARIANT store disables interactive spawning (including the vendor tier).

How the cerebras wiring works

buildElizaCodeCerebrasSpec sets the env eliza-code reads (packages/examples/code/src/lib/model-provider.ts): ELIZA_CODE_PROVIDER=openai, ELIZA_CODE_CODING_ONLY=1, OPENAI_API_KEY, OPENAI_BASE_URL (https://api.elizacloud.ai/v1), and OPENAI_{SMALL,MEDIUM,LARGE}_MODEL. The tier (fast/smart) controls which model small/medium lead with; large is always the smart model so heavy calls escalate. CODING_TOOLS_WORKSPACE_ROOTS and SHELL_ALLOWED_DIRECTORY confine eliza-code's own file/shell tools to the session cwd. --coding-only / ELIZA_CODE_CODING_ONLY=1 keeps the cockpit REPL from loading the orchestrator and recursively spawning sub-agents.

Conventions / gotchas

  • pty-contract.ts must stay structurally in sync with ConsoleBridge / PTYService in packages/agent/src/api/parse-action-block.ts. We redeclare it (not import from @elizaos/agent) to avoid a dependency cycle — the runtime binds them at the getService cast.
  • session_output / session_exit are the two bridge events. The agent server subscribes to session_output ({ sessionId, data }).
  • node-pty is an optionalDependency (native). Under Bun it isn't used at all; under Node it is required for spawning.
  • Never log the spawn request body — it can carry an API key.
  • PTY child processes do not inherit the full server process.env; only a small runtime allowlist plus explicit eliza-code spec env is passed through.
  • See the root AGENTS.md for repo-wide conventions.

NON-NEGOTIABLE — evidence & real end-to-end tests

The binding standard is AGENTS.md. The unit suite proves the store/bridge/routing/spec logic against an injected PTY; the gated pty.real.test.ts (and the manual real-runtime checks) prove the actual node-pty / Bun-truePty path spawns real processes, streams output, round-trips keystrokes, and reports exit codes. The full "real CLI on a phone" proof — interactive eliza-code answering /help against live cerebras on-device — requires a built eliza-code bundle + a real Eliza Cloud key and is captured as the device handoff.