Skip to content

feat(terminal): super_user PTY-over-WebSocket prototype (Studio terminal)#1755

Draft
dawsontoth wants to merge 3 commits into
mainfrom
claude/instance-terminal-prototype
Draft

feat(terminal): super_user PTY-over-WebSocket prototype (Studio terminal)#1755
dawsontoth wants to merge 3 commits into
mainfrom
claude/instance-terminal-prototype

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Prototype / draft — for team discussion. Not for merge as-is.

Adds a super_user-only interactive terminal (PTY over WebSocket) exposed from a Harper instance, so Studio can render an xterm.js shell running inside the container. This is the stepping-stone channel from the "Claude in the container" exploration; the authenticated WebSocket it establishes is the substrate the planned live-log-stream and Node-inspector-bridge features would reuse.

Companion Studio PR: HarperFast/studio#1467

Design decisions (settled with the team)

  • Operations API port. Served from the operations Fastify server's underlying node server (registerTerminalWebSocket in buildServer, right next to the MCP operations profile), gated on terminal.enabled. Keeps the terminal on the same security boundary as the rest of the operations surface, not the application data port.
  • First-message auth. The socket is accepted unauthenticated; the client's first WS frame carries the credential (operation token, or basic), which is validated and super_user-checked before any PTY spawns. Nothing sensitive rides in the handshake headers/subprotocol. No change to security/auth.ts.

What's here

  • components/terminal/ — the component + README (wire protocol, close codes, security notes).
  • server/operationsServer.ts — wires it in, gated on terminal.enabled.
  • config-root.schema.json — opt-in terminal block (default off).
  • package.jsonnode-pty as an optionalDependency.

Enable

terminal:
  enabled: true          # required — off by default
  # shell / cwd / idleTimeoutMs / maxSessions are optional

Status / caveats

  • Off by default (opt-in twice: terminal block present and enabled: true).
  • Direct-connection only — the Fabric Connect proxy buffers streamed responses and can't carry a WebSocket (same limitation as the existing log/deploy SSE tails).
  • Deliberate prototype scoping: the shell inherits the operations-thread user/privileges; only a global session cap + notify-level open/close/deny audit logging, no cgroup/user-namespace confinement. See components/terminal/README.md.
  • Branch is based on main@d33f0970f; main has since advanced. Rebase before merge — expect touch-points in server/operationsServer.ts, config-root.schema.json, and package.json.

Verified locally end-to-end in the real Studio UI (see companion PR).

🤖 Generated with Claude Code

Prototype (for team review) of an interactive terminal exposed from a Harper
instance, so Studio can render an xterm.js shell running inside the container.
The authenticated WebSocket channel it establishes is the shared substrate the
planned live-log-stream and inspector-bridge features reuse.

Design (per review): served on the OPERATIONS API port (same security boundary
as the rest of the operations surface, not the app data port), with
FIRST-MESSAGE auth (credential in the first WS frame, not the handshake).

- components/terminal: new built-in. registerTerminalWebSocket() attaches a
  `/terminal` upgrade handler to the operations Fastify server's node server
  (dedicated ws.WebSocketServer, subprotocol `harper-terminal`). Accepts the
  socket unauthenticated, then validates the first frame (operation token via
  validateOperationToken, or basic via server.authenticateUser), asserts
  super_user, and spawns a PTY (node-pty, optional native dep, dynamically
  imported). Opcode-framed wire protocol, 5s auth timeout, idle timeout,
  per-thread session cap, notify-level open/close/deny audit logging.
- server/operationsServer.ts: call registerTerminalWebSocket in buildServer
  (next to the MCP profile), gated on terminal.enabled.
- config-root.schema.json: opt-in `terminal` block (enabled: false default).
- package.json: node-pty as an optionalDependency.

No change to security/auth.ts — first-message auth is handled entirely in the
component. Off by default; direct-connection only (Fabric Connect proxy buffers
streams). See components/terminal/README.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednode-pty@​1.1.09310010096100

View full report

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a prototype for a built-in interactive terminal component, exposing a super_user-only PTY over a WebSocket on the operations API port. The feedback highlights critical security and stability improvements, including the addition of an authenticating flag to prevent concurrent authentication requests from leaking PTY processes, passing the missing request object to authenticateUser to avoid runtime errors, and configuring a maxPayload limit on the WebSocket server to mitigate potential Denial of Service (DoS) vulnerabilities.

Comment thread components/terminal/index.ts
Comment thread components/terminal/index.ts Outdated
Comment thread components/terminal/index.ts Outdated
Comment thread components/terminal/index.ts
Comment thread components/terminal/index.ts
Comment thread components/terminal/index.ts
Comment thread components/terminal/index.ts Outdated
Comment thread package.json
@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

Prior blocker (closed socket during auth awaits — leaked liveSessions, orphaned PTY) is resolved by commit 44fb1a9: if (closed) return after authenticate() and if (closed) { liveSessions--; return; } after loadPty() are both in place.

- Prevent auth re-entrancy / double-PTY spawn: synchronous `authenticating`
  flag set before the first await, plus reserve the session slot synchronously
  (check + liveSessions++ with no await between, rolled back on PTY-start
  failure) so concurrent connections can't overshoot maxSessions.
- Pass `req` through to `server.authenticateUser` (3-arg signature).
- Add `maxPayload` (1 MiB) and a `wss.on('error')` handler (unhandled 'error'
  on the ws.Server would crash the process).
- Guard authenticated frames after PTY exit: bail if closed/no term, and wrap
  `term.write` in try/catch → teardown (avoids unhandled rejection during the
  WS close handshake).
- Sync package-lock.json for node-pty (fixes `npm ci` in CI).
- Add node-pty to dependencies.md; correct config schema wording to
  "operations API port".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread components/terminal/index.ts
Add `if (closed) return` after `await authenticate()` and after
`await loadPty()` (rolling back the reserved slot in the latter). Without
these, a socket that closes mid-await — e.g. a second rapid auth frame that
safeCloses the connection while the first authenticate() is still in flight —
would still reach liveSessions++/pty.spawn() on a dead socket, leaking the
session counter and orphaning the PTY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant