Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .rules/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
- Software / updates: `https://software.cortex.foundation`.
- The harness is a client. Chat/Code turns, tools, computers, plugins, and snapshots go through that API. Do not embed a second model provider.
- Code turns: `POST /v1/code/sessions/{id}/turns` with `{ message, mode: "chat"|"code" }`. Plan/ask are TUI/harness locks, not API modes.
- Code runtime: **Cloud is the shipped default** for the TUI and `cortex exec` (Designer Q9, `CLI_100_CHROME_LOCK_SIGNED`). This PC and SSH are explicit opt-in (`CORTEX_COMPUTER`, or `CORTEX_SSH_HOST` / `CORTEX_SSH_TARGET`) and may ship in 0.1.x. They require an already connected Code session; do not substitute Cloud.
- When the API is unreachable, the product fails closed with a product-facing error. Do not fall back to a local mock model.
- Environment overrides (`CORTEX_API_URL`, `CORTEX_API_KEY`, `CORTEX_AUTH_TOKEN`) are for operators and tests. Defaults must stay production Cortex URLs.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

### Changed
- Code turns default to the **Cloud** runtime for the TUI and `cortex exec` (Designer Q9 / `CLI_100_CHROME_LOCK_SIGNED`). This PC and SSH are explicit opt-in in 0.1.x (`CORTEX_COMPUTER` or `CORTEX_SSH_HOST`) and refuse a fresh session with product copy instead of blocking every first turn.
- README `docs/media/intro.gif` sits on a photographed green forest desktop (not teal blobs): Terminal chrome, a pointer that walks titlebar → composer → slash / model → Shell, and the signed lock TUI. Local CLI only — no Cortex Cloud handoff in the banner story.

## 0.1.9
Expand Down
13 changes: 13 additions & 0 deletions docs/configuration/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ See [Data locations](data-locations.md) for the defaults these override.
Interactive use should prefer `cortex login`, which stores the session in the OS
keyring. See [Signing in](../reference/login.md).

## Code runtime

The TUI and `cortex exec` create a **Cloud** Code session unless you explicitly
select This PC or SSH. That is the shipped default (Designer Q9). A fresh
install can complete a turn without extra configuration. This PC and SSH are
opt-in and may ship in 0.1.x.

| Variable | Effect |
|----------|--------|
| `CORTEX_COMPUTER` | Where tools run. Unset (or `cloud`) uses Cloud. Set `this_pc` (aliases: `this-pc`, `local`, `paired`, `connected`) or `ssh` to select those runtimes. This PC and SSH require an already connected Code session; Cortex will not substitute Cloud. |
| `CORTEX_SSH_HOST` | SSH target. When set (or `CORTEX_SSH_TARGET`), the runtime is SSH — same connected-session rule as `CORTEX_COMPUTER=ssh`. |
| `CORTEX_SSH_TARGET` | Alias of `CORTEX_SSH_HOST`. |

## Model selection

| Variable | Effect |
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Cortex has two non-interactive entry points:
| `cortex run` | A single request from your own shell. Streams a formatted answer, can continue a session, can share the result. |

Both work without a terminal, so they are safe in pipelines where the
[TUI](tui.md) refuses to start.
[TUI](tui.md) refuses to start. Turns use the **Cloud** Code runtime unless you
set `CORTEX_COMPUTER` to `this_pc` or `ssh` (see
[Environment variables](../configuration/env.md)).

## `cortex exec`

Expand Down
9 changes: 6 additions & 3 deletions docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,12 @@ cortex

You get the session view from the recording on the [docs index](../README.md):
a timeline, a composer at the bottom, and a status line showing the current mode
and autonomy level. The welcome card shows the working directory and
**Computer** (`This PC` when you started in a workspace, `Cloud` or `SSH` when
those are configured). Type what you want changed and press `Enter`.
and autonomy level. The TUI and `cortex exec` run on the **Cloud** Code runtime
by default (Designer Q9), so a fresh install can complete a turn without extra
configuration. To run tools on This PC or over SSH, set `CORTEX_COMPUTER` (see
[Environment variables](../configuration/env.md)). Those runtimes are explicit
opt-in in 0.1.x and need an already connected Code session; Cortex will not
substitute Cloud. Type what you want changed and press `Enter`.

Turns go to the Code session API (`POST /v1/code/sessions/{id}/turns`) with
streaming tokens and first-class tool rows. Press `Esc` to cancel a turn that
Expand Down
7 changes: 7 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ Work through:
will tell you.
4. Are you signed in? `cortex whoami`.

## "This PC and SSH Code execution require an already connected Code session"

You selected This PC or SSH (`CORTEX_COMPUTER` or `CORTEX_SSH_HOST`) without a
connected Code session. Those runtimes are never created on the fly, and Cloud
is not substituted. Either resume a session that already has a host, or unset
`CORTEX_COMPUTER` to use Cloud. See [Environment variables](configuration/env.md).

## The TUI will not start

Cortex needs a terminal on both stdin and stdout. In a pipeline, a CI job, or
Expand Down
53 changes: 53 additions & 0 deletions src/cortex-cli/tests/exec_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ impl Run {
.env("CORTEX_HOME", self.home.path())
.env("CORTEX_API_KEY", "offline-fixture")
.env("CORTEX_API_URL", UNREACHABLE)
.env_remove("CORTEX_COMPUTER")
.env_remove("CORTEX_SSH_HOST")
.env_remove("CORTEX_SSH_TARGET")
.env("RUST_LOG", "off")
.current_dir(self.home.path());
command
Expand Down Expand Up @@ -64,6 +67,56 @@ fn frames(stdout: &[u8]) -> Vec<serde_json::Value> {
.collect()
}

#[test]
fn default_runtime_reaches_the_api_instead_of_refusing_this_pc() {
let run = Run::new();
let output = run.exec(&["--output-format", "json", "--timeout", "20", "hello"], "");
assert!(!output.status.success());
let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let error = result["error"].as_str().unwrap_or_default();
assert!(
!error.contains("already connected Code session"),
"unset CORTEX_COMPUTER must not refuse as This PC: {result}"
);
assert_eq!(
result["num_turns"], 1,
"the Cloud default must start a turn instead of refusing locally: {result}"
);
assert!(
error.contains("temporarily unavailable")
|| error.contains("cortex login")
|| error.contains("CORTEX_API_KEY"),
"default Cloud path fails closed after attempting a session, never This PC: {result}"
);
}

#[test]
fn this_pc_without_a_session_refuses_with_product_copy() {
let run = Run::new();
let mut command = run.command(&["--output-format", "json", "--timeout", "20", "hello"]);
command.env("CORTEX_COMPUTER", "this_pc");
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"").unwrap();
let output = child.wait_with_output().unwrap();
assert!(!output.status.success());
let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let error = result["error"].as_str().unwrap_or_default();
assert!(
error.contains("This PC") && error.contains("already connected Code session"),
"This PC without a session must use product copy: {result}"
);
assert!(
error.contains("CORTEX_COMPUTER"),
"the refuse path must name the explicit override: {result}"
);
assert!(!error.to_lowercase().contains("reqwest"), "{result}");
}

#[test]
fn an_unreachable_service_fails_the_run_and_reports_an_error_result_not_a_success() {
let run = Run::new();
Expand Down
69 changes: 2 additions & 67 deletions src/cortex-engine/src/client/code_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,62 +56,7 @@ pub fn normalize_api_base(url: &str) -> String {
/// Guest-cookie token prefix stored in the keyring / env.
pub const GUEST_TOKEN_PREFIX: &str = "gt:";

/// Where tools run for this CLI session.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ComputerKind {
/// Local workspace passed to the CLI (This PC).
#[default]
ThisPc,
/// Cloud runtime (Firecracker VM when the API provisions one).
Cloud,
/// SSH remote, when `CORTEX_SSH_HOST` is set.
Ssh,
}

impl ComputerKind {
/// Detect from the environment. A workspace path (cwd) means This PC
/// unless the operator forces cloud or sets an SSH target.
pub fn detect() -> Self {
if std::env::var("CORTEX_SSH_HOST")
.ok()
.filter(|s| !s.is_empty())
.is_some()
|| std::env::var("CORTEX_SSH_TARGET")
.ok()
.filter(|s| !s.is_empty())
.is_some()
{
return Self::Ssh;
}
match std::env::var("CORTEX_COMPUTER")
.unwrap_or_default()
.to_ascii_lowercase()
.as_str()
{
"cloud" => Self::Cloud,
"ssh" => Self::Ssh,
_ => Self::ThisPc,
}
}

/// Product label for the TUI.
pub fn label(self) -> &'static str {
match self {
Self::ThisPc => "This PC",
Self::Cloud => "Cloud",
Self::Ssh => "SSH",
}
}

pub fn as_str(self) -> &'static str {
match self {
Self::ThisPc => "this_pc",
Self::Cloud => "cloud",
Self::Ssh => "ssh",
}
}
}
pub use super::computer::{ComputerKind, DISCONNECTED_RUNTIME};

/// Per-turn context the TUI sets before `complete()`.
#[derive(Debug, Clone, Default)]
Expand Down Expand Up @@ -533,9 +478,7 @@ impl CodeAgentClient {
}
let ctx = self.turn_context();
if ctx.computer != ComputerKind::Cloud {
return Err(CortexError::InvalidInput(
"Local and SSH Code execution require an already connected Code session. Connect a host and resume that session, or explicitly select Cloud. No runtime was substituted.".into()
));
return Err(CortexError::InvalidInput(DISCONNECTED_RUNTIME.into()));
}
Ok(self
.create_session_with(CreateCodeSession {
Expand Down Expand Up @@ -955,14 +898,6 @@ mod tests {
assert_eq!(GUEST_TOKEN_PREFIX, "gt:");
}

#[test]
fn computer_kind_labels() {
assert_eq!(ComputerKind::ThisPc.label(), "This PC");
assert_eq!(ComputerKind::Cloud.label(), "Cloud");
assert_eq!(ComputerKind::Ssh.label(), "SSH");
assert_eq!(ComputerKind::ThisPc.as_str(), "this_pc");
}

#[test]
fn parses_tool_start_with_arguments() {
let raw = r#"{"type":"tool_start","invocation_id":"tci_2","tool_name":"Bash","arguments":{"command":"ls"}}"#;
Expand Down
Loading
Loading