Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Trajectory: Review and fix PR #1018

> **Status:** ✅ Completed
> **Confidence:** 78%
> **Started:** May 31, 2026 at 04:56 PM
> **Completed:** May 31, 2026 at 04:59 PM

---

## Summary

Reviewed PR #1018, fixed PTY relay-agent env inheritance for skip_relay_prompt launcher mode, added focused unit coverage and changelog entry.

**Approach:** Standard approach

---

## Key Decisions

### Explicitly remove inherited PTY relay-agent env before applying child contract

- **Chose:** Explicitly remove inherited PTY relay-agent env before applying child contract
- **Reasoning:** Tokio Command inherits the broker process environment by default, so skip_relay_prompt could still leak RELAY_AGENT_TOKEN, RELAY_AGENT_TYPE, or RELAY_STRICT_AGENT_NAME unless the keys are removed.

---

## Chapters

### 1. Work

_Agent: default_

- Explicitly remove inherited PTY relay-agent env before applying child contract: Explicitly remove inherited PTY relay-agent env before applying child contract
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"id": "traj_g4s1nlhpccd5",
"version": 1,
"task": {
"title": "Review and fix PR #1018"
},
"status": "completed",
"startedAt": "2026-05-31T16:56:25.502Z",
"completedAt": "2026-05-31T16:59:48.583Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-05-31T16:59:00.008Z"
}
],
"chapters": [
{
"id": "chap_fiwxq1j8cgqh",
"title": "Work",
"agentName": "default",
"startedAt": "2026-05-31T16:59:00.008Z",
"endedAt": "2026-05-31T16:59:48.583Z",
"events": [
{
"ts": 1780246740008,
"type": "decision",
"content": "Explicitly remove inherited PTY relay-agent env before applying child contract: Explicitly remove inherited PTY relay-agent env before applying child contract",
"raw": {
"question": "Explicitly remove inherited PTY relay-agent env before applying child contract",
"chosen": "Explicitly remove inherited PTY relay-agent env before applying child contract",
"alternatives": [],
"reasoning": "Tokio Command inherits the broker process environment by default, so skip_relay_prompt could still leak RELAY_AGENT_TOKEN, RELAY_AGENT_TYPE, or RELAY_STRICT_AGENT_NAME unless the keys are removed."
},
"significance": "high"
}
]
}
],
"retrospective": {
"summary": "Reviewed PR #1018, fixed PTY relay-agent env inheritance for skip_relay_prompt launcher mode, added focused unit coverage and changelog entry.",
"approach": "Standard approach",
"confidence": 0.78
},
"commits": [],
"filesChanged": [],
"projectId": "AgentWorkforce/relay",
"tags": [],
"_trace": {
"startRef": "0812e0203027da61332a8c58b518f0d29c2d9d03",
"endRef": "0812e0203027da61332a8c58b518f0d29c2d9d03"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Trajectory: Review and fix PR #1018

> **Status:** ✅ Completed
> **Confidence:** 90%
> **Started:** May 31, 2026 at 05:06 PM
> **Completed:** May 31, 2026 at 05:16 PM

---

## Summary

Reviewed PR #1018, added process-level PTY env regression coverage for skip_relay_prompt, and validated broker tests and formatting.

**Approach:** Standard approach

---

## Key Decisions

### Added process-level PTY env regression test

- **Chose:** Added process-level PTY env regression test
- **Reasoning:** The implementation bug depends on Command env removal, so coverage should verify stale relay-agent variables are absent from an actual child process when skip_relay_prompt is true.

---

## Chapters

### 1. Work

_Agent: default_

- Added process-level PTY env regression test: Added process-level PTY env regression test
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"id": "traj_j846cp13mg9k",
"version": 1,
"task": {
"title": "Review and fix PR #1018"
},
"status": "completed",
"startedAt": "2026-05-31T17:06:07.483Z",
"completedAt": "2026-05-31T17:16:17.170Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-05-31T17:07:34.628Z"
}
],
"chapters": [
{
"id": "chap_4e5d935w0w6j",
"title": "Work",
"agentName": "default",
"startedAt": "2026-05-31T17:07:34.628Z",
"endedAt": "2026-05-31T17:16:17.170Z",
"events": [
{
"ts": 1780247254629,
"type": "decision",
"content": "Added process-level PTY env regression test: Added process-level PTY env regression test",
"raw": {
"question": "Added process-level PTY env regression test",
"chosen": "Added process-level PTY env regression test",
"alternatives": [],
"reasoning": "The implementation bug depends on Command env removal, so coverage should verify stale relay-agent variables are absent from an actual child process when skip_relay_prompt is true."
},
"significance": "high"
}
]
}
],
"retrospective": {
"summary": "Reviewed PR #1018, added process-level PTY env regression coverage for skip_relay_prompt, and validated broker tests and formatting.",
"approach": "Standard approach",
"confidence": 0.9
},
"commits": [],
"filesChanged": [],
"projectId": "AgentWorkforce/relay",
"tags": [],
"_trace": {
"startRef": "dc98e12d9ece9e841930ed9a922b22b5ff3ea573",
"endRef": "dc98e12d9ece9e841930ed9a922b22b5ff3ea573"
}
}
7 changes: 3 additions & 4 deletions .github/workflows/pullfrog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,21 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY:
${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}

# for Amazon Bedrock (https://docs.pullfrog.com/bedrock)
# AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# AWS_REGION: us-east-1
# BEDROCK_MODEL_ID: <bedrock-model-id>

# for Google Vertex AI (https://docs.pullfrog.com/vertex)
# VERTEX_SERVICE_ACCOUNT_JSON: ${{ secrets.VERTEX_SERVICE_ACCOUNT_JSON }}
# GOOGLE_CLOUD_PROJECT: my-project
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- `@agent-relay/cloud`: CLI browser login ignores stray localhost callbacks with an invalid state parameter, so first-time sign-ins are not shown a false hosted error or aborted before the real OAuth callback returns.
- `agent-relay-broker` PTY spawns with `skip_relay_prompt` now receive the assigned `RELAY_AGENT_NAME` for launcher wiring without inheriting broker relay-agent registration env.
- `agent-relay-broker` harness configs now report harness PIDs instead of wrapper worker PIDs, validate app-server protocol/auth/host settings at spawn, and give app-server release requests time to finish.
- `@agent-relay/sdk` normalizes broker `pid: null` spawn responses to `undefined` while PTY harness PIDs are reported asynchronously.
- `web`: PR preview SST deploys use and comment the generated CloudFront URL and AWS's managed disabled cache policy instead of creating per-preview Cloudflare DNS records, ACM certificates, and custom CloudFront cache policies.
Expand Down
132 changes: 125 additions & 7 deletions crates/broker/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ const APP_SERVER_AUTH_ENV_KEYS: [&str; 4] = [
"AGENT_RELAY_APP_SERVER_AUTH_USERNAME",
"AGENT_RELAY_APP_SERVER_AUTH_PASSWORD",
];
const RELAY_AGENT_CHILD_ENV_KEYS: [&str; 4] = [
"RELAY_AGENT_NAME",
"RELAY_AGENT_TOKEN",
"RELAY_AGENT_TYPE",
"RELAY_STRICT_AGENT_NAME",
];
const DEFAULT_RELEASE_GRACE: Duration = Duration::from_secs(2);
const APP_SERVER_RELEASE_GRACE: Duration = Duration::from_secs(35);

Expand Down Expand Up @@ -699,13 +705,13 @@ impl WorkerRegistry {
command.env(key, value);
}
}
if !skip_relay_prompt && matches!(spec.runtime, AgentRuntime::Pty) {
if let Some(relay_key) = worker_relay_api_key {
command.env("RELAY_AGENT_TOKEN", relay_key);
}
command.env("RELAY_AGENT_NAME", &spec.name);
command.env("RELAY_AGENT_TYPE", "agent");
command.env("RELAY_STRICT_AGENT_NAME", "1");
if matches!(spec.runtime, AgentRuntime::Pty) {
apply_pty_relay_agent_env(
&mut command,
&spec.name,
worker_relay_api_key.as_deref(),
skip_relay_prompt,
);
}
// Remove CLAUDECODE from child env to prevent nested Claude Code instances
// from interfering with the parent's session management
Expand Down Expand Up @@ -1021,6 +1027,49 @@ fn release_grace_for_spec(spec: &AgentSpec) -> Duration {
}
}

fn pty_relay_agent_env_overrides(
agent_name: &str,
worker_relay_api_key: Option<&str>,
skip_relay_prompt: bool,
) -> Vec<(&'static str, Option<String>)> {
let token = if skip_relay_prompt {
None
} else {
worker_relay_api_key.map(str::to_string)
};
let agent_type = (!skip_relay_prompt).then(|| "agent".to_string());
let strict_name = (!skip_relay_prompt).then(|| "1".to_string());

vec![
("RELAY_AGENT_NAME", Some(agent_name.to_string())),
("RELAY_AGENT_TOKEN", token),
("RELAY_AGENT_TYPE", agent_type),
("RELAY_STRICT_AGENT_NAME", strict_name),
]
}

fn apply_pty_relay_agent_env(
command: &mut Command,
agent_name: &str,
worker_relay_api_key: Option<&str>,
skip_relay_prompt: bool,
) {
// Command inherits the broker process env by default, so unset every
// relay-agent key before applying the PTY child contract. Skipped prompt
// injection still exposes the assigned name for wrapper launchers, but not
// the broker-injected MCP registration env.
for key in RELAY_AGENT_CHILD_ENV_KEYS {
command.env_remove(key);
}
let overrides =
pty_relay_agent_env_overrides(agent_name, worker_relay_api_key, skip_relay_prompt);
for (key, value) in overrides {
if let Some(value) = value {
command.env(key, value);
}
}
}

fn validate_app_server_config(config: &HeadlessHarnessConfig) -> Result<()> {
if !matches!(&config.driver, HeadlessHarnessDriver::AppServer) {
anyhow::bail!("unsupported headless harness driver");
Expand Down Expand Up @@ -1607,6 +1656,75 @@ mod tests {
assert_eq!(reg.env_value("MISSING"), None);
}

#[test]
fn pty_relay_env_exposes_name_when_prompt_injection_is_skipped() {
let overrides: HashMap<_, _> =
pty_relay_agent_env_overrides("LauncherWorker", Some("tok_worker"), true)
.into_iter()
.collect();

assert_eq!(
overrides.get("RELAY_AGENT_NAME").and_then(|v| v.as_deref()),
Some("LauncherWorker")
);
assert_eq!(overrides.get("RELAY_AGENT_TOKEN"), Some(&None));
assert_eq!(overrides.get("RELAY_AGENT_TYPE"), Some(&None));
assert_eq!(overrides.get("RELAY_STRICT_AGENT_NAME"), Some(&None));
}

#[test]
fn pty_relay_env_includes_registration_vars_when_injecting_prompt() {
let overrides: HashMap<_, _> =
pty_relay_agent_env_overrides("RelayWorker", Some("tok_worker"), false)
.into_iter()
.collect();

assert_eq!(
overrides.get("RELAY_AGENT_NAME").and_then(|v| v.as_deref()),
Some("RelayWorker")
);
assert_eq!(
overrides
.get("RELAY_AGENT_TOKEN")
.and_then(|v| v.as_deref()),
Some("tok_worker")
);
assert_eq!(
overrides.get("RELAY_AGENT_TYPE").and_then(|v| v.as_deref()),
Some("agent")
);
assert_eq!(
overrides
.get("RELAY_STRICT_AGENT_NAME")
.and_then(|v| v.as_deref()),
Some("1")
);
}

#[tokio::test]
async fn pty_relay_env_removes_existing_registration_vars_when_prompt_injection_is_skipped() {
let mut command = Command::new("env");
command.env("RELAY_AGENT_NAME", "InheritedName");
command.env("RELAY_AGENT_TOKEN", "inherited-token");
command.env("RELAY_AGENT_TYPE", "human");
command.env("RELAY_STRICT_AGENT_NAME", "1");

apply_pty_relay_agent_env(&mut command, "LauncherWorker", Some("tok_worker"), true);

let output = command.output().await.expect("env command should run");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).expect("env output should be utf-8");
let env: HashMap<_, _> = stdout
.lines()
.filter_map(|line| line.split_once('='))
.collect();

assert_eq!(env.get("RELAY_AGENT_NAME"), Some(&"LauncherWorker"));
assert!(!env.contains_key("RELAY_AGENT_TOKEN"));
assert!(!env.contains_key("RELAY_AGENT_TYPE"));
assert!(!env.contains_key("RELAY_STRICT_AGENT_NAME"));
}

fn make_app_server_config() -> HeadlessHarnessConfig {
HeadlessHarnessConfig {
driver: HeadlessHarnessDriver::AppServer,
Expand Down
Loading