Skip to content
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `relay init --scope project` now tells the truth about Codex: Codex loads `.codex/config.toml` only when that exact project is trusted in `~/.codex/config.toml` (a trusted parent folder does not count, and `codex exec` never asks). In a terminal, `relay init` offers to record that trust; with `--yes` it prints the lines to add.
- The project workspace key file (`.agentworkforce/relay/workspace-key.json`) is no longer picked up by `git add -A`: any command that saves it adds `.agentworkforce/relay/` to `.git/info/exclude` (worktrees included), never to `.gitignore`.
- `relay node up` now reports the actual workspace-key source when startup fails, including the repository's `.agentworkforce/relay/workspace-key.json` pin instead of mislabeling every key as `AGENT_RELAY_WORKSPACE_KEY`.
- Codex sessions set up by `relay init` now read and answer relayed messages inside Codex's default sandbox with no approval prompt: the Codex hooks add `~/.codex/rules/agent-relay.rules`, which lets only the Agent Relay core's own socket client reach its session socket (no network access is granted), and reply commands are a single `agent-relay-probe relay socket-request … --body` call.
- Injected reply instructions now tell agents to answer only through this computer's Agent Relay route and never through another Agent Relay connector or app (such as Agent Relay in ChatGPT), which sends as a different identity.
- `relay init --uninstall` now leaves `~/.codex` as it found it: it removes a `hooks.json` it created (or restores yours byte for byte), its `.relay-desktop-backup`, the Codex hook-trust entries it added to `config.toml`, and its sandbox rules file. Your own hooks, trust entries and rules are kept.

### Security

Expand Down
47 changes: 47 additions & 0 deletions crates/probe/src/probe/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,3 +1182,50 @@ mod tests {
);
}
}

#[cfg(test)]
mod socket_request_tests {
use super::*;

// Review on #2019: a reply that starts with a hyphen is still the body.
#[test]
fn a_socket_request_body_may_start_with_a_hyphen() {
for body in ["- first item", "--help", "-"] {
let cli = Cli::try_parse_from([
"probe",
"relay",
"socket-request",
"--socket",
"/s/relay.sock",
"--method",
"POST",
"--path",
"/send?to=a",
"--body",
body,
])
.unwrap();
let Commands::Relay {
command: relay_commands::RelayCommands::SocketRequest { body: parsed, .. },
} = cli.command
else {
panic!("socket-request expected")
};
assert_eq!(parsed.as_deref(), Some(body));
}
}

// Review on #2019: the read Codex's sandbox rules allow names the
// socket, and `--dev` cannot point it at another one.
#[test]
fn a_read_bound_to_a_socket_refuses_dev() {
let read = |extra: &[&str]| {
let mut args = vec!["probe", "relay", "read", "--socket", "/s/relay.sock"];
args.extend_from_slice(extra);
Cli::try_parse_from(args)
};
assert!(read(&["0123456789ab"]).is_ok());
assert!(read(&["0123456789ab", "--dev"]).is_err());
assert!(read(&["--dev", "0123456789ab"]).is_err());
}
}
21 changes: 18 additions & 3 deletions crates/probe/src/probe/relay_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,13 @@ pub enum RelayCommands {
#[arg(value_name = "REF")]
message_ref: String,
/// Ask Agent Relay Dev instead of Agent Relay.
#[arg(long)]
#[arg(long, conflicts_with = "socket")]
dev: bool,
/// The session socket to ask; default is the one the running app
/// points to. The pointer a session is handed names it, which is
/// what Codex's sandbox rules allow.
#[arg(long)]
socket: Option<PathBuf>,
},
/// Make a session-scoped request to the local relay socket.
SocketRequest {
Expand All @@ -96,6 +101,11 @@ pub enum RelayCommands {
method: String,
#[arg(long)]
path: String,
/// The request body; read from standard input when omitted. One
/// command, with no pipe, is what Codex's sandbox rules allow. A
/// reply may start with a hyphen (`- first item`).
#[arg(long, allow_hyphen_values = true)]
body: Option<String>,
},
/// Agent Relay's Codex hook, which Codex runs after each tool call and
/// as a turn ends: hands the running turn the relayed messages waiting
Expand Down Expand Up @@ -220,8 +230,13 @@ pub fn run(command: RelayCommands) -> Result<()> {
socket,
method,
path,
} => relay::cli::socket_request(&socket, &method, &path),
RelayCommands::Read { message_ref, dev } => relay::cli::read_message(dev, &message_ref),
body,
} => relay::cli::socket_request(&socket, &method, &path, body.as_deref()),
RelayCommands::Read {
message_ref,
dev,
socket,
} => relay::cli::read_message(socket.as_deref(), dev, &message_ref),
RelayCommands::CodexStopHook { dev, socket } => {
relay::cli::codex_stop_hook(socket.as_deref(), dev)
}
Expand Down
32 changes: 24 additions & 8 deletions crates/probe/src/relay/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,38 @@ fn socket_request_allowed(method: &str, path: &str) -> bool {
}
}

pub fn socket_request(socket: &Path, method: &str, path: &str) -> Result<()> {
/// `relay socket-request`: `body` is the request body (`--body`); without
/// it a POST, DELETE or PATCH reads its body from standard input.
pub fn socket_request(socket: &Path, method: &str, path: &str, body: Option<&str>) -> Result<()> {
if !socket_request_allowed(method, path) {
return Err(SafeError("Unsupported relay socket request.".into()).into());
}
let mut body = Vec::new();
if matches!(method, "POST" | "DELETE" | "PATCH") {
std::io::stdin().read_to_end(&mut body)?;
}
let sends_body = matches!(method, "POST" | "DELETE" | "PATCH");
let body = match body {
Some(_) if !sends_body => {
return Err(SafeError(format!("A {method} request takes no --body.")).into());
}
Some(body) => body.as_bytes().to_vec(),
None => {
let mut body = Vec::new();
if sends_body {
std::io::stdin().read_to_end(&mut body)?;
}
body
}
};
socket_exchange(socket, method, path, &body, socket_request_timeout(path))
}

/// `relay read <ref>`: prints a relayed message this session was handed as a
/// pointer, then its note (`message_pointer::read_note`). Like
/// `socket-request`, the session is told from this process, so it reads only
/// its own messages.
pub fn read_message(development: bool, message_ref: &str) -> Result<()> {
let socket = pointed_socket(&paths::home()?, development)?;
pub fn read_message(socket: Option<&Path>, development: bool, message_ref: &str) -> Result<()> {
let socket = match socket {
Some(socket) => socket.to_path_buf(),
None => pointed_socket(&paths::home()?, development)?,
};
let text = read_text(&socket, message_ref)?;
let note = crate::relay::message_pointer::read_note(&text);
std::io::stdout().write_all(format!("{text}\n\n{note}\n").as_bytes())?;
Expand Down Expand Up @@ -781,7 +796,8 @@ mod tests {
] {
assert!(!socket_request_allowed(method, path), "{method} {path}");
}
let error = socket_request(Path::new("/unused"), "GET", "/setup/sign-in").unwrap_err();
let error =
socket_request(Path::new("/unused"), "GET", "/setup/sign-in", None).unwrap_err();
assert!(error
.to_string()
.contains("Unsupported relay socket request"));
Expand Down
Loading
Loading