Skip to content

zendev-lab/cue

Repository files navigation

cue-shell

Durable process substrate with named sessions shared by humans and agents.

⚠️ Pre-1.0 — core JOB / CRON flows, .cue scripts, real :fg PTY attach, client target resolution, and the official command set are implemented. Public contracts may still change before 1.0. Agent runtime concerns live above cue-shell.

Overview

cue-shell (cue) is a terminal-native runtime for durable async processes. It is not a traditional shell — it's a structured environment where sessions, jobs, scopes, chains, and crons are first-class primitives.

Key Features

  • Three-layer architecture: Process substrate (cued daemon) → Core model → Frontends (TUI/MCP/API)
  • Named shared sessions: humans and agents attach to one persistent job/scope context instead of maintaining separate terminal state
  • Primary interaction modes: JOB ⚡ · CRON ⏰ — switch with Shift+Tab
  • : prefix commands: Vim-style builtin access (:run, :kill, :jobs, :cron, ...)
  • .cue file scripts: cue run <file.cue> submits one R<n> script with fail-fast execution
  • Shared foreground PTY attach: :fg J<n> claims the single controller lease; :watch J<n> observes the same terminal read-only
  • Display tabs with clean semantics: :out J<n> snapshots stdout, :tail J<n> follows live stdout, :err J<n> opens stderr
  • Scope persistence: Environment snapshots with delta storage and lifecycle management
  • Chain syntax: -> serial · ~> ignore-failure · ||| parallel · |?| any-success; && / || stay inside one job
  • Daemon durability: named sessions, job history, cron definitions, and safe scope cursors survive client disconnects and daemon restarts

Architecture

┌─────────────────────────────────────────┐
│  L3 Frontend: TUI / MCP / REST API      │
├─────────────────────────────────────────┤
│  L2 Core model (cue-core)                │
│  Session · Job · Scope · Chain · Cron   │
├─────────────────────────────────────────┤
│  L1 Process substrate (cued daemon)     │
│  Unix socket · SQLite · Process mgmt    │
└─────────────────────────────────────────┘

Workspace Structure

crates/
├── cue-core/   — Core types and logic: Session, Job, Scope, Chain, Cron
├── cue-client/ — Client connection stack and `cue-client` CLI for session/target/run commands
├── cue-daemon/ — Background daemon library plus `cue-daemon` / `cued` CLIs
├── cue-tui/    — Interactive TUI frontend plus `cue-tui` CLI
├── cue-cli/    — `cue` aggregator entrypoint for explicit namespaces and extensions

Installation

# Install the official command set from PyPI:
# `cue`, `cue-client`, `cue-tui`, `cue-daemon`, and `cued`.
uv tool install cue-shell

Development

# Prerequisites: Rust 1.95+, just

# Build
cargo build

# Start daemon in foreground
cued -f

# Show the top-level aggregator help
cargo run -p cue-cli --bin cue --

# Start TUI (auto-connect / auto-reconnect)
cargo run -p cue-tui --bin cue-tui
# or through the aggregator
cargo run -p cue-cli --bin cue -- tui

# Create, inspect, and share a durable named session
cargo run -p cue-cli --bin cue -- session create dev
cargo run -p cue-cli --bin cue -- session list
cargo run -p cue-cli --bin cue -- tui --session dev

# Restart the daemon directly
cargo run -p cue-daemon --bin cue-daemon -- restart
# `cued` remains as a daemon alias
cargo run -p cue-daemon --bin cued -- restart

# Restart from inside the TUI
:restart

# Run checks
just check

# Run tests
just test

# Full CI locally
just ci

# Install pre-commit hooks
just pre-commit-install

Design Documents

See docs/design/README.md for the design index:

  • Design overview — Three-layer architecture, crates, primitives, IPC summary
  • conceptual-model.md — Jobs/scopes indexing, sequential composition, atomic tool surface
  • commands-and-modes.md — Command reference, mode system, :cron syntax
  • cue-script.md.cue file script contract for cue run <file.cue>

Client + daemon config

cue-shell uses split config files in the platform config dir:

  • client.toml — client-side transport/profile selection used by cue-client, cue-tui, and cue client ...
  • daemon.toml — daemon-side runtime defaults used by cue-daemon / cued

Named sessions

Named sessions are daemon-owned process workspaces. Creating one captures the current cwd/environment scope; another human or agent client can attach by name and sees the same owned jobs, crons, output events, and subsequent scope cursor. Closing a TUI or client does not delete the session or stop its jobs.

cue session create dev
cue session list
cue tui --session dev
CUE_SESSION=dev cue run examples/hello.cue

Finished sessions can be hidden from the everyday list without deleting their state:

cue session archive old-dev
cue session list --archived
cue session list --all --json
cue session restore old-dev

Archive is deliberately reversible: the session identity, scope cursor, job history, and retained terminal history remain intact. cue session list shows active sessions by default, and archived sessions cannot be attached until restored. The daemon refuses to archive a session that still has connected clients, non-terminal jobs, pending script/chain work, or an owned cron; detach clients and explicitly finish/cancel/remove those blockers first. There is no force-archive or deletion path. Archive-aware clients require the daemon's session-archive IPC capability and fail locally with an upgrade/restart hint before sending an unsupported request to an older daemon.

Scopes containing credential-like environment values are deliberately not written to SQLite. After a daemon restart such a session reports needs_refresh; recover it explicitly from a trusted process environment with cue tui --session dev --session-refresh or CUE_SESSION=dev cue run examples/hello.cue --session-refresh. Ordinary reconnects never replace an already-ready shared scope.

Foreground PTY jobs can be shared without sharing write access:

:fg J1       # attach and claim the controller lease
:watch J1    # attach as a read-only observer
cue fg watch J1 --session dev  # persistent non-interactive observer

Every attachment receives the current terminal snapshot and subsequent live output. Exactly one attached client may send keys, paste, resize, or use :send against that PTY. In the fullscreen terminal view, Ctrl+] releases control (or claims it when free), Ctrl+Z detaches, and Ctrl+Y copies the visible terminal. Releasing control keeps the client attached as an observer; disconnecting releases its controller lease without stopping the job.

cue fg watch keeps its daemon connection open until the PTY job exits. Its default stdout is the exact initial terminal snapshot followed by matching live PTY bytes, so it can be redirected without text conversion; lifecycle notices go to stderr. Add --jsonl for snapshot, output, control_changed, and exited records (binary fields are base64), or --session-refresh when a selected named session explicitly needs recovery after a daemon restart.

.cue file script mode

Scripts are .cue files executed through the client CLI, with a top-level shortcut retained by the aggregator:

cue-client run examples/hello.cue
cue client run examples/hello.cue
cue run examples/hello.cue

A script run gets an id such as R12. Top-level items run in file order with fail-fast semantics: if one item exits non-zero, later items are not submitted and cue run exits with that code. Bare non-: items default to :run, while explicit : commands remain available for builtins and :run(...) mode params. Output still belongs canonically to jobs, while the daemon delivers script job output and terminal script status directly to the cue run process so stdout/stderr and the final exit code do not depend on event-channel subscriptions.

Interactive JOB multiline input is not a script entry point; put multi-item workflows in a .cue file. See docs/design/cue-script.md and examples/.

Client transport and extension config

cue-client and cue-tui default to a local Unix socket profile, so local users do not need any config for the current flow. The top-level cue command is an explicit aggregator: bare cue prints help, cue session ... forwards to cue-client session ..., cue client ... forwards to cue-client ..., cue tui ... forwards to cue-tui, and cue daemon ... forwards to cue-daemon. Target/profile commands are intentionally namespaced under the client surface; use cue-client target ... or cue client target ... rather than cue target ....

To make the split explicit:

[transport]
default_profile = "local"

[transport.profiles.local]
transport = "unix"
# socket = "/custom/path/to/cued.sock"

[transport.profiles.remote-dev]
transport = "ssh"
destination = "user@example.com"
gateway_command = "cued gateway --stdio"
start_command = "cued start"

The local profile name is reserved for Unix socket transport; use another profile name for SSH targets.

Phase 1 uses the system OpenSSH client and runs the configured gateway command over SSH, so the client speaks the same IPC through cued gateway --stdio. Remote daemon startup still stays explicit: cue will not run start_command for you. cue-client owns client-side transport parsing and resolution, so CLI/TUI frontends share the same client.toml behavior.

When auto_detect_ssh is enabled, cue-shell adds implicit SSH profiles from ~/.ssh/config. Additional cluster inventory can be configured generically without hardcoding scheduler-specific names in cue-shell:

[transport.discovery]
# Values are host lists separated by comma, semicolon, or whitespace.
env_hosts = ["CLUSTER_HOSTS"]
# Values are endpoint lists; cue-shell extracts hosts from host:port or URI values.
env_endpoints = ["CLUSTER_ENDPOINTS"]
# Values point to files containing one host per line or host plus extra columns.
env_hostfiles = ["CLUSTER_HOSTFILE"]
# Values use bracket range syntax such as gpu-[01-03,08].
env_bracket_ranges = ["CLUSTER_NODELIST"]

Site-specific schedulers should be wired through this config or through external cue-* extensions rather than hardcoded in the cue-shell core. Other frontends, including Pi cue integrations, should use the cue-client resolver if they need to honor client.toml:

cue-client target resolve --json
cue client target resolve --json
cue-client target list --json

Direct daemon/socket integrations only see the server side and will not apply client profile selection by themselves.

cue can also dispatch external CLI extensions from client.toml after checking aggregator namespaces and direct shortcuts:

[extensions.commands.foo]
program = "cue-foo"
description = "Foo extension"

program is the executable path/name; extension arguments come from the cue foo ... invocation. Then cue foo arg runs cue-foo arg. Aggregator namespaces and shortcuts such as client, tui, daemon, run, target, help, and version take precedence, and extension names must be kebab-case without colliding with built-in or first-party subcommands. Optional PATH lookup for unknown cue-<name> binaries can be enabled explicitly:

[extensions]
path_lookup = true

Daemon runtime config

On first startup, cued creates ~/.config/cue-shell/daemon.toml with the built-in guardrails. daemon.toml can block unsafe commands or command arguments and attach remediation hints. Block rules run before advisory warnings, so a command can both warn generally and fail fast for specific arguments.

[block.versioned_commands]
python = "Use script_run/script_eval or uv run python ...; direct Python launchers are blocked."

[block.commands]
sh = "Avoid shell wrappers. Use cue-shell direct-exec, cwd=..., or cue operators."

[block.commands.git]
"--no-verify" = "Run the commit normally; if hooks fail, inspect and fix the hook/check."

[block.commands.npm]
"--force" = "Use the lockfile and normal install path."

[warn.commands]
cd = "Prefer cwd=... over cd in command strings."

Matching is literal, not glob or regex based:

  • [block.commands] sh = "..." blocks a command whose argv[0] basename is exactly sh. It matches sh and /bin/sh; it does not match zsh, /bin/zsh, or shellcheck.
  • [block.versioned_commands] python = "..." blocks command basenames python, python2, python3, python3.12, etc.; it does not match python-config.
  • The generated default config uses [block.versioned_commands] python = "..." to block direct python, python3, and versioned launchers such as python3.12, so Python execution goes through script_run / script_eval or explicit uv run python ....
  • Each [block.commands.<name>] entry maps one blocked argument pattern to its remediation hint. The command name is also matched by exact argv[0] basename.
  • Argument patterns are checked against each argv token independently, not against the joined command line. "--no-verify" matches an argument token --no-verify; it also matches --no-verify=... via the --flag=value convention.
  • [warn.commands] maps an exact command basename to an advisory hint.

daemon.toml can cap persisted job/script history:

[retention]
max_job_history = 200
max_script_runs = 100

It can also enable a runtime wrapper such as rtk. Wrapping is allowlist-only: commands not listed under [wrapper.allowlist] are never wrapped, and an empty allowlist wraps nothing.

[wrapper]
enabled = true
binary = "rtk"

[wrapper.allowlist]
commands = ["cargo", "git", "pnpm", "node"]

:wrap on/off/status overrides only the session-level enablement. Per-command mode params such as :run(wrapper=false) cargo test and cron params such as :cron(wrapper=true) every 5m cargo test override enablement for that invocation, but still must match the allowlist.

daemon.toml configures the overlay workspace sandbox used by :run(sandbox=overlay):

[sandbox]
# Root under which each sandboxed job gets its own <root>/<job-id>/{upper,work}.
# Defaults to shared memory so writes stay off disk; point it at a disk-backed
# path if /dev/shm is too small for your builds.
default_upper_root = "/dev/shm/cue-shell-sandbox"
# Refuse to start a sandbox when the upper-root filesystem has less than this
# fraction free (0.0 disables the guard). Protects /dev/shm from runaway writes.
min_free_ratio = 0.1

Each sandboxed job gets an independent per-job upper/work pair, so concurrent jobs never share an overlay layer, and the writes are discarded when the job finishes. An explicit :run(sandbox=overlay, sandbox.upper=<dir>) treats <dir> as an upper root (the daemon still appends /<job-id>/{upper,work}), while sandbox.upper=tmpfs forces a fresh in-memory upper for that job. Overlay sandboxing is a workspace view, not a security boundary — it does not isolate absolute paths outside the working tree, network access, process credentials, or environment variables. See docs/design/sandbox-threat-model.md for the full trust model.

Typical remote flow:

# Step 1: start the remote daemon explicitly
ssh user@example.com "cued start"

# Step 2: connect with the SSH transport profile
cue

If the remote daemon is not running (or its socket is missing), cue starts the TUI offline and keeps retrying the configured gateway. cue run <file> still fails immediately because file-script execution needs a live daemon.

Project Status

Component Status
Design docs ✅ Active
Cargo workspace ✅ Multi-crate workspace
CI/CD ✅ Tests, package smokes, PyPI/GitHub release path
cue-core ✅ Core types / IPC / parser in place
cue-client ✅ Named sessions / transport profiles / target JSON / script runner
cue-daemon ✅ Durable sessions / jobs / crons / scopes / PTY attach
cue-tui ✅ Session attach / interactive job+cron frontend / reconnect view
cue-cli ✅ Aggregator / extension dispatch / package feature gates for first-party commands

License

Licensed under the MIT License.

About

Durable process substrate with a TUI for managing long-lived jobs, scopes, and schedules.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages