diff --git a/CHANGELOG.md b/CHANGELOG.md index 368f48803..a09175286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add the official xAI Grok Build CLI as the `grok_cli` provider, including + isolated per-terminal MCP configuration, native hard tool restrictions, + multi-turn TUI support, orchestration e2e coverage, and provider docs. + ### Fixed - tmux listing parse failures are retried once and reported as a distinct condition instead of surfacing as a bare `ValueError` that reads like "session not found" one layer up. libtmux 0.53.1+ zips `parse_output`'s fields with `strict=True`, so any short row (a pane or session vanishing mid-listing, or trailing fields tmux omits) raised `ValueError: zip() argument 2 is shorter than argument 1` — which propagated through `server.sessions`/`window.panes`, blocked launches outright, and left the pipe-liveness watchdog unable to tell a genuinely-gone session from a transient parse failure. Adds `TmuxLookupError` and routes the listing reads in `clients/tmux.py` through a single retry-and-classify wrapper; a failed `create_session` no longer leaves an orphaned tmux session that blocks relaunching the same name. Also caps `libtmux<0.53.1`, the last release that zips non-strict (caom-anv) @@ -843,4 +849,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - accept optional U+03BB (λ) after % in kiro and q CLIs (#44) - diff --git a/README.md b/README.md index 95fb01b79..dcf76624b 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ Install: [Codex CLI](docs/codex-cli.md), [Antigravity CLI](docs/antigravity-cli.md), [Hermes](docs/hermes.md), [Kimi CLI](docs/kimi-cli.md), [GitHub Copilot CLI](docs/copilot-cli.md), - [OpenCode CLI](docs/opencode-cli.md), or - [Cursor CLI](docs/cursor-cli.md) + [OpenCode CLI](docs/opencode-cli.md), + [Cursor CLI](docs/cursor-cli.md), or + [Grok Build CLI](docs/grok-cli.md) The focused provider guides contain installation, authentication, and provider-specific behavior. @@ -130,8 +131,9 @@ provider override while keeping the same sequence. [Codex CLI](docs/codex-cli.md), [Antigravity CLI](docs/antigravity-cli.md), [Hermes](docs/hermes.md), [Kimi CLI](docs/kimi-cli.md), [GitHub Copilot CLI](docs/copilot-cli.md), - [OpenCode CLI](docs/opencode-cli.md), and - [Cursor CLI](docs/cursor-cli.md). + [OpenCode CLI](docs/opencode-cli.md), + [Cursor CLI](docs/cursor-cli.md), and + [Grok Build CLI](docs/grok-cli.md). - [Security policy](SECURITY.md): vulnerability reporting and deployment guidance. diff --git a/docs/agent-profile.md b/docs/agent-profile.md index bc830b52a..826c66434 100644 --- a/docs/agent-profile.md +++ b/docs/agent-profile.md @@ -65,14 +65,16 @@ portable and make profile listings useful. - `codexProfile` (string): named Codex configuration profile. - `codexConfig` (object): inline Codex configuration overrides. - `hermesProfile` (string): Hermes profile wrapper command. +- `grokNativeWorkflows` (boolean): explicit Grok Build-native worker/workflow + opt-in; defaults to `false`. Provider support for pass-through fields differs. Use the focused guides for [Kiro CLI](kiro-cli.md), [Claude Code](claude-code.md), [Codex CLI](codex-cli.md), [Antigravity CLI](antigravity-cli.md), [Hermes](hermes.md), [Kimi CLI](kimi-cli.md), -[GitHub Copilot CLI](copilot-cli.md), [OpenCode CLI](opencode-cli.md), and -[Cursor CLI](cursor-cli.md) instead of relying on a duplicated compatibility -catalog here. +[GitHub Copilot CLI](copilot-cli.md), [OpenCode CLI](opencode-cli.md), +[Cursor CLI](cursor-cli.md), and [Grok Build CLI](grok-cli.md) instead of +relying on a duplicated compatibility catalog here. ## Tool restrictions diff --git a/docs/grok-cli.md b/docs/grok-cli.md new file mode 100644 index 000000000..cc7f7a6db --- /dev/null +++ b/docs/grok-cli.md @@ -0,0 +1,268 @@ +# Grok Build CLI Provider + +## Overview + +The `grok_cli` provider runs the official [xAI Grok Build +CLI](https://docs.x.ai/build) as a long-lived, multi-turn agent in a tmux +window. Community Grok command-line clients and direct xAI API wrappers are +not supported by this provider. + +CAO launches Grok's interactive TUI with inline rendering, adds the selected +agent profile and CAO skill catalog as rules, and exposes CAO orchestration +tools through MCP. Grok's own subagent system is disabled so `assign` and +`handoff` remain the only agent-delegation paths in a CAO session. + +The integration was developed and tested with Grok Build `1.0.0` and the +`grok-4.5` model. Newer Grok versions may change TUI markers or native tool +names; report status or extraction regressions with `grok --version` output. + +## Prerequisites + +- tmux 3.3 or later +- The official `grok` executable on `PATH` +- An authenticated Grok account or an xAI API key + +Install the CLI using xAI's installer: + +```bash +curl -fsSL https://x.ai/cli/install.sh | bash +grok --version +``` + +Authenticate once in a normal terminal before launching it through CAO: + +```bash +grok login +grok models +``` + +For a remote machine without a browser, use `grok login --device-auth`. Grok +also accepts an API key from `XAI_API_KEY`: + +```bash +export XAI_API_KEY="xai-..." +grok models +``` + +Do not put an API key in an agent profile or commit it to a repository. + +## Quick Start + +Start `cao-server`, then install and launch a profile for Grok: + +```bash +cao install developer --provider grok_cli +cao launch --agents developer --provider grok_cli +``` + +Profile instructions use the normal Markdown format. The body is appended to +Grok's native system prompt with `--rules`, together with the runtime CAO skill +catalog. This preserves Grok's coding-agent behavior while applying the +profile's role and protocols. + +Set a default model in profile frontmatter: + +```yaml +--- +name: grok_developer +description: Developer backed by Grok Build +provider: grok_cli +model: grok-4.5 +role: developer +--- + +Implement the requested change and verify it. +``` + +An explicit launch override takes precedence: + +```bash +cao launch --agents grok_developer --provider grok_cli --model grok-4.5 +``` + +Use `grok models` to discover model IDs available to the authenticated +account. + +## Runtime Behavior + +The command has this shape: + +```text +env GROK_SUBAGENTS=0 GROK_WORKFLOWS=0 GROK_GOAL=0 \ + grok --no-alt-screen --no-subagents \ + [--model MODEL] [--rules RULES] \ + [--permission-mode dontAsk --allow RULE ... --deny RULE ...] +``` + +- `--no-alt-screen` keeps the rendered conversation observable by CAO. +- With `allowedTools: ["*"]`, `--always-approve` keeps unrestricted sessions + unattended. For a restricted profile, CAO instead uses Grok's deny-by-default + `--permission-mode dontAsk`, explicitly grants mapped native tools and known + MCP servers, and adds native `--deny` rules as defense in depth. +- Grok may retain built-in read-only behavior in some permission modes. That is + a provider limitation outside CAO's `allowedTools` vocabulary: an explicit + empty CAO allowlist sends `--deny *`, while restricted profiles explicitly + grant only the mapped native/MCP families below. Recheck this behavior after a + Grok CLI upgrade. +- `--no-subagents`, `GROK_SUBAGENTS=0`, `GROK_WORKFLOWS=0`, and + `GROK_GOAL=0` prevent Grok-native workers, workflows, and `/goal` from + bypassing CAO roles, permissions, callbacks, or terminal accounting. This + combination was verified against Grok Build 1.0.0; recheck it after a Grok + upgrade because these controls are not all shown by `grok --help`. +- A single Enter submits bracketed-paste input. `/quit` exits the session. + +### Native workflow opt-in + +CAO-managed terminals disable Grok-native workers by default, including when +`allowedTools: ["*"]` is used. Tool permission is not consent to bypass CAO's +orchestration accounting. To intentionally let this specific Grok profile use +native subagents, workflows, and `/goal`, set the typed profile field: + +```yaml +--- +name: grok_experimental +provider: grok_cli +grokNativeWorkflows: true +--- +``` + +With this opt-in CAO launches Grok with `GROK_SUBAGENTS=1`, +`GROK_WORKFLOWS=1`, and `GROK_GOAL=1`, and omits `--no-subagents`. CAO's MCP +tools remain available to the top-level Grok session, but any Grok-native +workers are outside CAO's profile selection, callback routing, and terminal +accounting. Do not enable this setting where those CAO controls are required. + +The empty `❯` composer may remain visible while Grok is working. CAO therefore +prioritizes current `Waiting for response…` and `Esc:cancel` markers over the +composer. A settled turn has a `Worked for ...` boundary, which CAO also uses +to extract only the latest response in a multi-turn session. + +## MCP Isolation + +CAO creates a private Grok home for every terminal and launches Grok with +`GROK_HOME` pointing to it. The terminal root is mode `0700`; CAO writes its +generated config atomically with mode `0600`. It does not run `grok mcp add` +and does not modify the user's `~/.grok/config.toml`. + +The isolated config contains the profile's MCP servers. CAO injects the +terminal-specific `CAO_TERMINAL_ID` into stdio MCP server environments so +`cao-mcp-server` can route `assign`, `handoff`, and `send_message` correctly. +Existing login state is reused without copying credential contents into CAO +logs or the repository. Generated state is removed when the terminal is +cleaned up. + +A newly isolated home can show Grok's `Help improve Grok` telemetry choice. +The banner is non-blocking and is ignored by CAO's status and response +extraction logic. + +CAO never automatically accepts Grok's directory-trust screen. Accepting it +would enable project-local MCP, LSP, and hook configuration under the terminal +user's privileges; selecting No quits Grok. If that screen is detected, CAO +fails startup with an actionable error. Review and remove project-local +configuration such as `.mcp.json` or `.grok/` before launching the CAO +terminal, or use standalone Grok when you intentionally want to trust it. + +## Tool Restrictions + +Grok is a hard-enforcement provider. CAO translates missing capabilities into +native Grok deny rules: + +| CAO capability | Grok tools denied when absent | +|---|---| +| `execute_bash` | `Bash` | +| `fs_read` | `Read`, `NotebookRead` | +| `fs_write` | `Edit`, `Write`, `NotebookEdit` | +| `fs_list` | `Grep`, `Glob` | +| `web_fetch` | `WebFetch`, `WebSearch`, with web search disabled | + +`allowedTools: ["*"]` adds no restrictive deny rules. It does not enable +Grok-native delegation: CAO keeps subagents, workflows, and `/goal` disabled +unless a profile explicitly sets `grokNativeWorkflows: true`, so `assign` and +`handoff` remain the accountable orchestration mechanisms by default. For a +restricted role, CAO uses `--permission-mode dontAsk` and emits explicit +`--allow` rules for the mapped native tools and configured MCP server names. +It also retains explicit native denies as defense in depth. Arbitrary +`@server` strings never become Grok MCP permission patterns: a server name must +be a literal Grok-safe identifier and be either `cao-mcp-server` or configured +in that profile's `mcpServers` block. + +`@cao-mcp-server` grants Grok's configured CAO MCP server as an all-or-nothing +server-level rule in a restricted profile. CAO does not yet express a rule for +an individual MCP tool such as `send_message` without `assign`; see [Tool +Restrictions](tool-restrictions.md). + +## Assign and Handoff Example + +Install all profiles for this provider before running the full orchestration +example: + +```bash +cao install examples/assign/data_analyst.md --provider grok_cli +cao install examples/assign/report_generator.md --provider grok_cli +cao install examples/assign/analysis_supervisor.md --provider grok_cli +cao launch --agents analysis_supervisor --provider grok_cli --auto-approve +``` + +`--auto-approve` skips CAO's launch confirmation but retains role-based tool +restrictions. Do not substitute `--yolo` when validating supervisor safety. + +## Known Limitations + +- The provider targets Grok Build's interactive TUI and currently requires the + tmux backend. Headless `-p` and ACP modes are not CAO transports. +- TUI parsing is calibrated against Grok Build 1.0.0. A future layout change + may require updated status and extraction fixtures. +- CAO reuses existing Grok authentication. Complete interactive login first; + CAO does not drive account or device-code login screens. +- Per-tool MCP gating is not available. `@cao-mcp-server` does not selectively + hide `assign`, `handoff`, or `send_message`. +- Grok-created non-secret files inside the private `0700` home can use their + own modes; the `0600` guarantee applies to CAO-authored config files. + +## Troubleshooting + +### Login or model errors + +Run `grok login` and `grok models` outside CAO. On a headless host, use +`grok login --device-auth` or set `XAI_API_KEY`. If a profile selects an +unavailable model, replace it with an ID printed by `grok models`. + +### MCP tools are missing or time out + +Confirm `cao-mcp-server` is installed in the same environment as `cao-server`. +Inspect the Grok terminal for an MCP startup error, then recreate the terminal +so CAO regenerates its isolated config and terminal ID. + +### Terminal remains processing + +Attach to the tmux session and check whether Grok still shows +`Waiting for response…` or `Esc:cancel`. If Grok is visibly settled but CAO +does not report completion, include a scrubbed pane capture and `grok --version` +in the bug report. + +### Permission or telemetry prompt is visible + +The telemetry banner is non-blocking. An actual permission picker should be +reported as waiting for user input; answer it in tmux. Restricted tool calls +should be denied automatically rather than prompting. + +### Broken rendering + +Use tmux 3.3 or later and a normal color terminal such as +`TERM=xterm-256color` or `TERM=tmux-256color`. Verify `grok --no-alt-screen` +works in a standalone tmux pane. + +## Validation + +```bash +# Provider unit tests +uv run pytest test/providers/test_grok_cli_unit.py -v -o "addopts=" + +# All Grok lifecycle, permissions, skills, and orchestration e2e tests +uv run pytest -m e2e test/e2e/ -k Grok -v -o "addopts=" + +# Maintainer-required three-analyst workflow +uv run pytest -m e2e \ + test/e2e/test_supervisor_orchestration.py \ + -k GrokCliSupervisorOrchestration -v -o "addopts=" +``` diff --git a/docs/tool-restrictions.md b/docs/tool-restrictions.md index a33184d96..84e53d5e7 100644 --- a/docs/tool-restrictions.md +++ b/docs/tool-restrictions.md @@ -197,15 +197,15 @@ The confirmation prompt is a **review gate** — it shows the resolved role and CAO defines a universal tool vocabulary (`execute_bash`, `fs_read`, `fs_write`, `fs_list`). However, not all providers understand this vocabulary natively. There are two categories: -**Providers that need translation** — Claude Code and Copilot CLI each have their own native tool names (e.g., Claude Code calls bash execution `Bash`, Copilot calls it `shell`). CAO uses an internal `TOOL_MAPPING` to translate the CAO vocabulary to provider-native names, then computes which native tools to block and passes them as CLI flags (e.g., `--disallowedTools Bash`, `--deny-tool shell`). +**Providers that need translation** — Claude Code, Copilot CLI, and Grok Build CLI each have their own native tool names (e.g., Claude Code and Grok call bash execution `Bash`, while Copilot calls it `shell`). CAO uses an internal `TOOL_MAPPING` to translate the CAO vocabulary to provider-native names, then computes which native tools to block and passes them as CLI flags (e.g., `--disallowedTools Bash`, `--deny-tool shell`, or `--deny Bash`). -| CAO Tool | Claude Code | Copilot CLI | -|----------|-------------|-------------| -| `execute_bash` | `Bash` | `shell` | -| `fs_read` | `Read` | `read` | -| `fs_write` | `Edit`, `Write` | `write` | -| `fs_list` | `Glob`, `Grep` | `list`, `grep` | -| `web_fetch` | `WebFetch`, `WebSearch` | (not mapped) | +| CAO Tool | Claude Code | Copilot CLI | Grok Build CLI | +|----------|-------------|-------------|----------------| +| `execute_bash` | `Bash` | `shell` | `Bash` | +| `fs_read` | `Read` | `read` | `Read`, `NotebookRead` | +| `fs_write` | `Edit`, `Write` | `write` | `Edit`, `Write`, `NotebookEdit` | +| `fs_list` | `Glob`, `Grep` | `list`, `grep` | `Grep`, `Glob` | +| `web_fetch` | `WebFetch`, `WebSearch` | (not mapped) | `WebFetch`, `WebSearch` + disabled web search | **Providers that accept CAO vocabulary directly** — Kiro CLI accepts `allowedTools` in the agent JSON at install time, using the same vocabulary as CAO. No translation needed. Kimi CLI and Codex use system prompt instructions to enforce restrictions. For all three, CAO passes the `allowedTools` list directly without translation — so no `TOOL_MAPPING` entry exists for them, and none is needed. @@ -251,6 +251,7 @@ As described in [How Tool Restrictions Are Enforced](#how-tool-restrictions-are- | **Kiro CLI** | Hard | `allowedTools` in agent JSON at install time | | **Copilot CLI** | Hard | `--deny-tool` flags override `--allow-all` | | **OpenCode CLI** | Hard | `permission:` YAML frontmatter enforced natively at install time | +| **Grok Build CLI** | Native (mapped families) | Restricted profiles use deny-by-default `--permission-mode dontAsk` with explicit native/MCP allows and defense-in-depth denies; native subagents are disabled | | **Kimi CLI** | Soft | Security system prompt only | | **Codex** | Soft | Security system prompt only | | **Antigravity CLI** | Soft | Security system prompt only | @@ -280,6 +281,32 @@ claude --dangerously-skip-permissions --disallowedTools Bash --disallowedTools E copilot --allow-all --deny-tool shell --deny-tool write ``` +**Grok Build CLI** — For a restricted profile, uses deny-by-default +`--permission-mode dontAsk`, explicitly grants mapped native tools and known +configured MCP servers, adds native `--deny` rules as defense in depth, and +disables Grok-native worker routes by default: + +```bash +GROK_SUBAGENTS=0 GROK_WORKFLOWS=0 GROK_GOAL=0 \ + grok --permission-mode dontAsk --no-subagents \ + --allow Read --allow Grep --allow 'MCPTool(cao-mcp-server__*)' \ + --deny Bash --deny Edit --deny Write +``` + +`allowedTools: ["*"]` remains the unrestricted path and uses +`--always-approve`. Grok may retain built-in read-only operations in some +permission modes; this provider limitation is not represented by CAO's +`allowedTools` vocabulary. An explicit empty allowlist sends `--deny *`, while +restricted profiles explicitly grant mapped native/MCP families and literal, +configured server names (plus `cao-mcp-server`). + +Set `grokNativeWorkflows: true` in a Grok agent profile only when intentionally +allowing Grok-native workers outside CAO's orchestration accounting. It is +separate from `allowedTools`, including `allowedTools: ["*"]`. + +See the [Grok Build CLI provider guide](grok-cli.md#tool-restrictions) for the +complete mapping and isolation behavior. + **Kimi CLI / Codex** — Prepends to the system prompt: ``` You may ONLY use these tools: @cao-mcp-server, fs_read, fs_list @@ -330,7 +357,7 @@ Each agent is restricted based on its own profile, not its parent's permissions. 1. **Claude Code tool mapping is nearly complete, with MCP tools the remaining gap.** The current mapping covers `Bash` (and its `Task`/`Agent`/`Monitor`/`BashOutput`/`KillShell` execution family), `Read`, `Edit`, `Write`, `Glob`, `Grep`, and — via `web_fetch` — [`WebFetch`](https://code.claude.com/docs/en/permissions#webfetch) and `WebSearch`. The subagent tool is intentionally **not** a separate category: it is folded into `execute_bash`, because a subagent spawns with its own full toolset and can run shell, so exposing it standalone would let a profile grant subagent access without `execute_bash` and re-open that escape. Claude Code **renamed this tool from `Task` to `Agent`**, so both names are denied — current builds expose only `Agent`, so denying just `Task` would be a silent no-op. Provider MCP tools remain unmapped (see limitation #2) — they cannot be blocked via `--disallowedTools`. -2. **`@cao-mcp-server` is a pass-through marker, not enforced at the provider level.** Including `@cao-mcp-server` in `allowedTools` signals intent (this agent should have orchestration tools), but it does **not** translate to any native `--disallowedTools` flag. MCP tools (`handoff`, `assign`, `send_message`, `answer_user_prompt`) are always available to the agent regardless of `allowedTools` — providers do not currently support blocking individual MCP tools. `answer_user_prompt` is exposed by the MCP server, but its structured prompt-navigation behavior is currently implemented for Hermes workers that report `waiting_user_answer`; other providers may only receive ordinary text input until they implement equivalent prompt states. Additionally, `@cao-mcp-server` is all-or-nothing: there is no way to allow only `send_message` while blocking `assign`. Future versions may support `@cao-mcp-server:send_message` syntax for per-tool MCP control. +2. **`@cao-mcp-server` is server-level, not per-tool control.** Grok restricted profiles translate it to an allow rule for the configured CAO MCP server; other providers generally treat it as an intent marker. No provider currently blocks individual MCP tools: once the server is available, its `handoff`, `assign`, `send_message`, and `answer_user_prompt` tools are all available. `answer_user_prompt` is exposed by the MCP server, but its structured prompt-navigation behavior is currently implemented for Hermes workers that report `waiting_user_answer`; other providers may only receive ordinary text input until they implement equivalent prompt states. Future versions may support `@cao-mcp-server:send_message` syntax for per-tool MCP control. 3. **Soft enforcement is best-effort.** Kimi CLI and Codex rely on system prompt instructions to restrict tools. The agent may ignore these restrictions. Do not rely on soft enforcement for security-critical workloads. diff --git a/examples/assign/README.md b/examples/assign/README.md index de41b8a8c..ba3164600 100644 --- a/examples/assign/README.md +++ b/examples/assign/README.md @@ -181,6 +181,7 @@ cao launch --agents analysis_supervisor --provider codex cao launch --agents analysis_supervisor --provider copilot_cli cao launch --agents analysis_supervisor --provider cursor_cli cao launch --agents analysis_supervisor --provider kimi_cli +cao launch --agents analysis_supervisor --provider grok_cli ``` ## Usage @@ -327,7 +328,7 @@ T=33s: Present final report ## E2E Testing -The `data_analyst` and `report_generator` profiles from this directory are used in the E2E test suite to validate assign and handoff flows across all providers (codex, claude_code, kiro_cli, kimi_cli). +The `data_analyst` and `report_generator` profiles from this directory are used in the E2E test suite to validate assign and handoff flows across supported providers, including `grok_cli`. ```bash # Install profiles for E2E testing diff --git a/pyproject.toml b/pyproject.toml index ff2a81f85..80511447b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ dependencies = [ # command unconditionally, so tomli must be a RUNTIME dependency on 3.10 — # not just a dev dependency — or every `cao` command would fail to import. "tomli>=2.0.0; python_version < '3.11'", + "psutil>=7.0.0", ] [project.optional-dependencies] @@ -307,6 +308,7 @@ dev = [ # JWT/JWKS test fixtures (test/fixtures/jwt_factory.py) mint RS256 tokens # with authlib; runtime verification needs only pyjwt[crypto]. "authlib>=1.7.1", + "types-psutil>=7.0.0", ] [tool.black] diff --git a/src/cli_agent_orchestrator/api/main.py b/src/cli_agent_orchestrator/api/main.py index d6b41043e..30c5412ae 100644 --- a/src/cli_agent_orchestrator/api/main.py +++ b/src/cli_agent_orchestrator/api/main.py @@ -2237,6 +2237,7 @@ async def list_providers_endpoint() -> List[Dict]: "opencode_cli": "opencode", "cursor_cli": "agent", "antigravity_cli": "agy", + "grok_cli": "grok", } result = [] for provider, binary in provider_binaries.items(): @@ -2559,7 +2560,22 @@ async def delete_session( result = await asyncio.to_thread( session_service.delete_session, session_name, registry=get_plugin_registry(request) ) + deleted = result.get("deleted") or [] + errors = result.get("errors") or [] + deferred = (isinstance(errors, list) and bool(errors)) or ( + isinstance(deleted, (list, tuple)) and session_name not in deleted + ) + if deferred: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + f"cleanup deferred for session '{session_name}'; " + "retry delete after residual Grok processes exit" + ), + ) return {"success": True, **result} + except HTTPException: + raise except ValueError as e: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) except Exception as e: @@ -5420,7 +5436,17 @@ async def delete_terminal( terminal_id, registry=get_plugin_registry(request), ) - return {"success": success} + if not success: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + f"cleanup deferred for terminal '{terminal_id}'; " + "retry delete after residual Grok processes exit" + ), + ) + return {"success": True} + except HTTPException: + raise except ValueError as e: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) except Exception as e: diff --git a/src/cli_agent_orchestrator/cli/commands/launch.py b/src/cli_agent_orchestrator/cli/commands/launch.py index 271cd18c3..02ebeb00c 100644 --- a/src/cli_agent_orchestrator/cli/commands/launch.py +++ b/src/cli_agent_orchestrator/cli/commands/launch.py @@ -29,6 +29,7 @@ "codex", "copilot_cli", "cursor_cli", + "grok_cli", "hermes", "kimi_cli", "kiro_cli", diff --git a/src/cli_agent_orchestrator/cli/commands/shutdown.py b/src/cli_agent_orchestrator/cli/commands/shutdown.py index 10dde5e22..89fe03b16 100644 --- a/src/cli_agent_orchestrator/cli/commands/shutdown.py +++ b/src/cli_agent_orchestrator/cli/commands/shutdown.py @@ -21,7 +21,21 @@ def _delete_session(name): if response.status_code == 404: click.echo(f"Session '{name}' already removed", err=True) return False + if response.status_code == 409: + raise click.ClickException( + f"Session '{name}' cleanup is pending; retry shutdown after residual " + "Grok processes exit" + ) response.raise_for_status() + try: + payload = response.json() + except ValueError: + payload = {} + if isinstance(payload, dict) and (payload.get("success") is False or payload.get("errors")): + raise click.ClickException( + f"Session '{name}' cleanup is pending; retry shutdown after residual " + "Grok processes exit" + ) return True except requests.exceptions.RequestException as e: raise click.ClickException(f"Failed to connect to cao-server: {e}") diff --git a/src/cli_agent_orchestrator/mcp_server/server.py b/src/cli_agent_orchestrator/mcp_server/server.py index a937fb396..8a649cbc2 100644 --- a/src/cli_agent_orchestrator/mcp_server/server.py +++ b/src/cli_agent_orchestrator/mcp_server/server.py @@ -1530,11 +1530,36 @@ def delete_terminal( response = requests.delete( f"{API_BASE_URL}/terminals/{terminal_id}", timeout=_mcp_timeout() ) + if response.status_code == 409: + return { + "success": False, + "message": ( + f"Terminal {terminal_id} cleanup is pending; retry delete_terminal " + "after the Grok process exits." + ), + } response.raise_for_status() + payload = response.json() + if not payload.get("success", False): + return { + "success": False, + "message": ( + f"Terminal {terminal_id} cleanup is pending; retry delete_terminal " + "after the Grok process exits." + ), + } return {"success": True, "message": f"Terminal {terminal_id} deleted successfully"} except requests.HTTPError as e: if e.response is not None and e.response.status_code == 404: return {"success": False, "message": f"Terminal {terminal_id} not found"} + if e.response is not None and e.response.status_code == 409: + return { + "success": False, + "message": ( + f"Terminal {terminal_id} cleanup is pending; retry delete_terminal " + "after the Grok process exits." + ), + } return {"success": False, "message": f"Failed to delete terminal: {str(e)}"} except Exception as e: return {"success": False, "message": f"Failed to delete terminal: {str(e)}"} diff --git a/src/cli_agent_orchestrator/models/agent_profile.py b/src/cli_agent_orchestrator/models/agent_profile.py index fb1e3af10..3608238c0 100644 --- a/src/cli_agent_orchestrator/models/agent_profile.py +++ b/src/cli_agent_orchestrator/models/agent_profile.py @@ -101,3 +101,10 @@ class AgentProfile(BaseModel): # example one created by `hermes profile alias `). When omitted, # the Hermes provider launches the default `hermes` command. hermesProfile: Optional[str] = Field(default=None, min_length=1) + + # Grok-only. Explicitly permits Grok's own subagents, workflows, and /goal + # engine in this CAO terminal. Omission remains ``None`` so existing profile + # API responses do not gain a new false-valued field; Grok resolves None as + # disabled because those workers are outside CAO's profile, callback, and + # terminal-accounting boundaries. + grokNativeWorkflows: Optional[bool] = None diff --git a/src/cli_agent_orchestrator/models/provider.py b/src/cli_agent_orchestrator/models/provider.py index ef9e8e5cc..0194ad330 100644 --- a/src/cli_agent_orchestrator/models/provider.py +++ b/src/cli_agent_orchestrator/models/provider.py @@ -13,5 +13,6 @@ class ProviderType(str, Enum): HERMES = "hermes" CURSOR_CLI = "cursor_cli" ANTIGRAVITY_CLI = "antigravity_cli" + GROK_CLI = "grok_cli" # Credentials-free mock provider for tests/CI (no real CLI binary). MOCK_CLI = "mock_cli" diff --git a/src/cli_agent_orchestrator/providers/base.py b/src/cli_agent_orchestrator/providers/base.py index bd26960ad..6062dc578 100644 --- a/src/cli_agent_orchestrator/providers/base.py +++ b/src/cli_agent_orchestrator/providers/base.py @@ -269,8 +269,13 @@ def exit_cli(self) -> str: pass @abstractmethod - def cleanup(self) -> None: - """Clean up provider resources.""" + def cleanup(self) -> bool | None: + """Clean up provider resources. + + Providers may return ``False`` when cleanup is intentionally deferred + and lifecycle metadata must be retained for a retry. Existing providers + that return ``None`` are treated as successfully cleaned up. + """ pass def mark_input_received(self) -> None: @@ -291,6 +296,26 @@ def mark_input_received(self) -> None: self._done_first_detected = 0.0 self._idle_first_detected = 0.0 + def notify_status_buffer_reset(self, epoch: int) -> None: + """Notify the provider that StatusMonitor started a fresh byte buffer. + + ``StatusMonitor.clear_rolling_buffer()`` is used immediately before a + new prompt is pasted. Providers that carry state across observations + (for example a completion fingerprint plus a monotonic stream offset) + must not infer continuity through that explicit boundary. The monitor + supplies a monotonically increasing per-terminal ``epoch`` so a + provider can retain stale-screen protections while recognising output + from the newly-dispatched turn. + + Implementations must be synchronous, cheap, and must not call back + into ``StatusMonitor``: the notification is delivered while its lock is + held to make the clear and reset atomic relative to output processing. + """ + + # Most providers only inspect their current rolling buffer and have no + # cross-observation state, so the default is intentionally a no-op. + del epoch + def _resolve_native_status(self, buffer: Optional[str] = None) -> Optional[TerminalStatus]: """Resolve status from the backend's native agent state, if available. diff --git a/src/cli_agent_orchestrator/providers/grok_cli.py b/src/cli_agent_orchestrator/providers/grok_cli.py new file mode 100644 index 000000000..af6f0c3a9 --- /dev/null +++ b/src/cli_agent_orchestrator/providers/grok_cli.py @@ -0,0 +1,1113 @@ +"""Official xAI Grok Build CLI provider implementation. + +Observed with ``grok 1.0.0 (3cd0d0cbce) [stable]`` in ``--no-alt-screen`` +mode. The empty composer remains visible while a turn is running, so status +detection gives the live ``Waiting for response…`` / ``[stop]`` / +``Esc:cancel`` markers priority. Completed turns end at ``Worked for