Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e8e6cab
fix(orchestration): submit Codex prompts after composer readiness
Oct 14, 2026
652b746
fix(limits): explain unavailable subscription limits for API profiles
Oct 15, 2026
2891dd0
fix(orchestration): recognize alternate empty Codex composers
Oct 15, 2026
004b1e2
fix(orchestration): handle follow-up composers and MCP startup states
Oct 15, 2026
0140f4a
fix(limits): reload Codex when provider configuration changes
Oct 15, 2026
efa9cd2
fix(orchestration): inspect the active logical composer
Oct 15, 2026
6f0e91b
feat(limits): show upstream Codex quotas from local OmniRoute
Oct 15, 2026
daea420
fix(limits): use resolved CLI paths for OmniRoute quota reads
Oct 15, 2026
2adec4a
Persist person-approved Codex folder trust outside agent isolation
Oct 15, 2026
1e4ee3f
Correct quota reset timestamps and recheck Codex before follow-ups
Oct 15, 2026
443e0a5
Handle serialized and canonical Codex trust profiles
Oct 15, 2026
3d09039
Retain upstream Codex quota during transient failures
Oct 15, 2026
f3f296b
Preserve typed Codex drafts and cached quotas on malformed replies
Oct 15, 2026
745f826
Gate plugin tasks and history resumes on Codex readiness and trust
Oct 15, 2026
12525f8
Expose Codex orchestration tools before ranked discovery
Oct 16, 2026
cb3a4cb
Fix restored Codex trust, quota cache and wrapped handoffs
Oct 16, 2026
74e9c71
Preserve Codex folder trust for local plugin launches
Oct 16, 2026
b1596b2
Initialize missing Codex homes before reading folder trust
Oct 16, 2026
146ed8f
Fix orchestration MCP authentication across new CLI threads
Oct 16, 2026
e2a41e4
Preserve long worker answers before closing subagents
Oct 16, 2026
75a10bd
Require reading completed worker results before cleanup
Oct 16, 2026
db0949c
Report closed children truthfully on late orchestration waits
Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ The application interface currently supports English and Russian. This documenta

Launch a shell or agent in a project directory, move and resize its live terminal, zoom out to navigate semantically, and return to Home for sessions, limits, media, and launch shortcuts. CanvasTTY keeps PTY state in the trusted main process and exposes only typed, allow-listed capabilities to the renderer.

For Codex profiles routed through a local OmniRoute gateway, see [real upstream subscription limits](docs/codex-api-limits.md).

## Agents, orchestration and protection

Agents start in **Auto** by default: the CLI's own auto mode (Claude Code, Codex, Grok), CanvasTTY's per-run rules for OpenCode, or, for a CLI without one, its approval bypass only inside agent isolation. Manual, Accept edits, Plan and Bypass are offered only where the CLI has them; Bypass is acknowledged once per CLI and never handed to a subagent. Around every mode sit layers the person controls in Settings → Agents: base protection (hooks that refuse elevation, `curl | sh`, disk commands and writes outside the project before the tool call runs), delegation rules (a subagent never gets more than its orchestrator and stays in its project; 2 levels and 8 live subagents by default), and an OS isolation layer (macOS `sandbox-exec`, Linux bubblewrap; none on Windows yet, where subagents run in Manual). When an isolated session ends, CanvasTTY checks the repositories it touched for git settings that would run programs outside isolation and offers to neutralize them.

For a new local Codex folder, CanvasTTY asks the person to trust it before launching. The host saves only that folder's trust through Codex's configuration API, checks for concurrent edits and keeps a private backup. Isolated agents still cannot modify their permission settings or read these backups. Plugin-managed profiles keep their own trust flow.

An **Orchestrator** session gets the `canvastty_agents` tools: `list_providers` (installed agents, sign-in state, models, efforts, profiles), `spawn_agent` with optional `model`, `effort` and `profile`, `wait_for_agent` (up to 600 s) and `get_agent_result`, which returns a Codex or OpenCode subagent's final reply as `answer` (up to 4,096 characters, masked). See [agent orchestration and isolation](docs/agent-orchestration.md) and [protection layers](docs/installing-and-security.md#agent-protection-layers).

## Windows shells and provider CLIs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ The orchestration MCP is a **second tool surface on the agent-bridge architectur
sessions. No tool ever names an unrelated session.
3. Bootstrap capability injection happens at PTY launch exactly as the browser bridge does
today (one-use, rotated to session-scoped, revoked at exit), but only for sessions whose
metadata role is `orchestrator`.
metadata role is `orchestrator`. A new CLI thread may start another MCP helper
with the same PTY environment: bootstrap authentication can join the same
launch while it has a healthy authenticated connection, even after the initial
bootstrap TTL. It cannot revive a disconnected, expired or revoked launch.
Helpers share a session-scoped reconnect capability so a new join does not
invalidate an earlier helper's reconnect path. PTY cleanup revokes them all.
4. Per-provider MCP config injection follows `ProviderLaunch.ts`'s existing adapters
(CLI args for Claude/Codex/Qwen, inline config for OpenCode, owned temp entries for
Kimi/Hermes), gated on the same role.
Expand Down
23 changes: 20 additions & 3 deletions docs/agent-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A session launched from the desktop with the **Orchestrator** role gets the `can
| `list_providers` | The agent providers CanvasTTY can launch as subagents: `id` (the exact `spawn_agent.provider` value), `name`, `installed`/`available` (from the provider CLI registry resolved at startup or on a recheck), `signIn` (`ok`, `signed_out`, `expired` or `unknown`, from the last usage-limits read; it never starts a read and never reads credentials), `subagent` and `orchestrator` support, `model` (whether the CLI takes one, its format, and for OpenCode the models a cached `opencode models` listing reported: run in the background with a 5 s timeout, refreshed after 10 minutes, never awaited) and `efforts`, and plugin launch options (`launchOptions`, plus `launchOptionTools` such as an accounts plugin's `list_routes`). Fast: cached state only, no network calls. |
| `spawn_agent` | Launches a subagent (profile: see below). `provider` must be a `list_providers` id (the schema lists them); an unknown one is refused with `INVALID_REQUEST` naming `list_providers`. Optional `model` goes to the CLI's own `--model` for that run (OpenCode `provider/model`; Codex, Claude, Qwen, Kimi alias, Grok, OMP, Pi, Cursor; Hermes, MiniMax, Devin and Antigravity take none) and optional `effort` to its reasoning effort (Codex `-c model_reasoning_effort`, Claude `--effort`, Grok `--reasoning-effort`). Both are checked per provider, kept on restart and restore, and never written to the CLI's configuration. If the person names a model, the orchestrator passes it as `model`. An OpenCode model its own `opencode models` list does not contain is refused before launch with up to five closest listed ids (OpenCode would otherwise stop with only "Unexpected server error"); when that list cannot be read, the model is passed as given. A subagent that exits anyway reports the last lines of its screen as `exitLines` in `wait_for_agent`, `observe_agent` and `get_agent_result`. |
| `wait_for_agent` | `{ sessionId, timeoutSeconds ≤ 600 }` (default 55). Returns when the subagent is `idle` (after a prompt: only once a turn that started after the latest delivered prompt has ended, so the CLI's startup idle does not count), `needs_approval`, `done`/`failed` (exited), `quiet` (reports no status and its screen stopped changing), `closed`, or on `timeout`, with `status`, `exitCode`, `waitedMs` and the masked terminal tail (masked before it is cut). Only this orchestrator's own subagents; it stops at once when the call is canceled or the orchestrator disconnects. |
| `get_agent_result`, `observe_agent` | `get_agent_result`: `answer`, the final reply of the subagent's last turn as the agent itself reported it (Codex through its Stop hook, OpenCode through CanvasTTY's plugin reading the session's last assistant message at `session.idle`; at most 4,096 characters with the end kept, masked, in memory only and cleared when the next turn starts), plus `status`, the exit state and the masked terminal tail. Other providers have no `answer`: their tail is raw screen output. `observe_agent`: the current status and tail. |
| `get_agent_result`, `observe_agent` | `get_agent_result`: `answer`, the final reply of the subagent's last turn as the agent itself reported it (Codex through its Stop hook, OpenCode through CanvasTTY's plugin reading the session's last assistant message at `session.idle`; up to 65,536 UTF-16 code units retained, masked, in memory only and cleared when the next turn starts; MCP returns 16,384-unit pages via `answer.nextOffset` and `get_agent_result(answerOffset=...)`), plus `status` and the exit state. When `answer` is present, `output` is empty; use `observe_agent` for the terminal tail. Other providers have no `answer`: their tail is raw screen output. `observe_agent`: the current status and tail. |
| `send_to_agent`, `cancel_agent`, `list_agents` | Follow-up prompts, disposing a subagent, listing this session's subagents. |

### Subagent profiles
Expand All @@ -32,7 +32,7 @@ A launch contributor that puts OpenCode on another model keeps `bash` asking, li

The control CLI's `create --profile auto|normal|acceptEdits|plan|yolo` is the equivalent for its workers (from the person's own terminal it defaults to YOLO, which needs the person's acknowledgement for that CLI).

The workflow is `list_providers` → `spawn_agent` (one per part) → `wait_for_agent` → `get_agent_result`. Agents should not explore the filesystem for agent CLIs or their configuration; the MCP server's instructions, the tool descriptions and the refusal messages say so.
The workflow is `list_providers` → `spawn_agent` (one per part) → `wait_for_agent` → `get_agent_result`. Call `get_agent_result` after waiting, even when `wait_for_agent` returned a short answer, then read every `answer.nextOffset` page in order before `cancel_agent`. Closing a child with an unread captured result is refused. After successful cleanup, use the results already received and do not wait on that child again. A late wait for a recently closed child returns `reason: "closed"` and `resultReadBeforeClose`, not a subtree error. These bounded, memory-only receipts retain no answer text or terminal output; they are visible only to the orchestrator that closed the child. Unknown and foreign IDs remain refused. A new `send_to_agent` invalidates the previous result receipt; an actively working child can still be canceled directly. A wait timeout only ends that wait: inspect reported errors and stop on failure instead of repeatedly waiting on a failed request. `sourceTruncated: true` means the capture itself exceeded its limit: have the worker save the full deliverable in a project file and read it before closing. Only captured final replies may use a 512 KiB runtime frame; decision and ordinary status frames retain their 64 KiB limit, and MCP frames remain capped at 128 KiB. Agents should not explore the filesystem for agent CLIs or their configuration; the MCP server's instructions, the tool descriptions and the refusal messages say so.

The control CLI below is a separate surface for automation; `providers` is its equivalent of `list_providers`.

Expand Down Expand Up @@ -114,17 +114,22 @@ The default private controller file is next to the connection descriptor. Use a
- `status` returns the native session state plus the controller-submitted turn, if any.
- Before its first prompt a ready provider can still report `unavailable`; inspect `screen`. Startup/authentication/trust menus require resolution before tasks. Task delivery never doubles as menu confirmation.
- `send` accepts at most 16,000 characters of task text. It rejects slash commands, terminal control sequences and input while a submitted turn is active. It does not offer automatic permission approval.
- Automated Codex prompts wait for its empty task composer in the current launch, then use explicit bracketed paste followed by Enter. Startup menus are not task input; cancellation, timeout or restart drops the pending text. A screen from a previous launch cannot establish readiness, and paste-only sends do not submit the draft.
- An accepted send means text was written to the PTY. It is not a completed task.
- A result becomes fresh only after a corresponding working-to-idle lifecycle transition. Provider turn IDs, when present, reject stale completion signals.
- `resultRevision` counts completed observations. A previous completed answer is kept separate from the active turn. `result --after N` returns `fresh: false` until a newer result exists.
- Only controlled Codex sessions opt into the Stop hook's final-message capture. Text is bounded to 4,096 UTF-16 code units, with a `truncated` flag. Ordinary sessions continue reporting lifecycle without capturing their answers.
- Only controlled Codex sessions opt into the Stop hook's final-message capture. Text is bounded to 65,536 UTF-16 code units, with a `truncated` flag. Ordinary sessions continue reporting lifecycle without capturing their answers.
- `no_result` means a turn became idle without a final-message payload. Inspect files and screen; do not report success from that state alone.
- `interrupt` returns `stopped: false` because Ctrl-C delivery is only a request. Confirm the resulting lifecycle before proceeding.

Human changes to an owned window still matter. Do not submit work while someone is editing its composer. The first version recognizes the Codex empty composer, not arbitrary provider TUIs.

For a visible numbered menu, `screen` returns `interaction.options`, the selected option and a screen revision. After inspecting the choices and confirming the action is within the task's authorization, use `choose SESSION_ID --choice N --revision HASH`. To close an idle menu that explicitly offers Escape, use `dismiss SESSION_ID --revision HASH`. Both commands reject a changed screen, act only on the owned PTY and return delivery receipts; inspect the next screen to confirm the outcome. They do not automatically approve anything.

Launch options do not imply a separate Codex profile: after preparation, CanvasTTY compares the resolved configuration file with its local profile, including symlink aliases. A local person-started or restarted card keeps the host folder-trust check; a plugin-owned profile or environment does not grant trust to the unrelated local profile. The per-launch interactive guard is never persisted or handed to agents or controllers.

Automatic restoration checks the saved trust for builtin local Codex cards before launch. If trust is absent or cannot be read, the card and its descendants remain stopped; use Restart to review the folder through the host. Restoration never grants new trust, and descendants wait for a contributed parent's trust check before starting. Concurrent person-approved trust changes are serialized per canonical Codex profile and retain Codex's version guard against external edits.

Codex may show **Hooks need review** on first launch or after the bundled lifecycle helpers change. Review the listed commands before trusting them through the observed menu. This is a separate startup step from selecting YOLO and is never dismissed by submitting a task. Controlled launches disable Codex's terminal animations for stable composer detection; ordinary sessions keep their configuration.

## Retry and ownership
Expand Down Expand Up @@ -153,3 +158,15 @@ node scripts/smoke-agent-control.mjs --live --cwd /already/trusted/test-director
The opt-in live smoke uses the actual CLI client, native PTY, installed Codex and authenticated lifecycle/result hooks. It asks Codex to create and edit one uniquely named proof file, independently verifies its bytes, and retains evidence. It never launches or controls the user's desktop window. It uses the existing Codex authentication and consumes provider usage. Platform claims require a run on that platform; Unix unit tests do not prove Windows behavior.

The smoke prints its session ID and private connection/client paths at startup. If a startup menu blocks it, inspect that session with the CLI using those paths and resolve only reviewed, authorized choices. The smoke waits up to two minutes for a ready composer; it does not silently trust hooks or approve permissions.


Codex receives the enabled `canvastty_agents` tools directly through its supported
per-server `omit_tools_from = ["deferred"]` setting. Coordination does not depend
on ranked `tool_search` results. The launch allowlist, role checks, profile ceilings
and project boundary are unchanged; browser tools retain their existing exposure.

Verify the Codex exposure contract without credentials or real providers with
`npm run smoke:codex-mcp-discovery -- /absolute/path/to/codex`
(pass the native `codex.exe` on Windows). This uses temporary profiles, a synthetic
model and loopback MCP/Responses servers. It checks both the deferred negative
control and direct exposure of the exact enabled coordination catalog.
17 changes: 17 additions & 0 deletions docs/codex-api-limits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Codex limits behind an API gateway

`codex app-server` reports ChatGPT subscription limits only for its authenticated native account. A custom API profile cannot identify the upstream account from its model name.

For a local OmniRoute gateway, CanvasTTY can read the real Codex connection quota through the installed OmniRoute CLI. Set these variables in CanvasTTY's launch environment:

- `CANVASTTY_OMNIROUTE_BIN`: absolute path to the installed `omniroute` executable (or Windows `.cmd`).
- `CANVASTTY_OMNIROUTE_URL`: the local gateway origin, such as `http://127.0.0.1:20128`.
- `CANVASTTY_OMNIROUTE_CODEX_CONNECTION`: the connection ID used by the inference route.

This is explicit opt-in. The connection must be active and belong to `codex`; update the binding if routing moves to another account. CanvasTTY uses the CLI's existing local management authentication and never reads or copies upstream OAuth credentials. Remote gateways and URL-borne credentials are rejected.

Only API-backed Codex profiles use this fallback. Native profiles retain their normal app-server source. The reader uses the connection-specific live usage endpoint, not the generic synthetic quota/queue signal. It shows only windows actually reported by the provider, including their real duration and reset time. A source key named `session` does not imply a five-hour window.

Gateway observations carry an unknown local account scope and are not attributed to a different local ChatGPT login. Missing configuration keeps the API-profile explanation; unavailable or malformed upstream data never becomes a fabricated quota bar.

Cached quotas are discarded when the local Codex account or provider configuration changes. A login change during a gateway read also discards that response, including transient-error fallback, so a previous connection's quota cannot appear under the new account.
Loading
Loading