Skip to content

Commit d27feec

Browse files
authored
Merge branch 'main' into feature/local-cloud-workflow-files
2 parents 25f5e2f + 6eef2da commit d27feec

39 files changed

Lines changed: 938 additions & 160 deletions

File tree

.github/workflows/publish.yml

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,55 @@ jobs:
964964
fi
965965
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
966966
967+
# Publish @agent-relay/harnesses after the publish-packages matrix, which is
968+
# where its exact-version workspace deps (@agent-relay/sdk and
969+
# @agent-relay/harness-driver) land on the registry. Publishing harnesses
970+
# before those exist would leave a window where
971+
# `npm install @agent-relay/harnesses@<v>` cannot resolve its dependencies —
972+
# the same install race the broker/sdk ordering above is built to avoid.
973+
publish-harnesses:
974+
name: Publish @agent-relay/harnesses
975+
needs: [build, publish-packages]
976+
runs-on: ubuntu-latest
977+
if: github.event.inputs.package == 'all'
978+
979+
steps:
980+
- name: Checkout code
981+
uses: actions/checkout@v4
982+
983+
- name: Setup Node.js
984+
uses: actions/setup-node@v4
985+
with:
986+
node-version: '22.14.0'
987+
registry-url: 'https://registry.npmjs.org'
988+
989+
- name: Download build artifacts
990+
uses: actions/download-artifact@v4
991+
with:
992+
name: build-output
993+
path: .
994+
995+
- name: Update npm for OIDC support
996+
run: npm install -g npm@latest
997+
998+
- name: Dry run check
999+
if: github.event.inputs.dry_run == 'true'
1000+
working-directory: packages/harnesses
1001+
run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
1002+
1003+
- name: Publish to NPM
1004+
if: github.event.inputs.dry_run != 'true'
1005+
working-directory: packages/harnesses
1006+
run: |
1007+
set -euo pipefail
1008+
PKG_NAME=$(node -p "require('./package.json').name")
1009+
PKG_VERSION=$(node -p "require('./package.json').version")
1010+
if npm view "${PKG_NAME}@${PKG_VERSION}" version >/dev/null 2>&1; then
1011+
echo "${PKG_NAME}@${PKG_VERSION} already exists on npm; skipping publish"
1012+
exit 0
1013+
fi
1014+
npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
1015+
9671016
# package=main publishes only the root `agent-relay` tarball, but that
9681017
# tarball pins several @agent-relay/* runtime dependencies to the freshly
9691018
# bumped version. Publish those direct deps first so a main-only release
@@ -1528,13 +1577,18 @@ jobs:
15281577
# Create git tag and release
15291578
create-release:
15301579
name: Create Release
1531-
needs: [build, build-broker, build-standalone, verify-binaries, publish-main]
1580+
needs: [build, build-broker, build-standalone, verify-binaries, publish-main, publish-harnesses]
15321581
runs-on: ubuntu-latest
1582+
# publish-harnesses only runs for package=all; for a package=main release it
1583+
# is skipped, which must not block the tag. Gate on "not failed" rather than
1584+
# "succeeded" so a real harness publish failure stops the release but a
1585+
# skipped one does not.
15331586
if: |
15341587
always() &&
15351588
github.event.inputs.package != 'cli-prerelease' &&
15361589
github.event.inputs.dry_run != 'true' &&
15371590
needs.publish-main.result == 'success' &&
1591+
(needs.publish-harnesses.result == 'success' || needs.publish-harnesses.result == 'skipped') &&
15381592
needs.publish-main.outputs.published == 'true'
15391593
15401594
steps:
@@ -2011,6 +2065,7 @@ jobs:
20112065
publish-sdk-internal-deps,
20122066
publish-broker-packages,
20132067
publish-packages,
2068+
publish-harnesses,
20142069
publish-brand-only,
20152070
publish-sdk-py,
20162071
publish-main,
@@ -2053,6 +2108,7 @@ jobs:
20532108
echo "| Publish SDK Internal Deps | ${{ needs.publish-sdk-internal-deps.result == 'success' && '✅' || (needs.publish-sdk-internal-deps.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-sdk-internal-deps.result }} |" >> $GITHUB_STEP_SUMMARY
20542109
echo "| Publish Broker Packages | ${{ needs.publish-broker-packages.result == 'success' && '✅' || (needs.publish-broker-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-broker-packages.result }} |" >> $GITHUB_STEP_SUMMARY
20552110
echo "| Publish Packages | ${{ needs.publish-packages.result == 'success' && '✅' || (needs.publish-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-packages.result }} |" >> $GITHUB_STEP_SUMMARY
2111+
echo "| Publish Harnesses | ${{ needs.publish-harnesses.result == 'success' && '✅' || (needs.publish-harnesses.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-harnesses.result }} |" >> $GITHUB_STEP_SUMMARY
20562112
echo "| Publish Brand | ${{ needs.publish-brand-only.result == 'success' && '✅' || (needs.publish-brand-only.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-brand-only.result }} |" >> $GITHUB_STEP_SUMMARY
20572113
echo "| Publish Python SDK | ${{ needs.publish-sdk-py.result == 'success' && '✅' || (needs.publish-sdk-py.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-sdk-py.result }} |" >> $GITHUB_STEP_SUMMARY
20582114
echo "| Publish Main | ${{ needs.publish-main.result == 'success' && '✅' || (needs.publish-main.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-main.result }} |" >> $GITHUB_STEP_SUMMARY

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `@agent-relay/harnesses` adds a `grok` PTY harness for the Grok CLI, including Relaycast MCP support for spawned agents.
13+
- `@agent-relay/harnesses` is now published to npm, so SDK consumers can install the prebuilt PTY harnesses and harness-authoring helpers.
1214
- `agent-relay drive` and `agent-relay passthrough` add adaptive predictive echo so typing stays responsive when driving a high-latency or remote agent, and stays invisible on fast local links.
1315
- `@agent-relay/harness-driver` exports a reusable `PredictiveEchoEngine` so other attach UIs (CLI, Electron, browser) can share one predictive-echo implementation.
1416
- `@agent-relay/sdk` `relay.addListener(...)` on a workspace client now receives all workspace-visible events: `events.connect()` opens the relaycast 2.5 workspace stream when no agent client is present, so the documented `relay.addListener('message.created', ...)` quickstart path streams without registering an agent.
@@ -135,6 +137,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
135137
- `agent-relay-sdk` refreshes `packages/sdk-py/uv.lock` to clear 20 transitive CVEs across `urllib3` (2.6.3→2.7.0), `gitpython` (3.1.46→3.1.50), `pillow` (12.1.1→12.2.0), `python-multipart` (0.0.22→0.0.29), `cryptography` (46.0.6→48.0.0), `authlib` (1.6.9→1.7.2), `idna` (3.11→3.16), `python-dotenv` (1.1.1→1.2.2), `pytest` (9.0.2→9.0.3), and `uv` (0.9.30→0.11.16). Only `starlette` PYSEC-2026-161 remains pending an upstream `google-adk` upper-bound bump.
136138
- `gemini-relay-extension` refreshes its `package-lock.json` to clear `fast-uri` (GHSA path-traversal via percent-encoded dots) and `path-to-regexp` (GHSA sequential-optional-groups DoS), plus moderate alerts on `hono`, `qs`, `ip-address`, `express-rate-limit`, and `@hono/node-server`.
137139

140+
## [8.2.0] - 2026-06-04
141+
142+
### Added
143+
144+
- Add lifecycle-aware SpawnedAgentHandle
145+
146+
### Changed
147+
148+
- Publish @agent-relay/harnesses on release
149+
150+
## [8.1.2] - 2026-06-04
151+
152+
### Fixed
153+
154+
- Export ./predictive-echo subpath
155+
156+
## [8.1.1] - 2026-06-04
157+
158+
### Changed
159+
160+
- Predictive echo for remote attach + interactive latency fixes
161+
138162
## [8.1.0] - 2026-06-03
139163

140164
### Changed

crates/broker/src/snippets.rs

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ pub fn ensure_cursor_mcp_config(
687687
Ok(changed)
688688
}
689689

690-
/// - `cli`: CLI tool name (e.g. "claude", "codex", "gemini", "droid", "opencode", "cursor")
690+
/// - `cli`: CLI tool name (e.g. "claude", "codex", "gemini", "droid", "grok", "opencode", "cursor")
691691
/// - `agent_name`: the name of the agent being spawned
692692
/// - `api_key`: optional relay API key (empty or `None` means omit)
693693
/// - `base_url`: optional relay base URL (empty or `None` means omit)
@@ -765,6 +765,7 @@ pub async fn configure_agent_relay_mcp_with_result(
765765
let is_gemini = cli_lower == "gemini";
766766
let is_droid = cli_lower == "droid";
767767
let is_opencode = cli_lower == "opencode";
768+
let is_grok = cli_lower == "grok";
768769
let is_cursor = cli_lower == "cursor" || cli_lower == "cursor-agent" || cli_lower == "agent"; // "agent" is cursor-agent's binary name
769770

770771
let api_key = api_key.map(str::trim).filter(|s| !s.is_empty());
@@ -920,6 +921,17 @@ pub async fn configure_agent_relay_mcp_with_result(
920921
None,
921922
)
922923
.await?;
924+
} else if is_grok {
925+
configure_grok_mcp(
926+
cli,
927+
api_key,
928+
base_url,
929+
Some(agent_name),
930+
agent_token,
931+
workspaces_json,
932+
default_workspace,
933+
)
934+
.await?;
923935
} else if is_opencode && !existing_args.iter().any(|a| a == "--agent") {
924936
ensure_opencode_config_with_result(
925937
cwd,
@@ -1110,6 +1122,150 @@ fn gemini_droid_mcp_add_args_with_result(
11101122
args
11111123
}
11121124

1125+
fn grok_mcp_add_args(
1126+
api_key: Option<&str>,
1127+
base_url: Option<&str>,
1128+
agent_name: Option<&str>,
1129+
agent_token: Option<&str>,
1130+
workspaces_json: Option<&str>,
1131+
default_workspace: Option<&str>,
1132+
) -> Vec<String> {
1133+
let mut args = vec!["mcp".to_string(), "add".to_string()];
1134+
if let Some(key) = api_key {
1135+
args.push("--env".to_string());
1136+
args.push(format!("RELAY_API_KEY={key}"));
1137+
}
1138+
if let Some(url) = base_url {
1139+
args.push("--env".to_string());
1140+
args.push(format!("RELAY_BASE_URL={url}"));
1141+
}
1142+
if let Some(name) = agent_name.map(str::trim).filter(|s| !s.is_empty()) {
1143+
args.push("--env".to_string());
1144+
args.push(format!("RELAY_AGENT_NAME={name}"));
1145+
args.push("--env".to_string());
1146+
args.push("RELAY_AGENT_TYPE=agent".to_string());
1147+
args.push("--env".to_string());
1148+
args.push("RELAY_STRICT_AGENT_NAME=1".to_string());
1149+
}
1150+
if let Some(token) = agent_token.map(str::trim).filter(|s| !s.is_empty()) {
1151+
args.push("--env".to_string());
1152+
args.push(format!("RELAY_AGENT_TOKEN={token}"));
1153+
args.push("--env".to_string());
1154+
args.push("RELAY_SKIP_BOOTSTRAP=1".to_string());
1155+
}
1156+
if let Some(wj) = workspaces_json.map(str::trim).filter(|s| !s.is_empty()) {
1157+
args.push("--env".to_string());
1158+
args.push(format!("RELAY_WORKSPACES_JSON={wj}"));
1159+
}
1160+
if let Some(dw) = default_workspace.map(str::trim).filter(|s| !s.is_empty()) {
1161+
args.push("--env".to_string());
1162+
args.push(format!("RELAY_DEFAULT_WORKSPACE={dw}"));
1163+
}
1164+
args.push(AGENT_RELAY_MCP_SERVER.to_string());
1165+
let mcp_command = agent_relay_mcp_command();
1166+
args.push("--command".to_string());
1167+
args.push(mcp_command.command);
1168+
for arg in mcp_command.args {
1169+
args.push("--args".to_string());
1170+
args.push(arg);
1171+
}
1172+
args
1173+
}
1174+
1175+
fn grok_manual_mcp_add_cmd(cli: &str) -> String {
1176+
let mcp_command = agent_relay_mcp_command();
1177+
let rendered_args = mcp_command
1178+
.args
1179+
.iter()
1180+
.map(|arg| format!("--args {arg}"))
1181+
.collect::<Vec<_>>()
1182+
.join(" ");
1183+
format!(
1184+
"{cli} mcp add --env RELAY_API_KEY=<key> --env RELAY_BASE_URL=<url> {AGENT_RELAY_MCP_SERVER} --command {} {rendered_args}",
1185+
mcp_command.command
1186+
)
1187+
}
1188+
1189+
async fn remove_grok_mcp_servers(exe: &str) {
1190+
for server_name in [AGENT_RELAY_MCP_SERVER, LEGACY_RELAYCAST_SERVER] {
1191+
let mut cmd = Command::new(exe);
1192+
cmd.args(["mcp", "remove", server_name])
1193+
.stdin(Stdio::null())
1194+
.stdout(Stdio::null())
1195+
.stderr(Stdio::null());
1196+
if let Ok(mut child) = cmd.spawn() {
1197+
let _ = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
1198+
}
1199+
}
1200+
}
1201+
1202+
#[allow(clippy::too_many_arguments)]
1203+
async fn configure_grok_mcp(
1204+
cli: &str,
1205+
api_key: Option<&str>,
1206+
base_url: Option<&str>,
1207+
agent_name: Option<&str>,
1208+
agent_token: Option<&str>,
1209+
workspaces_json: Option<&str>,
1210+
default_workspace: Option<&str>,
1211+
) -> Result<()> {
1212+
let exe = shlex::split(cli)
1213+
.and_then(|parts| parts.first().cloned())
1214+
.unwrap_or_else(|| cli.trim().to_string());
1215+
let manual_cmd = grok_manual_mcp_add_cmd(&exe);
1216+
1217+
remove_grok_mcp_servers(&exe).await;
1218+
1219+
let mut mcp_cmd = Command::new(&exe);
1220+
mcp_cmd.args(grok_mcp_add_args(
1221+
api_key,
1222+
base_url,
1223+
agent_name,
1224+
agent_token,
1225+
workspaces_json,
1226+
default_workspace,
1227+
));
1228+
mcp_cmd
1229+
.stdin(Stdio::null())
1230+
.stdout(Stdio::null())
1231+
.stderr(Stdio::null());
1232+
1233+
match mcp_cmd.spawn() {
1234+
Ok(mut child) => match tokio::time::timeout(Duration::from_secs(15), child.wait()).await {
1235+
Ok(Ok(status)) if !status.success() => {
1236+
anyhow::bail!(
1237+
"failed to configure Agent Relay MCP for {cli}: `{cli} mcp add` exited with code {:?}. \
1238+
Please configure the Agent Relay MCP server manually:\n {manual_cmd}",
1239+
status.code()
1240+
);
1241+
}
1242+
Ok(Err(error)) => {
1243+
anyhow::bail!(
1244+
"failed to configure Agent Relay MCP for {cli}: {error}. \
1245+
Please configure the Agent Relay MCP server manually:\n {manual_cmd}"
1246+
);
1247+
}
1248+
Err(_) => {
1249+
let _ = child.kill().await;
1250+
let _ = child.wait().await;
1251+
anyhow::bail!(
1252+
"failed to configure Agent Relay MCP for {cli}: `{cli} mcp add` timed out after 15s. \
1253+
Please configure the Agent Relay MCP server manually:\n {manual_cmd}"
1254+
);
1255+
}
1256+
_ => {}
1257+
},
1258+
Err(error) => {
1259+
anyhow::bail!(
1260+
"failed to configure Agent Relay MCP for {cli}: {error}. \
1261+
Please configure the Agent Relay MCP server manually:\n {manual_cmd}"
1262+
);
1263+
}
1264+
}
1265+
1266+
Ok(())
1267+
}
1268+
11131269
#[allow(clippy::too_many_arguments)]
11141270
async fn configure_gemini_droid_mcp(
11151271
cli: &str,
@@ -1584,6 +1740,36 @@ mod tests {
15841740
assert_eq!(args[0], "--mcp-config");
15851741
}
15861742

1743+
#[test]
1744+
fn grok_mcp_add_args_use_command_and_args_flags() {
1745+
let args = super::grok_mcp_add_args(
1746+
Some("rk_live_xyz"),
1747+
Some("https://api.relaycast.dev"),
1748+
Some("GrokWorker"),
1749+
Some("tok_grok_123"),
1750+
None,
1751+
None,
1752+
);
1753+
1754+
assert!(args.starts_with(&["mcp".to_string(), "add".to_string()]));
1755+
assert!(args.contains(&"--env".to_string()));
1756+
assert!(args.contains(&"RELAY_API_KEY=rk_live_xyz".to_string()));
1757+
assert!(args.contains(&"RELAY_AGENT_NAME=GrokWorker".to_string()));
1758+
assert!(args.contains(&"RELAY_AGENT_TOKEN=tok_grok_123".to_string()));
1759+
let server_idx = args
1760+
.iter()
1761+
.position(|arg| arg == "agent-relay")
1762+
.expect("agent-relay arg");
1763+
assert_eq!(args[server_idx + 1], "--command");
1764+
assert_eq!(args[server_idx + 2], "npx");
1765+
assert_eq!(args[server_idx + 3], "--args");
1766+
assert_eq!(args[server_idx + 4], "-y");
1767+
assert_eq!(args[server_idx + 5], "--args");
1768+
assert_eq!(args[server_idx + 6], "agent-relay");
1769+
assert_eq!(args[server_idx + 7], "--args");
1770+
assert_eq!(args[server_idx + 8], "mcp");
1771+
}
1772+
15871773
#[test]
15881774
fn droid_mcp_add_args_include_option_separator() {
15891775
let args = super::gemini_droid_mcp_add_args(

crates/broker/src/worker.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ impl WorkerRegistry {
280280
let is_claude = cli_lower == "claude" || cli_lower.starts_with("claude:");
281281
let is_codex = cli_lower == "codex";
282282
let is_gemini = cli_lower == "gemini";
283+
let is_grok = cli_lower == "grok";
283284
if let Some(model) = apply_codex_model_arg_fallback(
284285
&resolved_cli,
285286
&cli_lower,
@@ -356,6 +357,8 @@ impl WorkerRegistry {
356357
Some("--dangerously-bypass-approvals-and-sandbox")
357358
} else if is_gemini && !effective_args.iter().any(|a| a == "--yolo" || a == "-y") {
358359
Some("--yolo")
360+
} else if is_grok && !effective_args.iter().any(|a| a == "--always-approve") {
361+
Some("--always-approve")
359362
} else {
360363
None
361364
};
@@ -481,6 +484,7 @@ impl WorkerRegistry {
481484
let is_claude = cli_lower == "claude" || cli_lower.starts_with("claude:");
482485
let is_codex = cli_lower == "codex";
483486
let is_gemini = cli_lower == "gemini";
487+
let is_grok = cli_lower == "grok";
484488
if let Some(model) = apply_codex_model_arg_fallback(
485489
&resolved_cli,
486490
&cli_lower,
@@ -559,6 +563,8 @@ impl WorkerRegistry {
559563
&& !effective_args.iter().any(|a| a == "--yolo" || a == "-y")
560564
{
561565
Some("--yolo")
566+
} else if is_grok && !effective_args.iter().any(|a| a == "--always-approve") {
567+
Some("--always-approve")
562568
} else {
563569
None
564570
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agent-relay/monorepo",
3-
"version": "8.1.0",
3+
"version": "8.2.0",
44
"private": true,
55
"description": "Agent Relay monorepo",
66
"type": "module",

0 commit comments

Comments
 (0)