feat: integrated terminal — runtime-owned shells for people and agents - #490
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo many files! This PR contains 954 files, which is 654 over the limit of 300. To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (178)
📒 Files selected for processing (954)
You can disable this status message by setting the |
|
React Doctor found no issues, but some checks were incomplete. Incomplete Checks
|
0887eba to
dbcf2b9
Compare
083f40b to
e670e6b
Compare
e670e6b to
0f84c0e
Compare
Close is idempotent (exited terminals answer their recorded exit), the stream client settles as closed instead of reconnecting a dead terminal, size votes made before the socket opened are delivered on open, and the fish shim rides vendor_conf.d without hiding the user's config. The web terminal is window-native: one terminal per OS window, an id-less route that adopts or creates, head-level Journal/New/Close verbs, and window close never signals the PTY. Co-Authored-By: Claude Fable 5 <[email protected]>
The zsh marker hooks shadowed zsh's read-only status parameter, killing the precmd hook on every prompt and dropping every finish marker; a live zsh test now pins the clean hook. The id-less terminal route waits for a catalog fetch from this mount before adopting or creating, so a terminal opened moments before the window cannot be missed and duplicated. Browser journeys walk the window-native flow end to end, and the QA tracker records the walked verdicts. Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Pull ADR-012 door 3 (`_meta.terminal_output`): the daemon no longer opts into agent-reported terminal output, and the whole reported pipeline goes with it — ACP ingestion and suppression, the `agent_reported` origin, contract/stream payload fields, transcript codec/replay, acpmock steps, and the web "reported by agent" xterm blocks. Agent-internal commands render as plain command output again; the deliberate S3 supervised block and the ACP pipe door stay untouched. Co-Authored-By: Claude Fable 5 <[email protected]>
A daemon bridge observes terminal opened events and, for agent-actor interactive PTY terminals (terminal_open, terminal_exec visible:true), opens a managed Terminal window in the workspace window model — on the desktop showing the bound session, never moving focus, one-shot per terminal, deduped by instance key, and never failing the tool call. Pipe terminals and human actors materialize nothing. The transcript now recognizes real agent traffic: hosted-MCP tool names (mcp__<server>__compozy__*) normalize to their native ids for the S3 supervised block and row labels, MCP string envelopes decode so terminal_id reaches the block, and the remaining terminal tools carry registered labels. E2E-003 asserts the window materializes without stealing focus. Co-Authored-By: Claude Fable 5 <[email protected]>
…rfaces Terminal tool descriptors now lead with the desktop-window capability, the official skill documents the windows contract and the write-wait-read loop, and the site docs replace the agent-reported block section with the plain-output and window-materialization truth. QA tracker updated for ADR-019: the reported-terminal scenario is deleted, affected scenarios were reset and re-walked with a real Haiku agent in an isolated lab, and a new materialization scenario records the walk (docs/qa/reports/2026-09-01-terminal-rework.md). Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
… rebase Main's loop work took 00098/00099, so the workspace-deletion-intents migration moves to the next gap-free version (00100), atlas.sum is rehashed for the unpublished tail, and the SDK contract chunks regenerate to the merged truth. Co-Authored-By: Claude Fable 5 <[email protected]>
Main's new loop tests construct ActionExecutionInput with a value actor while this branch moved the field to *task.ActorContext; the merged call sites now take the pointer. The global ahead-schema fixtures pinned version 100, which the renumbered terminal migration just occupied — they use the 999 sentinel the other streams already use. Co-Authored-By: Claude Fable 5 <[email protected]>
b61b065 to
fc34ec8
Compare
|
Too many files changed for review (1132 files, 100 file limit). |
Summary
CompozyOS gains real, fully functional terminals that live in the runtime. A terminal is owned by the daemon — not by the window that happens to display it — so it survives page reloads, app restarts, and dropped connections, and it renders identically in the web app and the desktop app. The same terminals are first-class for agents: an agent can deliberately run work in a terminal a human watches live, either side can hold control with explicit handoff, interactive prompts route to the human when they should, and everything that ran is answerable afterwards from a durable per-project command journal.
This lands as one PR because the pieces are load-bearing for each other. Nothing registers publicly until the journal, the control lease, the approval ladder, the CLI, the docs, and the official skill are all in place — a terminal without its audit trail is not a shippable terminal. Fifteen commits, 631 files, ~63k insertions across the Go runtime, CLI, HTTP/UDS, native tools, Web,
packages/ui, docs, SDK contracts, and the QA tracker.Motivation
Users leave CompozyOS every time work needs a shell: starting a dev server, running a migration, poking at a failing build, answering an installer's prompt. That context switch breaks the promise of an agent operating system.
It cuts both ways. When an agent's work would benefit from being watched — a long process, an interactive program, a demo — there is today no shared place where a person can see it happen, step in, and hand back. The agent runs blind and the human reads a summary after the fact.
This change closes both gaps with one substrate, and adds the part that makes shared control trustworthy rather than merely convenient: an audit trail that can answer "what ran here, who approved it, and how do we know" without keeping every byte forever.
What users can do now
Open a terminal in a project and keep it
Terminals are tabbed inside a terminal window, identified, and sized to the connected clients. They persist across reload and restart — the current screen comes back first, recent scrollback behind it.
Watch an agent work, and take control when it matters
Watching is never gated on the writer's permission (up to 16 viewers per terminal). Exactly one actor types at a time. When an agent holds control the cursor is hollow and the only affordance is "Take control".
Human takeover from an agent is immediate and unilateral. Displacing another person is not — it asks first and names who you are interrupting.
Approve what an agent runs
Agent execution is approval-by-default. The prompt shows the command, the working directory, the terminal, and the scope being granted.
A fixed set of irreversible commands never runs unprompted regardless of autonomy tier, and it is visually distinct from a routine approval. "Always allow" is not offered here.
Answer the prompts programs actually ask
When a program asks for a secret, the request is pinned to the live terminal with the prompt excerpt. The entry is hidden, and the value is never displayed, stored, journaled, recorded, or shown to the agent.
Answer "what ran here" afterwards
Every command executed in a CompozyOS terminal produces exactly one journal row: who ran it, the directory, the outcome, the approval that allowed it, and how the boundaries were detected. Rows whose boundaries are inferred are labeled
estimatedrather than presented as exact.Live output is otherwise ephemeral. Full-fidelity recording is opt-in per request and retention-limited, with replay from the journal.
See the truth when something degrades
If the journal cannot record, the terminal enters audit-blocked: new input and exec are refused with
journal_unavailable, while output draining and watching continue. The product says so instead of silently running unaudited work.A watcher that falls too far behind is disconnected and told so — the terminal is never slowed down to accommodate a slow client.
Work per profile
A terminal belongs to the profile that opened it. Switching profile re-scopes the list the way it re-scopes everything else — another profile's terminal is hidden, not lost, and is still running when you switch back.
Reach terminals where the rest of CompozyOS lives
Deliberate agent terminal work appears inside the session transcript as a live block that links back to the real terminal.
And terminals launch from the dock like every other CompozyOS app, with an attention badge when one needs a person.
Architecture and terminal lifecycle
A new
internal/terminalpackage (65 non-test Go files) owns interactive terminal sessions in the daemon:Manager/ registrypty/xptyplus the Compozy hardening rules (dup +O_NONBLOCKrescue,Setsid/Setctty,Control()-only ioctls); ConPTY adapter for Windows; pipe implementation viamvdan.cc/shvt/charmbracelet/x/vt— screen snapshots and ANSI-stripped reads for agentsjournal/workspacedbpersistence and querieswire/compozy.terminal.v1— opcodes, sequence numbers, ack/drop flow-control accounting, coalescerData flow (interactive). PTY read on the dup'd fd → OSC security filter → coalescer (8 KiB / 5 ms, sub-1 KiB bypass) → sequence-stamped ring append → fan-out to WebSocket subscribers, the VT actor, and the recorder when active. The filter returns two ordered outputs — display bytes and typed marker facts — so the journal is fed by parse-once typed facts rather than a second byte-parsing consumer downstream.
Every internal consumer is bounded and none can stall the hot path. The VT actor has a 256 KiB mailbox and on overflow goes dirty, rebuilding its grid from the ring at an escape-safe replay point (
Screen()returnsbusy:truemeanwhile). The recorder has a 1 MiB write buffer and on storage stall stops with an auditablerecording_stopped{reason, truncated:true}rather than degrading the terminal. EventBus observers are synchronous, O(1), non-blocking, panic-recovered per invocation, and frozen at wiring.Lifecycle and lease. Clients attach over a binary WebSocket, one socket per attached terminal, authorized by a single-use attach ticket even on localhost; the terminal catalog and lifecycle events ride SSE. The control lease is single-writer and generation-fenced against session runtime generations, so a stale or recovered agent run cannot act on a terminal it no longer owns. A disconnected controller's lease survives a pinned 30-second grace and then drops to available. When the agent run bound to a terminal ends, control auto-yields to the human — an abandoned agent terminal becomes the human's usable shell, never a locked orphan.
Closeis actor-aware: agents may close terminals bound to their own run or lease, are denied on human-controlled ones with the controller named, and humans always may.Boundaries.
internal/terminalimportsprocutil,toolruntime,workspace,store(types only),store/workspacedb,hooks,config, andredact— neverdaemon,api/*,cli,acp,session,tools,profile, orsandbox. Profile availability arrives through a consumer-definedProfileGuardinterface injected by the daemon.internal/daemonis the only composition point, andmagefiles/boundaries.gogains the enforcing rules in this same change.Security, approval, and redaction
A terminal is arbitrary command execution with the operator's authority, and the design treats it that way.
ESC ] 7113 ; v1 ; <nonce> ; …, whose nonce is cryptographically random per terminal and injected by the shell-integration shim into non-exported shell state, so child processes never inherit it.detected_by:"marker"is accepted only on a nonce match; bare OSC 133 and any wrong-nonce 7113 frame is an untrusted hint that degrades the row todetected_by:"idle". Marker frames are consumed by the filter before the ring, so they are never visible to clients.Persistence, journal, and fail-closed audit
The journal is durable, per-project, and lives in
workspacedb:internal/store/workspacedb/schema/definitions/01_terminal_journal.sqlinternal/store/workspacedb/queries/terminal_journal.sql00002_schema.sqland00003_schema.sqlExactly one row per executed command, carrying actor, approval, outcome, and detection confidence, with approximate boundaries labeled approximate and exit causes truthful — exited, signaled, or unknown, never fabricated.
Row assembly is pure and O(fact). Profile validity is checked at admission (
Open/Exec) through the injectedProfileGuard, never on the write path: once a command is admitted its row appends regardless of what happens to the owning profile afterwards, so archiving a profile can never strand rows, audit-block a live terminal, or fail daemon shutdown. Persistence is async through a bounded 64-row in-memory pending lane into the sameworkspacedbwriter — deliberately not a second durable queue. On write failure the row is retained and retried; on retry exhaustion the terminal enters audit-blocked and refuses new input and exec withjournal_unavailablewhile output draining, watching, and reads continue.Manager.Shutdownflushes the pending lane within a bounded wait, and if the store is dead the daemon exits with an explicit audit-gap error — the terminal having already been audit-blocked, so no command ever ran without its eventual row.Live output is otherwise ephemeral: bounded in-memory scrollback for reattach, gone on terminal end or runtime restart. Recording happens only by explicit request and expires per retention policy (default 30 days).
Agent-internal execution outside CompozyOS terminals never enters the terminal journal or the terminal list. Its record stays in the session view, rendered as a terminal-styled block labeled as agent-reported, with no control affordances and no journal rows.
Workspace and profile isolation
Every new datum here is workspace-scoped and profile-scoped, and both are fixed at creation and never change.
(workspace, profile). Listing, viewing, journal queries, and any live feed never cross either boundary.terminal_not_found, not a permission error that would confirm existence.profileID; aggregate reads take astore.ReadScopesoall_profilesis expressible where it is legitimate.Listis fail-closed: an invalid scope yields an empty slice, never an unfiltered one.terminal.closed{reason:"profile_archived"}; ticket invalidation and pending-request resolution ride that event as observers. Journal history is retained.Surfaces
Enumerated from the tree at this head, not from the spec.
CLI —
compozy terminal:open,list,get,attach,kill,exec,signal,input-requests,respond,journal,record start|stop,quote.attachdials the WebSocket over UDS and keeps input in raw mode, intercepting the detach chord locally so it never terminates the client.HTTP / UDS — 16 terminal paths, verified against
openapi/compozy.json:Native tools — the
compozy__terminaltoolset:compozy__terminal_open,_list,_read,_write,_exec,_wait,_signal,_claim,_yield,_close,_request_input. Descriptors, toolset wiring, capability strings (terminal.observe/terminal.exec), risk flags, and the native-tool catalog fixture are updated together.Web — new
web/src/systems/terminal/domain system (protocol client, wire schema, lease/grant/permission logic, gap buffer, resync, store, fixtures) plus theTerminalViewprimitive inpackages/uion@xterm/xterm, withterminal-tokens.cssand a Storybook story.TerminalViewhas zero protocol knowledge; the domain system owns the client.Desktop — inherits the web app unchanged under the thin-shell contract. Nothing terminal-shaped lives only in the desktop shell; the only desktop change in this diff is an Electron E2E spec.
Docs — new
packages/site/content/docs/terminal/section (index, tutorial, agents-and-safety, journal-and-recordings, platform-support, profile-segmentation), the fulldocs/cli/terminal/*command reference, and a configurationlifecycle-matrix.mdx.Config — a
[terminal]block with typedcompozy config setsupport and validation:default_shell,detached_ttl,exit_retention,max_per_daemon,max_per_workspace,max_subscribers,recording_retention_days,scrollback_bytes.Hooks / extensibility — 11
terminal.*events:opened,closed,command_started,command_finished,input_requested,input_provided,lease_changed,limit_rejected,recording_started,recording_stopped,subscriber_evicted. Regeneratedsdk/go/contracts(hook contracts, hook events, types 017/028–031) andsdk/typescript/src/generated/contracts.ts, plus extension SDK named types for the terminal domain.Platform honesty — macOS and Linux ship the complete experience. Windows and remote sandboxes execute commands from day one and state what they cannot do yet, before anything can hang.
Testing and QA evidence
Two tiers, kept separate on purpose.
Focused checks at this exact head (
7cf022483, post-rebase)These are the only results attributable to the commit this PR points at:
go test -race ./internal/clicodegen-check(within those focused runs)Two notes so these numbers are not read as more than they are. The 29-test protocol run is a rerun of a suite already contained in the 145 above — it is not additive; it is reported only to show that removing the two unused exports was safe. And React Doctor's three remaining findings are inherited unchanged from
origin/main; they are not attributable to this branch.Pre-rebase QA evidence
The following comes from the QA pass recorded in
docs/qa/reports/2026-08-26-integrated-terminal.md, run against bootstrap buildae5c2eb1cin an isolated lab. This is pre-rebase QA evidence for the feature's behavior — it is not verification of this head and is not a substitute for exact-head CI.make web-buildtask_05(reference / implementation / side-by-side / diff)"clean": true,survivors: []Five defects were found and fixed during that pass, each with production code changed rather than a test weakened: journal access rejecting the public workspace id; CLI attach losing the selected profile; the detach chord terminating the CLI client; terminal settings not settable through
config set; and profile-scoped agents failing to start sessions. Zero open product defects remain from the pass. Corresponding QA scenarios, charters, journeys, and content-addressed bug files are indocs/qa/in this diff.Compozy Impact Audit
compozy__terminal_*tool IDs in a newcompozy__terminaltoolset (internal/tools/builtin/terminal.go,descriptors.go,toolsets.go,builtin_ids_terminal.go), with I/O schemas, risk flags,terminal.observe/terminal.execcapability gates, approval-grant handling ininternal/tools/approval_grants.go, dispatch and dispatch-hook wiring, and thetestdata/native-tool-catalog.jsonfixture regenerated.terminal.*hook events with per-event payloads bridged from the terminal EventBus throughinternal/hooksvia non-blocking submit to the existing async pool; regeneratedsdk/go/contracts(hook contracts + events + types 017/028–031) andsdk/typescript/src/generated/contracts.ts; new extension SDK named types ininternal/extension/contract/sdk_named_types_terminal.go;[terminal]config lifecycle registered ininternal/config/lifecycleand documented in the site lifecycle matrix. Extension-controlled terminal surfaces are explicitly not promised in v1, and nothing here precludes them.profileID; aggregate reads takestore.ReadScopeand fail closed to an empty result on an invalid scope. Cross-profile lookup returnsterminal_not_found. Limits count per profile. HTTP/UDS routes are workspace-prefixed; SSE catalog and per-terminal streams carry the same fence;ArchiveProfiledrains that profile's terminals and emitsterminal.closed{reason:"profile_archived"}. A project-less global session is refused rather than silently scoped. Covered byET-terminal-profile-segmentation,ET-terminal-profile-selectors, and the runtime isolation suites.skills/compozy/SKILL.mdplus a newskills/compozy/references/terminal.mdcovering the terminal domain, its tool IDs, CLI paths, and hook events.Known blocked human-only verifications
Three scenarios could not be executed in the macOS lab. These are environment limits, not known failures — each needs a platform or delivery target the lab does not have:
They are recorded as
blocked-verifyindocs/qa/scenarios/, not asuntestedorfail.CI ownership
7cf022483.make gateattempt was BLOCKED when its internal golangci-lint subprocess exceeded the configured 10-minute timeout (recorded in the lab'sgate-verify.log, alongside passing codegen-check, gate-integration, and mage-test lanes). That attempt was also pre-rebase — so no full gate has ever run against7cf022483.7cf022483; a red or pending check keeps the workstream open and I repair, push, and re-wait.Reviewing this PR
Suggested order:
internal/terminal/— the domain authority. Start at theManagerinterface and the session actor, thenpty/,vt/,journal/,wire/.internal/api/coreterminal handlers (shared by HTTP and UDS),internal/cli/terminal*.go,internal/tools/builtin/terminal.go.web/src/systems/terminal/and theTerminalViewprimitive inpackages/ui.internal/store/workspacedb/for the journal schema and migrations, andmagefiles/boundaries.gofor the enforced import rules.Note that
.compozy/tasks/**is gitignored, so the spec, the 18 ADRs, the DX and UI/UX contracts, and the visual-contract evidence bundles are local-only and are not in this diff. The screenshots above are those evidence bundles, hosted so they are reviewable here.🤖 Generated with Claude Code
Closes #535