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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ The workspace is layered bottom-up; each crate depends only on those below it.
- **`cast`** — asciinema v2 `.cast` recording: a `Recorder` writes a header line
plus timestamped output events as bytes arrive (output only; UTF-8 split
across reads is carried over). Used by `terminal` for session capture.
- **`container`** — container-backed terminals, pure argv construction with no
I/O beyond a `$PATH` probe: Docker/Podman detection, the OS profiles behind
"OS Tabs", and the project **sandbox** — one long-lived container a human and
a whole agent team share. The sandbox identity-mounts the project (a path
means the same thing inside and out, so git worktrees stay valid from both
sides), generates the image `Recipe` that installs the agent CLIs, and
discovers containers a project already has by label so an editor's
devcontainer is entered rather than duplicated. `app` drives it; `relay` uses
the same builders so a `docker exec` assembled on either side cannot drift.
See `docs/sandbox.md`.
- **`input`** — keyboard/mouse encoding to terminal byte sequences (CSI, kitty
keyboard protocol, mouse reporting, bracketed paste).
- **`config`** — layered settings: compiled-in defaults overridden by the
Expand Down Expand Up @@ -216,5 +226,8 @@ Keep the vt/terminal layers free of gpui types — the boundary is the bridge.
- `docs/guise.md` — the guise component-library migration: how `vendor/guise` is
wired (the single-gpui patch), the theme bridge, and the surface-by-surface
port status.
- `docs/sandbox.md` — the shared project sandbox: one container for a human and
a whole agent team, the identity mount, the generated image, adoption, and
which `devcontainer.json` fields are honoured.
- `docs/relay.md` — the agent mesh: roles, teams/tiles, the `relay` CLI, and the
MCP coordination tools.
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ The workspace is layered bottom-up; each crate depends only on those below it.
- **`cast`** — asciinema v2 `.cast` recording: a `Recorder` writes a header line
plus timestamped output events as bytes arrive (output only; UTF-8 split
across reads is carried over). Used by `terminal` for session capture.
- **`container`** — container-backed terminals, pure argv construction with no
I/O beyond a `$PATH` probe: Docker/Podman detection, the OS profiles behind
"OS Tabs", and the project **sandbox** — one long-lived container a human and
a whole agent team share. The sandbox identity-mounts the project (a path
means the same thing inside and out, so git worktrees stay valid from both
sides), generates the image `Recipe` that installs the agent CLIs, and
discovers containers a project already has by label so an editor's
devcontainer is entered rather than duplicated. `app` drives it; `relay` uses
the same builders so a `docker exec` assembled on either side cannot drift.
See `docs/sandbox.md`.
- **`input`** — keyboard/mouse encoding to terminal byte sequences (CSI, kitty
keyboard protocol, mouse reporting, bracketed paste).
- **`config`** — layered settings: compiled-in defaults overridden by the
Expand Down Expand Up @@ -215,5 +225,8 @@ Keep the vt/terminal layers free of gpui types — the boundary is the bridge.
- `docs/guise.md` — the guise component-library migration: how `vendor/guise` is
wired (the single-gpui patch), the theme bridge, and the surface-by-surface
port status.
- `docs/sandbox.md` — the shared project sandbox: one container for a human and
a whole agent team, the identity mount, the generated image, adoption, and
which `devcontainer.json` fields are honoured.
- `docs/relay.md` — the agent mesh: roles, teams/tiles, the `relay` CLI, and the
MCP coordination tools.
40 changes: 22 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async-task = { git = "https://github.com/smol-rs/async-task.git", rev = "b4486cd
block = { path = "thirdparty/block" }

[workspace.package]
version = "1.30.1"
version = "1.31.0"
edition = "2021"
license = "Apache-2.0"

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ is migrated automatically on first launch.)
"confirm-quit": true, // warn if a process is still running
"shell-integration": true, // OSC 133/7 prompt-jump + cwd hooks
"session-restore": false, // reopen tabs/splits on launch
"tab-title-show-host": false, // keep user@host: in tab titles
"tab-title-show-host": false, // keep user@host: in tab titles (off: just the path)

// AI — opt-in (also editable in Settings → AI); see docs/relay.md
"ai-enabled": true,
Expand All @@ -183,6 +183,7 @@ is migrated automatically on first launch.)
"relay-default-agent": "claude",
"relay-team-window": true, // a team gets its own window
"relay-team-autonomy": true, // team members skip permission prompts
"sandbox-enabled": false, // run this project + its agents in one container

// Keybindings — trigger=action[:param]; use =unbind to remove a default
"keybind": [
Expand Down Expand Up @@ -312,6 +313,12 @@ and optionally start it on launch. An **AI** menu then appears:
the layout you were working in left alone. Members run unattended (their
permission prompts skipped), since nobody is sitting in each pane to answer
one. Both are settings — `relay-team-window` and `relay-team-autonomy`.
- **Sandbox ▸** — put this project and its whole team inside **one container**
instead of running on the host: a shared filesystem, one toolchain, and a real
boundary around agents whose prompts you just turned off. Needs Docker or
Podman and nothing else — no VS Code, no `devcontainer` CLI, no image to pull.
The project is mounted at its own path, so git worktrees stay valid on both
sides. See [`docs/sandbox.md`](docs/sandbox.md).

The same `relay` binary works on its own (`relay start`, `relay launch <name>`,
`relay feed --follow`, `relay ps`, `relay stop`). **Claude** and **Codex** join
Expand Down
2 changes: 2 additions & 0 deletions crates/app/src/agentpicker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,14 @@ impl AgentPickerView {
role: role.clone(),
task: task.clone(),
});
let sandbox = crate::root::sandbox::active_ref(cx);
let cmd = crate::relay::launch_agent_command(
&self.opts,
&provider,
&name,
role.as_deref(),
task.as_deref(),
sandbox.as_ref(),
);
create(cx, cmd, window);
}
Expand Down
1 change: 1 addition & 0 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod reload;
mod rename;
mod resume;
mod root;
mod sandbox;
mod session;
mod sessionstate;
mod sidecar;
Expand Down
50 changes: 48 additions & 2 deletions crates/app/src/relay/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ pub fn launch_agent_command(
name: &str,
role: Option<&str>,
task: Option<&str>,
sandbox: Option<&SandboxRef>,
) -> String {
let r = resolve_provider(opts, provider);
let mut s = format!(
Expand Down Expand Up @@ -162,16 +163,23 @@ pub fn launch_agent_command(
for arg in &r.args {
s.push_str(&format!(" --agent-arg {}", sh_quote(arg)));
}
if let Some(sandbox) = sandbox {
s.push_str(&sandbox.flags());
}
keep_open(s)
}

/// Immediately launch a configured provider (Claude Code, Codex, …) as a one-off
/// agent — the quick-launch menu entries. Reuses [`launch_agent_command`] (so the
/// token-optimization threading applies) with a generated unique name, the
/// default `worker` role, and no standing task.
pub fn quick_launch_command(opts: &config::Options, provider: &str) -> String {
pub fn quick_launch_command(
opts: &config::Options,
provider: &str,
sandbox: Option<&SandboxRef>,
) -> String {
let name = unique_agent_name(provider);
launch_agent_command(opts, provider, &name, None, None)
launch_agent_command(opts, provider, &name, None, None, sandbox)
}

/// A friendly display name for a provider, for menus. Built-ins get their brand
Expand Down Expand Up @@ -270,3 +278,41 @@ pub(crate) fn keep_open(cmd: String) -> String {
"{cmd} || {{ echo; echo '[relay] launch failed — check Settings → AI (is the server running?)'; exec \"${{SHELL:-/bin/sh}}\"; }}"
)
}

/// Where an agent should run: inside the project's shared sandbox, so it lands
/// in the same filesystem and toolchain as every other pane on the team.
///
/// Relay stays on the host either way — only the agent itself is exec'd inside
/// the container — so this is a set of flags on the `relay launch` command
/// rather than a different command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxRef {
pub engine: String,
pub name: String,
/// Working directory inside the container. With the identity mount the
/// sandbox uses, this is the project's own path.
pub workdir: String,
}

impl SandboxRef {
/// The `--sandbox …` flags to append to a `relay launch` command line.
pub fn flags(&self) -> String {
format!(
" --sandbox {} --sandbox-engine {} --sandbox-workdir {} --sandbox-relay-dir {}",
sh_quote(&self.name),
sh_quote(&self.engine),
sh_quote(&self.workdir),
sh_quote(container::RELAY_DIR),
)
}
}

/// `SandboxRef` for a resolved sandbox, or `None` when the agent runs on the
/// host.
pub fn sandbox_ref(sandbox: Option<&container::Sandbox>) -> Option<SandboxRef> {
sandbox.map(|s| SandboxRef {
engine: s.engine.binary().to_string(),
name: s.name.clone(),
workdir: s.workdir.clone(),
})
}
9 changes: 7 additions & 2 deletions crates/app/src/relay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,19 @@ pub fn save_agent_def(def: AgentDef) {
}

/// Build the launch command for a previously-saved agent.
pub fn launch_saved_command(opts: &config::Options, name: &str) -> Option<String> {
pub fn launch_saved_command(
opts: &config::Options,
name: &str,
sandbox: Option<&SandboxRef>,
) -> Option<String> {
let def = list_agent_defs().into_iter().find(|d| d.name == name)?;
Some(launch_agent_command(
opts,
&def.provider,
&def.name,
def.role.as_deref(),
def.task.as_deref(),
sandbox,
))
}

Expand All @@ -97,7 +102,7 @@ pub(crate) fn binary() -> String {

/// Fixed state directory for the mesh, beside the config file, so every relay
/// call shares one mesh regardless of the calling pane's working directory.
fn home() -> PathBuf {
pub(crate) fn home() -> PathBuf {
config::default_path()
.and_then(|p| p.parent().map(|d| d.join("relay")))
.unwrap_or_else(|| PathBuf::from(".relay"))
Expand Down
Loading
Loading