From 8a7969ac414debd3e62e6304609aca227a98e88c Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 4 Nov 2026 08:53:53 -0800 Subject: [PATCH 1/3] feat(core): allow executables to own embedded build identity --- .../2026-11/traj_bjhvd5dhdi87/summary.md | 31 +++++ .../2026-11/traj_bjhvd5dhdi87/trajectory.json | 53 ++++++++ .github/workflows/relay-node-ci.yml | 6 + crates/relay-node/Cargo.toml | 5 + crates/relay-node/README.md | 34 +++++ crates/relay-node/build.rs | 13 +- crates/relay-node/src/build_info.rs | 76 ++++++++++++ crates/relay-node/src/cli.rs | 2 +- crates/relay-node/src/lib.rs | 10 ++ crates/relay-node/src/relay/cloud_http.rs | 2 +- .../relay-node/src/relay/codex/app_server.rs | 2 +- crates/relay-node/src/relay/daemon.rs | 4 +- crates/relay-node/src/relay/integrations.rs | 2 +- crates/relay-node/src/relay/sdk_gaps.rs | 2 +- crates/relay-node/tests/build_metadata.rs | 8 ++ .../relay-node/tests/embedded_build_info.rs | 117 ++++++++++++++++++ .../relay-node/tests/embedded_build_script.rs | 87 +++++++++++++ crates/relay-node/tests/relay_agents.rs | 2 +- crates/relay-node/tests/relay_contract.rs | 2 +- crates/relay-node/tests/test_build_info.rs | 67 ++++++++++ 20 files changed, 513 insertions(+), 12 deletions(-) create mode 100644 .agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/summary.md create mode 100644 .agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/trajectory.json create mode 100644 crates/relay-node/src/build_info.rs create mode 100644 crates/relay-node/tests/embedded_build_info.rs create mode 100644 crates/relay-node/tests/embedded_build_script.rs create mode 100644 crates/relay-node/tests/test_build_info.rs diff --git a/.agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/summary.md b/.agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/summary.md new file mode 100644 index 0000000000..ac63dddc47 --- /dev/null +++ b/.agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/summary.md @@ -0,0 +1,31 @@ +# Trajectory: Keep executable release identity out of compiled messaging libraries + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** November 4, 2026 at 08:49 AM +> **Completed:** November 4, 2026 at 08:53 AM + +--- + +## Summary + +Added library-only embedded identity accessors and immutable startup installation; default constants/stamping preserved. Site/scheme remain compile-time, executable metadata/resource inputs no longer watched. Strict/fixture/build-script tests pass; all integration test targets compile with fixture. Isolated desktop version-only prototype67.342s ->1.149s, only executable rebuilt; production desktop adoption awaits profile/restructure handoff. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Make build identity an executable-owned value +- **Chose:** Make build identity an executable-owned value +- **Reasoning:** A metadata dependency would propagate Cargo fingerprints into large libraries. Optional library-only embedded mode removes version/revision watches and exposes immutable startup initialization; standalone node behavior and compile-time site/scheme stay unchanged. Isolated desktop package split reduced version-only rebuild67.342s to1.149s with both libraries Fresh and executable reporting new baked identity. + +--- + +## Chapters + +### 1. Initial work +*Agent: codex-relay-desktop-speed-up* + +- Make build identity an executable-owned value: Make build identity an executable-owned value diff --git a/.agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/trajectory.json b/.agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/trajectory.json new file mode 100644 index 0000000000..05b2e3c630 --- /dev/null +++ b/.agentworkforce/trajectories/codex-build-identity/completed/2026-11/traj_bjhvd5dhdi87/trajectory.json @@ -0,0 +1,53 @@ +{ + "id": "traj_bjhvd5dhdi87", + "version": 1, + "task": { + "title": "Keep executable release identity out of compiled messaging libraries" + }, + "status": "completed", + "startedAt": "2026-11-04T16:49:54.626Z", + "completedAt": "2026-11-04T16:53:53.135Z", + "agents": [ + { + "name": "codex-relay-desktop-speed-up", + "role": "lead", + "joinedAt": "2026-11-04T16:49:54.728Z" + } + ], + "chapters": [ + { + "id": "chap_or3b4rv9bqch", + "title": "Initial work", + "agentName": "codex-relay-desktop-speed-up", + "startedAt": "2026-11-04T16:49:54.728Z", + "endedAt": "2026-11-04T16:53:53.135Z", + "events": [ + { + "ts": 1793811192903, + "type": "decision", + "content": "Make build identity an executable-owned value: Make build identity an executable-owned value", + "raw": { + "question": "Make build identity an executable-owned value", + "chosen": "Make build identity an executable-owned value", + "alternatives": [], + "reasoning": "A metadata dependency would propagate Cargo fingerprints into large libraries. Optional library-only embedded mode removes version/revision watches and exposes immutable startup initialization; standalone node behavior and compile-time site/scheme stay unchanged. Isolated desktop package split reduced version-only rebuild67.342s to1.149s with both libraries Fresh and executable reporting new baked identity." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Added library-only embedded identity accessors and immutable startup installation; default constants/stamping preserved. Site/scheme remain compile-time, executable metadata/resource inputs no longer watched. Strict/fixture/build-script tests pass; all integration test targets compile with fixture. Isolated desktop version-only prototype67.342s ->1.149s, only executable rebuilt; production desktop adoption awaits profile/restructure handoff.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "ad983d7278bab88b0a7d541494521146b869f78a", + "endRef": "ad983d7278bab88b0a7d541494521146b869f78a" + } +} \ No newline at end of file diff --git a/.github/workflows/relay-node-ci.yml b/.github/workflows/relay-node-ci.yml index bb09d97d6f..c4c094050a 100644 --- a/.github/workflows/relay-node-ci.yml +++ b/.github/workflows/relay-node-ci.yml @@ -131,6 +131,12 @@ jobs: export HOME="$RUNNER_TEMP/relay-node-home" mkdir -p "$HOME" cargo test --locked --no-default-features -- --test-threads=4 + - name: Embedded executable identity + run: cargo test --locked --no-default-features --features embedded-build-info --test embedded_build_info --test embedded_build_script + - name: Explicit downstream test identity fixture + run: | + cargo check --locked --no-default-features --features test-build-info --tests + cargo test --locked --no-default-features --features test-build-info --test test_build_info macos: name: macOS arm64 build + test diff --git a/crates/relay-node/Cargo.toml b/crates/relay-node/Cargo.toml index 8ef1ca9518..dbb521fb1a 100644 --- a/crates/relay-node/Cargo.toml +++ b/crates/relay-node/Cargo.toml @@ -25,6 +25,11 @@ name = "relay_core_binary" required-features = ["production-binaries"] [features] +# Library embedders bake identity into their executable instead of this crate. +# Install BuildInfo before calling the core. Use with default-features = false. +embedded-build-info = [] +# Deterministic identity fixture for downstream dev-dependencies only. +test-build-info = ["embedded-build-info"] # Ordinary builds keep every production executable. The desktop apps' # crates opt out with --no-default-features when they only need the library. default = ["production-binaries", "relay-cli"] diff --git a/crates/relay-node/README.md b/crates/relay-node/README.md index 121e4c5ddd..41a12ae000 100644 --- a/crates/relay-node/README.md +++ b/crates/relay-node/README.md @@ -89,6 +89,40 @@ The internal Cargo package version is never reported as a release on its own. On Windows, `AGENT_RELAY_WINDOWS_ICON` additionally embeds an icon and version resource. +### Executable-owned identity + +An embedding application can enable `embedded-build-info` with +`default-features = false`. In this mode the node library does not read or watch +release version, Git revision, changelog, or Windows resource inputs. It still +validates and bakes in the site and URL scheme. The feature is incompatible with +`production-binaries`, whose standalone executables use the default stamping. + +The executable must install its compiled identity before calling the CLI, +constructing a node, or reading identity: + +```rust,ignore +agent_relay_node::install_build_info(agent_relay_node::BuildInfo { + version: env!("APP_RELEASE_VERSION"), + revision: env!("APP_SOURCE_REVISION"), +}).expect("build identity is installed once at startup"); +``` + +The executable's build script supplies these values at compile time. They are +not read from the environment at startup. Getters `version()` and +`build_revision()` fail if initialization was omitted, and a second install is +rejected. These APIs do not configure the site, scheme, or branding. + +Keep the identity build script and Windows resources in an executable-only +Cargo package. Putting changing metadata in a dependency of the heavy libraries +would still invalidate them. The default node build continues to expose the +existing `VERSION` and `BUILD_REVISION` constants; embedded builds use the getters. + +Downstream test suites can enable `test-build-info` through dev-dependencies +only. It lazily installs a deterministic `0.0.0-dev+test` / `test-fixture` +identity when a test does not install one explicitly. Production dependencies +must use only `embedded-build-info`; verify real-binary startup separately +without the fixture feature so missing initialization still fails closed. + ### The bridge contract The bridge's methods, events and types are declared once in Rust diff --git a/crates/relay-node/build.rs b/crates/relay-node/build.rs index a42cc23e24..770cf0277f 100644 --- a/crates/relay-node/build.rs +++ b/crates/relay-node/build.rs @@ -37,13 +37,20 @@ fn git_path(name: &str) -> Option { } fn main() { + println!("cargo:rerun-if-env-changed=AGENT_RELAY_SITE_URL"); + println!("cargo:rerun-if-env-changed=AGENT_RELAY_URL_SCHEME"); + println!("cargo:rerun-if-changed=build_site.rs"); + if cfg!(feature = "embedded-build-info") { + // Only the executable owns release identity and Windows resources. + // Site and scheme remain compile-time properties of the core. + println!("cargo:rerun-if-changed=build.rs"); + built_in_site(); + return; + } println!("cargo:rerun-if-env-changed=AGENT_RELAY_PROBE_VERSION"); println!("cargo:rerun-if-env-changed=AGENT_RELAY_PROBE_BASE_VERSION"); println!("cargo:rerun-if-env-changed=AGENT_RELAY_PROBE_CHANGELOG"); println!("cargo:rerun-if-env-changed=AGENT_RELAY_PROBE_GIT_DIR"); - println!("cargo:rerun-if-env-changed=AGENT_RELAY_SITE_URL"); - println!("cargo:rerun-if-env-changed=AGENT_RELAY_URL_SCHEME"); - println!("cargo:rerun-if-changed=build_site.rs"); println!("cargo:rerun-if-changed=Cargo.lock"); println!("cargo:rerun-if-changed=build_version.rs"); // Resolve through git so linked worktrees also track their own HEAD. diff --git a/crates/relay-node/src/build_info.rs b/crates/relay-node/src/build_info.rs new file mode 100644 index 0000000000..8c6f5f917b --- /dev/null +++ b/crates/relay-node/src/build_info.rs @@ -0,0 +1,76 @@ +//! Identity supplied by an embedding executable, before it starts the node. +//! +//! Only the executable's version and source revision belong here. Site, +//! scheme and other build-profile values remain checked at compile time. +//! +//! `test-build-info` is an explicit dev-dependency-only fixture: its first +//! identity query installs a deterministic test identity unless an executable +//! already initialized one. Production bootstrap must be tested separately +//! with `embedded-build-info` and without that fixture feature. + +#[cfg(feature = "embedded-build-info")] +use std::sync::OnceLock; + +/// The embedding executable's compiled identity. Static strings keep the +/// identity immutable for the process without reading runtime configuration. +#[cfg(feature = "embedded-build-info")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BuildInfo { + pub version: &'static str, + pub revision: &'static str, +} + +#[cfg(feature = "embedded-build-info")] +static BUILD_INFO: OnceLock = OnceLock::new(); + +/// Install the executable's identity before calling the CLI or constructing +/// a node. The first caller wins; a later call returns its rejected identity, +/// including when it supplies the same strings as the first call. +#[cfg(feature = "embedded-build-info")] +pub fn install_build_info(info: BuildInfo) -> Result<(), BuildInfo> { + BUILD_INFO.set(info) +} + +#[cfg(feature = "embedded-build-info")] +fn installed() -> &'static BuildInfo { + #[cfg(feature = "test-build-info")] + { + BUILD_INFO.get_or_init(|| BuildInfo { + version: "0.0.0-dev+test", + revision: "test-fixture", + }) + } + #[cfg(not(feature = "test-build-info"))] + { + BUILD_INFO + .get() + .expect("embedded build identity must be installed before use") + } +} + +/// The release version, or Git-identified development version, of the node's +/// executable. Embedded builds require `install_build_info` before access, +/// except when the explicit `test-build-info` fixture feature is enabled. +pub fn version() -> &'static str { + #[cfg(feature = "embedded-build-info")] + { + installed().version + } + #[cfg(not(feature = "embedded-build-info"))] + { + crate::VERSION + } +} + +/// The source revision of the node's executable. Embedded builds require +/// `install_build_info` before access, except with the `test-build-info` fixture. +pub fn build_revision() -> &'static str { + #[cfg(feature = "embedded-build-info")] + { + installed().revision + } + #[cfg(not(feature = "embedded-build-info"))] + { + crate::BUILD_REVISION + } +} diff --git a/crates/relay-node/src/cli.rs b/crates/relay-node/src/cli.rs index 4e95786b3b..2ee4991a25 100644 --- a/crates/relay-node/src/cli.rs +++ b/crates/relay-node/src/cli.rs @@ -13,7 +13,7 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command( name = "agent-relay-node", - version = crate::VERSION, + version = crate::version(), propagate_version = true, about = "Connect coding sessions to Agent Relay" )] diff --git a/crates/relay-node/src/lib.rs b/crates/relay-node/src/lib.rs index 4b681b2806..ddc08f06fa 100644 --- a/crates/relay-node/src/lib.rs +++ b/crates/relay-node/src/lib.rs @@ -1,10 +1,20 @@ //! The Agent Relay core: the messaging runtime (`relay`) and Relay Connect //! rooms. `agent-relay-node` (src/cli) is its binary. +#[cfg(all(feature = "embedded-build-info", feature = "production-binaries"))] +compile_error!("embedded-build-info is library-only; disable default features and initialize identity from the embedding executable"); + /// The stamped release (AGENT_RELAY_PROBE_VERSION), or a Git-identified development build. The internal /// Cargo package version is never presented as a separate product release. +#[cfg(not(feature = "embedded-build-info"))] pub const VERSION: &str = env!("RELAY_DESKTOP_VERSION"); +#[cfg(not(feature = "embedded-build-info"))] pub const BUILD_REVISION: &str = env!("RELAY_DESKTOP_REVISION"); +mod build_info; +pub use build_info::{build_revision, version}; +#[cfg(feature = "embedded-build-info")] +pub use build_info::{install_build_info, BuildInfo}; + /// The build-time check of `AGENT_RELAY_SITE_URL` and `AGENT_RELAY_URL_SCHEME` /// (`build.rs`), tested here. #[cfg(test)] diff --git a/crates/relay-node/src/relay/cloud_http.rs b/crates/relay-node/src/relay/cloud_http.rs index 41e713f32f..bbafd264ca 100644 --- a/crates/relay-node/src/relay/cloud_http.rs +++ b/crates/relay-node/src/relay/cloud_http.rs @@ -145,7 +145,7 @@ pub fn send_raw_blocking( .set("Accept", "application/json") .set( "User-Agent", - &format!("AgentRelayDesktop/{}", crate::VERSION), + &format!("AgentRelayDesktop/{}", crate::version()), ); if let Some(bearer) = bearer { request = request.set("Authorization", &format!("Bearer {bearer}")); diff --git a/crates/relay-node/src/relay/codex/app_server.rs b/crates/relay-node/src/relay/codex/app_server.rs index c1692d9a53..98b119c06c 100644 --- a/crates/relay-node/src/relay/codex/app_server.rs +++ b/crates/relay-node/src/relay/codex/app_server.rs @@ -88,7 +88,7 @@ impl<'a> Client<'a> { } client.send( &json!({"method": "initialize", "id": 1, "params": {"clientInfo": { - "name": "agent-relay-desktop", "title": "Agent Relay", "version": crate::VERSION, + "name": "agent-relay-desktop", "title": "Agent Relay", "version": crate::version(), }}}), deadline, )?; diff --git a/crates/relay-node/src/relay/daemon.rs b/crates/relay-node/src/relay/daemon.rs index 30f33f6ccd..efaf2933e4 100644 --- a/crates/relay-node/src/relay/daemon.rs +++ b/crates/relay-node/src/relay/daemon.rs @@ -127,7 +127,7 @@ impl Config { // Read before `env` moves into the config. delivery_timing: DeliveryTiming::from_env(&lookup), env, - version: crate::VERSION.to_string(), + version: crate::version().to_string(), host_name: machine::host_name(), rediscover_every: Duration::from_secs(30), push_timing: PushTiming::default(), @@ -227,7 +227,7 @@ impl Core { let machine_prefix = machine::machine_prefix(&config.host_name); let hub = Hub::new( DaemonInfo { - build_revision: Some(crate::BUILD_REVISION.into()), + build_revision: Some(crate::build_revision().into()), history_version: plugins.history_version.map(Into::into), version: config.version.clone(), bridge_version: BRIDGE_VERSION, diff --git a/crates/relay-node/src/relay/integrations.rs b/crates/relay-node/src/relay/integrations.rs index 94a58ddd94..5fc86d058a 100644 --- a/crates/relay-node/src/relay/integrations.rs +++ b/crates/relay-node/src/relay/integrations.rs @@ -102,7 +102,7 @@ impl Relayfile { .set("Authorization", &format!("Bearer {}", token.expose())) .set( "User-Agent", - &format!("AgentRelayDesktop/{}", crate::VERSION), + &format!("AgentRelayDesktop/{}", crate::version()), ); let response = match body { Some(body) => request.send_json(body), diff --git a/crates/relay-node/src/relay/sdk_gaps.rs b/crates/relay-node/src/relay/sdk_gaps.rs index 3d1bfdfe1f..2890aca125 100644 --- a/crates/relay-node/src/relay/sdk_gaps.rs +++ b/crates/relay-node/src/relay/sdk_gaps.rs @@ -69,7 +69,7 @@ pub fn http(base_url: &str, credential: &Secret) -> relaycast::Result { + assert!(std::panic::catch_unwind(version).is_err()); + assert!(std::panic::catch_unwind(build_revision).is_err()); + } + "installed" => { + install_build_info(IDENTITY).unwrap(); + assert_eq!(version(), IDENTITY.version); + assert_eq!(build_revision(), IDENTITY.revision); + assert_eq!(install_build_info(IDENTITY), Err(IDENTITY)); + let replacement = BuildInfo { + version: "9999.1.1", + revision: "other", + }; + assert_eq!(install_build_info(replacement), Err(replacement)); + assert_eq!(version(), IDENTITY.version); + assert_eq!(build_revision(), IDENTITY.revision); + } + "concurrent" => { + let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); + let threads: Vec<_> = (0..8) + .map(|index| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + let identity = if index % 2 == 0 { + IDENTITY + } else { + BuildInfo { + version: "2026.11.4-beta.3", + revision: "012345abcdef", + } + }; + barrier.wait(); + let result = install_build_info(identity); + (identity, result, (version(), build_revision())) + }) + }) + .collect(); + let outcomes: Vec<_> = threads + .into_iter() + .map(|thread| thread.join().unwrap()) + .collect(); + let winners: Vec<_> = outcomes + .iter() + .filter(|(_, result, _)| result.is_ok()) + .collect(); + assert_eq!(winners.len(), 1); + let winner = winners[0].0; + for (_, _, observed) in outcomes { + assert_eq!(observed, (winner.version, winner.revision)); + } + assert_eq!(version(), winner.version); + assert_eq!(build_revision(), winner.revision); + } + "daemon" => daemon_reports_executable_identity(), + _ => panic!("unexpected scenario"), + } +} + +fn daemon_reports_executable_identity() { + use agent_relay_node::relay::daemon::{Config, Core}; + use agent_relay_node::relay::paths::CorePaths; + use std::collections::BTreeMap; + use std::time::Duration; + + install_build_info(IDENTITY).unwrap(); + let home = tempfile::tempdir().unwrap(); + let config = Config { + paths: CorePaths::for_socket(home.path().join("run/core.sock")).unwrap(), + home: home.path().to_path_buf(), + app_data_dir: home.path().join("app-data"), + env: BTreeMap::new(), + version: version().to_owned(), + host_name: "embedded build fixture".into(), + rediscover_every: Duration::from_secs(3600), + push_timing: Default::default(), + delivery_timing: Default::default(), + }; + let (core, _) = Core::new(config); + let daemon = core.status().daemon; + assert_eq!(daemon.version, IDENTITY.version); + assert_eq!(daemon.build_revision.as_deref(), Some(IDENTITY.revision)); +} diff --git a/crates/relay-node/tests/embedded_build_script.rs b/crates/relay-node/tests/embedded_build_script.rs new file mode 100644 index 0000000000..f5568a1c78 --- /dev/null +++ b/crates/relay-node/tests/embedded_build_script.rs @@ -0,0 +1,87 @@ +//! Release identity changes must not become inputs to the embedded library. +use std::{path::Path, process::Command}; + +fn run(script: &Path, directory: &Path, site: &str, version: &str) -> std::process::Output { + Command::new(script) + .current_dir(directory) + .env("CARGO_CFG_TARGET_OS", "windows") + .env("AGENT_RELAY_PROBE_VERSION", version) + .env("AGENT_RELAY_PROBE_GIT_DIR", directory.join("missing-git")) + .env( + "AGENT_RELAY_PROBE_CHANGELOG", + directory.join("missing-changelog"), + ) + .env( + "AGENT_RELAY_WINDOWS_ICON", + directory.join("missing-icon.ico"), + ) + .env("AGENT_RELAY_SITE_URL", site) + .env("AGENT_RELAY_URL_SCHEME", "relay-test") + .output() + .unwrap() +} + +#[test] +fn embedded_build_watches_profile_but_not_release_identity() { + let directory = tempfile::tempdir().unwrap(); + let executable = directory + .path() + .join(format!("build-script{}", std::env::consts::EXE_SUFFIX)); + let compiled = Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())) + .arg("--edition=2021") + .args(["--cfg", "feature=\"embedded-build-info\""]) + .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("build.rs")) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + compiled.status.success(), + "{}", + String::from_utf8_lossy(&compiled.stderr) + ); + let first = run( + &executable, + directory.path(), + "https://example.com", + "2026.11.4", + ); + assert!( + first.status.success(), + "{}", + String::from_utf8_lossy(&first.stderr) + ); + let second = run( + &executable, + directory.path(), + "https://example.com", + "2026.11.5-beta.1", + ); + assert!(second.status.success()); + assert_eq!(first.stdout, second.stdout); + let output = String::from_utf8(first.stdout).unwrap(); + assert!(output.contains("cargo:rustc-env=AGENT_RELAY_SITE_URL=https://example.com")); + assert!(output.contains("cargo:rustc-env=AGENT_RELAY_URL_SCHEME=relay-test")); + for forbidden in [ + "AGENT_RELAY_PROBE_", + "AGENT_RELAY_WINDOWS_ICON", + "RELAY_DESKTOP_VERSION", + "RELAY_DESKTOP_REVISION", + "missing-", + ] { + assert!( + !output.contains(forbidden), + "Unexpected identity input: {output}" + ); + } + let invalid_site = run( + &executable, + directory.path(), + "https://example.com/not-an-origin", + "2026.11.4", + ); + assert!( + !invalid_site.status.success(), + "Embedded mode must retain site validation" + ); +} diff --git a/crates/relay-node/tests/relay_agents.rs b/crates/relay-node/tests/relay_agents.rs index 7e6a869396..9179f44527 100644 --- a/crates/relay-node/tests/relay_agents.rs +++ b/crates/relay-node/tests/relay_agents.rs @@ -3854,7 +3854,7 @@ async fn agents_name_their_owner_and_keep_it_when_another_person_attaches() { registered.origin, ( Some("relay-desktop".to_string()), - Some(agent_relay_node::VERSION.to_string()) + Some(agent_relay_node::version().to_string()) ) ); diff --git a/crates/relay-node/tests/relay_contract.rs b/crates/relay-node/tests/relay_contract.rs index 3cd95882ae..c2873d7700 100644 --- a/crates/relay-node/tests/relay_contract.rs +++ b/crates/relay-node/tests/relay_contract.rs @@ -86,7 +86,7 @@ async fn every_registered_method_reaches_a_handler() { ); assert_eq!( status["result"]["daemon"]["build_revision"], - agent_relay_node::BUILD_REVISION + agent_relay_node::build_revision() ); let listed: Vec = serde_json::from_value(hello["result"]["methods"].clone()).unwrap(); let registered: Vec = METHODS.iter().map(|m| m.name.to_string()).collect(); diff --git a/crates/relay-node/tests/test_build_info.rs b/crates/relay-node/tests/test_build_info.rs new file mode 100644 index 0000000000..7baaa4e1ed --- /dev/null +++ b/crates/relay-node/tests/test_build_info.rs @@ -0,0 +1,67 @@ +#![cfg(feature = "test-build-info")] + +use agent_relay_node::{build_revision, install_build_info, version, BuildInfo}; +use std::process::Command; + +const EXECUTABLE: BuildInfo = BuildInfo { + version: "2026.11.4-beta.2", + revision: "abcdef012345", +}; + +#[test] +fn explicit_fixture_scenarios() { + for scenario in ["parallel", "immutable", "explicit-first"] { + // A fresh process isolates the process-lifetime OnceLock. Conflicting + // runtime environment values prove the fixture never consults them. + let result = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "fixture_identity_child", "--nocapture"]) + .env("RELAY_TEST_BUILD_INFO_SCENARIO", scenario) + .env("AGENT_RELAY_PROBE_VERSION", "9999.99.99") + .env("RELAY_DESKTOP_REVISION", "runtime-value-must-not-be-read") + .output() + .unwrap(); + assert!( + result.status.success(), + "{scenario}: {}{}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + } +} + +#[test] +fn fixture_identity_child() { + let Ok(scenario) = std::env::var("RELAY_TEST_BUILD_INFO_SCENARIO") else { + return; + }; + match scenario.as_str() { + "parallel" => { + let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); + let readers: Vec<_> = (0..8) + .map(|_| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + barrier.wait(); + (version(), build_revision()) + }) + }) + .collect(); + for reader in readers { + assert_eq!(reader.join().unwrap(), ("0.0.0-dev+test", "test-fixture")); + } + } + "immutable" => { + assert_eq!(version(), "0.0.0-dev+test"); + assert_eq!(install_build_info(EXECUTABLE), Err(EXECUTABLE)); + assert_eq!(version(), "0.0.0-dev+test"); + assert_eq!(build_revision(), "test-fixture"); + } + "explicit-first" => { + install_build_info(EXECUTABLE).unwrap(); + assert_eq!(version(), EXECUTABLE.version); + assert_eq!(build_revision(), EXECUTABLE.revision); + assert_eq!(install_build_info(EXECUTABLE), Err(EXECUTABLE)); + } + _ => panic!("unexpected scenario"), + } +} From cea0fc08215859b287c6d63ff1827f09d7e461da Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Thu, 5 Nov 2026 11:05:54 -0800 Subject: [PATCH 2/3] Cover executable identity against the current release core --- CHANGELOG.md | 6 +++++- crates/relay-node/build.rs | 2 +- crates/relay-node/tests/embedded_build_info.rs | 2 +- crates/relay-node/tests/embedded_build_script.rs | 8 ++++++-- crates/relay-node/tests/test_build_info.rs | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d9257c64..4472db9b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to Agent Relay will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased - Minor] + +### Added + +- Applications embedding `agent-relay-node` can install their compiled version and revision at startup, so changing release identity only rebuilds their executable. ## [17.0.0] - 2026-11-05 diff --git a/crates/relay-node/build.rs b/crates/relay-node/build.rs index 770cf0277f..61fcd671f0 100644 --- a/crates/relay-node/build.rs +++ b/crates/relay-node/build.rs @@ -40,7 +40,7 @@ fn main() { println!("cargo:rerun-if-env-changed=AGENT_RELAY_SITE_URL"); println!("cargo:rerun-if-env-changed=AGENT_RELAY_URL_SCHEME"); println!("cargo:rerun-if-changed=build_site.rs"); - if cfg!(feature = "embedded-build-info") { + if env::var_os("CARGO_FEATURE_EMBEDDED_BUILD_INFO").is_some() { // Only the executable owns release identity and Windows resources. // Site and scheme remain compile-time properties of the core. println!("cargo:rerun-if-changed=build.rs"); diff --git a/crates/relay-node/tests/embedded_build_info.rs b/crates/relay-node/tests/embedded_build_info.rs index 7e402dbf7d..fce9fe8d83 100644 --- a/crates/relay-node/tests/embedded_build_info.rs +++ b/crates/relay-node/tests/embedded_build_info.rs @@ -19,7 +19,7 @@ fn embedded_identity_scenarios() { .output() .unwrap(); assert!( - result.status.success(), + result.status.success() && String::from_utf8_lossy(&result.stdout).contains("1 passed"), "{scenario}: {}{}", String::from_utf8_lossy(&result.stdout), String::from_utf8_lossy(&result.stderr) diff --git a/crates/relay-node/tests/embedded_build_script.rs b/crates/relay-node/tests/embedded_build_script.rs index f5568a1c78..37a8050197 100644 --- a/crates/relay-node/tests/embedded_build_script.rs +++ b/crates/relay-node/tests/embedded_build_script.rs @@ -4,6 +4,7 @@ use std::{path::Path, process::Command}; fn run(script: &Path, directory: &Path, site: &str, version: &str) -> std::process::Output { Command::new(script) .current_dir(directory) + .env("CARGO_FEATURE_EMBEDDED_BUILD_INFO", "1") .env("CARGO_CFG_TARGET_OS", "windows") .env("AGENT_RELAY_PROBE_VERSION", version) .env("AGENT_RELAY_PROBE_GIT_DIR", directory.join("missing-git")) @@ -29,7 +30,6 @@ fn embedded_build_watches_profile_but_not_release_identity() { .join(format!("build-script{}", std::env::consts::EXE_SUFFIX)); let compiled = Command::new(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())) .arg("--edition=2021") - .args(["--cfg", "feature=\"embedded-build-info\""]) .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("build.rs")) .arg("-o") .arg(&executable) @@ -57,7 +57,11 @@ fn embedded_build_watches_profile_but_not_release_identity() { "https://example.com", "2026.11.5-beta.1", ); - assert!(second.status.success()); + assert!( + second.status.success(), + "{}", + String::from_utf8_lossy(&second.stderr) + ); assert_eq!(first.stdout, second.stdout); let output = String::from_utf8(first.stdout).unwrap(); assert!(output.contains("cargo:rustc-env=AGENT_RELAY_SITE_URL=https://example.com")); diff --git a/crates/relay-node/tests/test_build_info.rs b/crates/relay-node/tests/test_build_info.rs index 7baaa4e1ed..257dbe3c5f 100644 --- a/crates/relay-node/tests/test_build_info.rs +++ b/crates/relay-node/tests/test_build_info.rs @@ -21,7 +21,7 @@ fn explicit_fixture_scenarios() { .output() .unwrap(); assert!( - result.status.success(), + result.status.success() && String::from_utf8_lossy(&result.stdout).contains("1 passed"), "{scenario}: {}{}", String::from_utf8_lossy(&result.stdout), String::from_utf8_lossy(&result.stderr) From 11da9e287ccdbfc4b413d80fc3ab651b6fef9a30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Nov 2026 19:09:32 +0000 Subject: [PATCH 3/3] style: auto-format with Prettier --- .../skills/choosing-swarm-patterns/SKILL.md | 48 ++++++++++--------- .../creating-claude-rules-skill/SKILL.md | 3 +- AGENTS.md | 5 +- flows/verify/fleet-daytona.spec.ts | 16 +++---- packages/cli/src/cli/agent-relay-mcp.ts | 3 +- packages/cli/src/cli/commands/cloud.ts | 3 +- packages/cli/src/cli/commands/core.test.ts | 6 +-- .../commands/integration-cleanup-journal.ts | 5 +- .../src/cli/commands/integration-recipient.ts | 3 +- packages/cli/src/cli/commands/local-agent.ts | 3 +- .../cli/src/cli/lib/attach-passthrough.ts | 3 +- .../cli/src/cli/lib/deprecate-command.test.ts | 36 +++++++++++--- packages/cli/src/cli/telemetry/events.ts | 6 +-- packages/cloud/src/connect.ts | 3 +- packages/cloud/src/device-auth.ts | 7 +-- packages/cloud/src/fleet-sandbox.ts | 15 ++---- packages/config/src/shadow-config.ts | 6 +-- packages/fleet/README.md | 8 +--- packages/harness-driver/src/harness.ts | 4 +- packages/harness-driver/src/protocol.ts | 7 +-- packages/harnesses/src/ai-sdk/harness-host.ts | 10 +--- packages/sdk/src/actions/json-schema-lite.ts | 3 +- packages/sdk/src/actions/types.ts | 3 +- packages/sdk/src/listeners.ts | 6 +-- .../sdk/src/messaging/relaycast-placement.ts | 5 +- packages/sdk/src/messaging/types.ts | 9 +--- packages/sdk/src/session/types.ts | 10 +--- packages/session/src/client.test.ts | 32 ++++++------- packages/session/src/types.ts | 3 +- plans/001-adopt-ai-sdk-harnesses.md | 3 +- .../verify-features/escalation-status.d.mts | 7 +-- scripts/verify-features/targeted-pr-plan.mjs | 2 +- tests/e2e/github-subscriptions/proof.mjs | 10 ++-- .../evals/scenarios/s05-phrasing-variants.ts | 7 +-- 34 files changed, 118 insertions(+), 182 deletions(-) diff --git a/.agents/skills/choosing-swarm-patterns/SKILL.md b/.agents/skills/choosing-swarm-patterns/SKILL.md index 6431ac0e3c..4d066e93e6 100644 --- a/.agents/skills/choosing-swarm-patterns/SKILL.md +++ b/.agents/skills/choosing-swarm-patterns/SKILL.md @@ -66,22 +66,23 @@ Is cost the primary concern? These 14 additional patterns exist in `SwarmPattern` (types.ts:114-139). The coordinator has role-based auto-selection heuristics (`coordinator.ts:51-165`), but they only fire when `swarm.pattern` is **omitted** — YAML validation requires it (`runner.ts:2105-2117`), so auto-selection is effectively a programmatic-API feature. In YAML, set `swarm.pattern` explicitly. Topology is still resolved per-pattern once selected; the "Triggering roles" column reflects what the coordinator looks for to shape edges (per `coordinator.ts:250-450`): -| Pattern | Roles the topology keys off | Topology | + +| Pattern | Roles the topology keys off | Topology | | ----------------- | ------------------------------------------------------- | ---------------------------------------------- | -| `map-reduce` | `mapper` + `reducer` | coordinator → mappers → reducers → coordinator | -| `scatter-gather` | — | hub → workers → hub | -| `supervisor` | `supervisor` | supervisor ↔ workers | -| `reflection` | `critic` or `reviewer` (auto-select uses `critic` only) | producers → critic → producers (loop) | -| `red-team` | `attacker`/`red-team` + `defender`/`blue-team` | adversarial mesh with optional judges | -| `verifier` | `verifier` | producers → verifiers → back to producers | -| `auction` | `auctioneer` | auctioneer → bidders → auctioneer | -| `escalation` | `tier-*` | tiered chain, escalate up / report down | -| `saga` | `saga-orchestrator`, `compensate-handler` | orchestrator ↔ participants | -| `circuit-breaker` | `primary` + `fallback`/`backup` | try primary, fallback on failure | -| `blackboard` | `blackboard` / `shared-workspace` | shared state hub | -| `swarm` | `hive-mind` / `swarm-agent` | stigmergy-style | -| `competitive` | — (declared explicitly) | independent parallel implementations + judge | -| `review-loop` | `implement*` + 2+ `reviewer*` | implementer ↔ reviewers | +| `map-reduce` | `mapper` + `reducer` | coordinator → mappers → reducers → coordinator | +| `scatter-gather` | — | hub → workers → hub | +| `supervisor` | `supervisor` | supervisor ↔ workers | +| `reflection` | `critic` or `reviewer` (auto-select uses `critic` only) | producers → critic → producers (loop) | +| `red-team` | `attacker`/`red-team` + `defender`/`blue-team` | adversarial mesh with optional judges | +| `verifier` | `verifier` | producers → verifiers → back to producers | +| `auction` | `auctioneer` | auctioneer → bidders → auctioneer | +| `escalation` | `tier-*` | tiered chain, escalate up / report down | +| `saga` | `saga-orchestrator`, `compensate-handler` | orchestrator ↔ participants | +| `circuit-breaker` | `primary` + `fallback`/`backup` | try primary, fallback on failure | +| `blackboard` | `blackboard` / `shared-workspace` | shared state hub | +| `swarm` | `hive-mind` / `swarm-agent` | stigmergy-style | +| `competitive` | — (declared explicitly) | independent parallel implementations + judge | +| `review-loop` | `implement*` + 2+ `reviewer*` | implementer ↔ reviewers | ### Structured Squad Review Loop @@ -303,15 +304,16 @@ flat names. In a client that decorates MCP tools, the prefix comes from the configured server key. With the relay broker's `agent-relay` server key, Claude Code users commonly see `mcp__agent-relay__send_dm`; Codex and opencode users see the bare canonical name `send_dm`. -| Purpose | Canonical tool | Claude Code form with `agent-relay` key | + +| Purpose | Canonical tool | Claude Code form with `agent-relay` key | | ------------------------ | ----------------- | --------------------------------------- | -| Send DM to another agent | `send_dm` | `mcp__agent-relay__send_dm` | -| Check inbox | `check_inbox` | `mcp__agent-relay__check_inbox` | -| List agents | `list_agents` | `mcp__agent-relay__list_agents` | -| Post to a channel | `post_message` | `mcp__agent-relay__post_message` | -| Reply in a thread | `reply_to_thread` | `mcp__agent-relay__reply_to_thread` | -| Spawn sub-agent | `add_agent` | `mcp__agent-relay__add_agent` | -| Remove sub-agent | `remove_agent` | `mcp__agent-relay__remove_agent` | +| Send DM to another agent | `send_dm` | `mcp__agent-relay__send_dm` | +| Check inbox | `check_inbox` | `mcp__agent-relay__check_inbox` | +| List agents | `list_agents` | `mcp__agent-relay__list_agents` | +| Post to a channel | `post_message` | `mcp__agent-relay__post_message` | +| Reply in a thread | `reply_to_thread` | `mcp__agent-relay__reply_to_thread` | +| Spawn sub-agent | `add_agent` | `mcp__agent-relay__add_agent` | +| Remove sub-agent | `remove_agent` | `mcp__agent-relay__remove_agent` | > `interactive: false` agents run as non-interactive subprocesses with no relay connection. They must not call Relay MCP tools. diff --git a/.claude/skills/creating-claude-rules-skill/SKILL.md b/.claude/skills/creating-claude-rules-skill/SKILL.md index ed60b24c2b..aeaea10915 100644 --- a/.claude/skills/creating-claude-rules-skill/SKILL.md +++ b/.claude/skills/creating-claude-rules-skill/SKILL.md @@ -84,11 +84,10 @@ alwaysApply: true paths: - **/*.ts --- - # ✅ CORRECT - quote glob patterns --- paths: - - "**/*.ts" + - '**/*.ts' --- ``` diff --git a/AGENTS.md b/AGENTS.md index 032295138b..288f4c2668 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,8 +93,9 @@ Publishing (npm, crates, GitHub releases) is gated on chief green-light. delete chief's orgchart env-unset workaround when fixed), #1383 (non-Error rejections render as `[object Object]`). - Also pending: 64 dependabot alerts on main (1 critical); skills repo -relay-team/relay-pipeline/relay-fanout SKILL.mds still instruct printing -raw observer URLs — unsatisfiable once #1380 lands. + relay-team/relay-pipeline/relay-fanout SKILL.mds still instruct printing + raw observer URLs — unsatisfiable once #1380 lands. + diff --git a/flows/verify/fleet-daytona.spec.ts b/flows/verify/fleet-daytona.spec.ts index 4999562b49..5914be723e 100644 --- a/flows/verify/fleet-daytona.spec.ts +++ b/flows/verify/fleet-daytona.spec.ts @@ -552,15 +552,13 @@ node ${shellQuote(path.join(TRUSTED_ROOT, 'scripts/verify-features/materialize-f { workflows: [ { - steps: steps.map( - (step): RelayFlowTimeoutStep => ({ - name: step.id, - timeoutMs: planTimeouts.get(step.id) as number, - retries: planRetries.get(step.id) ?? 0, - ...(step.agent ? { agent: step.agent as string } : {}), - dependsOn: (step.dependsOn as string[] | undefined) ?? [], - }) - ), + steps: steps.map((step): RelayFlowTimeoutStep => ({ + name: step.id, + timeoutMs: planTimeouts.get(step.id) as number, + retries: planRetries.get(step.id) ?? 0, + ...(step.agent ? { agent: step.agent as string } : {}), + dependsOn: (step.dependsOn as string[] | undefined) ?? [], + })), }, ], errorHandling: { maxRetries: 0 }, diff --git a/packages/cli/src/cli/agent-relay-mcp.ts b/packages/cli/src/cli/agent-relay-mcp.ts index 91cf269015..6bb50e8efb 100644 --- a/packages/cli/src/cli/agent-relay-mcp.ts +++ b/packages/cli/src/cli/agent-relay-mcp.ts @@ -1028,8 +1028,7 @@ async function verifyMetadataLanded( } const record = agents.find((agent) => (agent as { name?: string } | null)?.name === name) as - | { metadata?: Record } - | undefined; + { metadata?: Record } | undefined; if (!record) { return { diff --git a/packages/cli/src/cli/commands/cloud.ts b/packages/cli/src/cli/commands/cloud.ts index 7070708ed3..8a9cca06ad 100644 --- a/packages/cli/src/cli/commands/cloud.ts +++ b/packages/cli/src/cli/commands/cloud.ts @@ -824,8 +824,7 @@ export function registerCloudCommands(program: Command, overrides: Partial null)) as - | (WhoAmIResponse & { error?: string }) - | null; + (WhoAmIResponse & { error?: string }) | null; if (!response.ok || !payload?.authenticated) { throw new Error(payload?.error || 'Failed to resolve auth status'); diff --git a/packages/cli/src/cli/commands/core.test.ts b/packages/cli/src/cli/commands/core.test.ts index eecaf90f7f..d4ca722944 100644 --- a/packages/cli/src/cli/commands/core.test.ts +++ b/packages/cli/src/cli/commands/core.test.ts @@ -2320,8 +2320,7 @@ describe('registerCoreCommands', () => { const onSignalMock = deps.onSignal as unknown as { mock: { calls: unknown[][] } }; const sigintHandler = onSignalMock.mock.calls.find((call) => call[0] === 'SIGINT')?.[1] as - | (() => Promise) - | undefined; + (() => Promise) | undefined; expect(sigintHandler).toBeDefined(); const sigint = sigintHandler as () => Promise; @@ -2368,8 +2367,7 @@ describe('registerCoreCommands', () => { const onSignalMock = deps.onSignal as unknown as { mock: { calls: unknown[][] } }; const sigtermHandler = onSignalMock.mock.calls.find((call) => call[0] === 'SIGTERM')?.[1] as - | (() => Promise) - | undefined; + (() => Promise) | undefined; expect(sigtermHandler).toBeDefined(); await expect((sigtermHandler as () => Promise)()).rejects.toMatchObject({ code: 0 }); diff --git a/packages/cli/src/cli/commands/integration-cleanup-journal.ts b/packages/cli/src/cli/commands/integration-cleanup-journal.ts index e3541254d7..610547aaf7 100644 --- a/packages/cli/src/cli/commands/integration-cleanup-journal.ts +++ b/packages/cli/src/cli/commands/integration-cleanup-journal.ts @@ -40,10 +40,7 @@ import { formatBrokerNotFoundError, getBrokerBinaryPath } from '@agent-relay/har */ export type PendingCleanupKind = - | 'relayfile-webhook-subscription' - | 'relay-webhook' - | 'relay-subscription' - | 'subscribe-attempt'; + 'relayfile-webhook-subscription' | 'relay-webhook' | 'relay-subscription' | 'subscribe-attempt'; export interface PendingCleanupOwner { pid: number; diff --git a/packages/cli/src/cli/commands/integration-recipient.ts b/packages/cli/src/cli/commands/integration-recipient.ts index 20131ae7fc..e3c4727527 100644 --- a/packages/cli/src/cli/commands/integration-recipient.ts +++ b/packages/cli/src/cli/commands/integration-recipient.ts @@ -157,8 +157,7 @@ export async function resolveSubscriptionAgentChannel( ); if (!response.ok) { const detail = (await response.json().catch(() => undefined)) as - | { error?: { code?: unknown } } - | undefined; + { error?: { code?: unknown } } | undefined; const code = typeof detail?.error?.code === 'string' && /^[a-zA-Z0-9_-]{1,100}$/.test(detail.error.code) ? ` ${detail.error.code}` diff --git a/packages/cli/src/cli/commands/local-agent.ts b/packages/cli/src/cli/commands/local-agent.ts index 97952a290b..31dff6010a 100644 --- a/packages/cli/src/cli/commands/local-agent.ts +++ b/packages/cli/src/cli/commands/local-agent.ts @@ -261,8 +261,7 @@ function withDefaults(overrides: Partial = {}): LocalAge } type AttachCredentialSelection = - | { ok: true; workspaceKey?: string; joinTicket?: string } - | { ok: false; error: string }; + { ok: true; workspaceKey?: string; joinTicket?: string } | { ok: false; error: string }; function resolveAttachCredentialSelection( rawWorkspaceKey: string | undefined, diff --git a/packages/cli/src/cli/lib/attach-passthrough.ts b/packages/cli/src/cli/lib/attach-passthrough.ts index 37ee0bbe7a..54dc188bb8 100644 --- a/packages/cli/src/cli/lib/attach-passthrough.ts +++ b/packages/cli/src/cli/lib/attach-passthrough.ts @@ -233,8 +233,7 @@ function isStringObject(value: unknown): value is Record { * queue in passthrough session, so those events (which the broker doesn't * emit while the worker is in `auto_inject`) would be `other`. */ export type PassthroughWsEvent = - | { kind: 'worker_stream'; chunk: string; offset?: number } - | { kind: 'other' }; + { kind: 'worker_stream'; chunk: string; offset?: number } | { kind: 'other' }; /** * Inspect a single WebSocket frame and classify it relative to the diff --git a/packages/cli/src/cli/lib/deprecate-command.test.ts b/packages/cli/src/cli/lib/deprecate-command.test.ts index 10cd7b8e11..a3a8aae7ab 100644 --- a/packages/cli/src/cli/lib/deprecate-command.test.ts +++ b/packages/cli/src/cli/lib/deprecate-command.test.ts @@ -94,7 +94,11 @@ describe('deprecateCommand', () => { it('still runs the original action', async () => { const { program, cloud, ran } = programWithCloud(); - deprecateCommand(cloud.commands.find((c) => c.name() === 'run')!, NOTICE, { warn: () => {} }); + deprecateCommand( + cloud.commands.find((c) => c.name() === 'run')!, + NOTICE, + { warn: () => {} } + ); await program.parseAsync(['cloud', 'run', 'flow.yaml'], { from: 'user' }); @@ -104,7 +108,11 @@ describe('deprecateCommand', () => { it('warns once, with the full invocation path', async () => { const { program, cloud } = programWithCloud(); const warn = vi.fn(); - deprecateCommand(cloud.commands.find((c) => c.name() === 'run')!, NOTICE, { warn }); + deprecateCommand( + cloud.commands.find((c) => c.name() === 'run')!, + NOTICE, + { warn } + ); await program.parseAsync(['cloud', 'run', 'flow.yaml'], { from: 'user' }); @@ -114,7 +122,11 @@ describe('deprecateCommand', () => { it('hides the command from the parent help but leaves its siblings', () => { const { cloud } = programWithCloud(); - deprecateCommand(cloud.commands.find((c) => c.name() === 'run')!, NOTICE, { warn: () => {} }); + deprecateCommand( + cloud.commands.find((c) => c.name() === 'run')!, + NOTICE, + { warn: () => {} } + ); const help = cloud.helpInformation(); expect(help).not.toMatch(/^\s+run\b/m); @@ -126,8 +138,16 @@ describe('deprecateCommand', () => { // would let the second call replace the first and un-hide `run`. const { cloud } = programWithCloud(); const warn = () => {}; - deprecateCommand(cloud.commands.find((c) => c.name() === 'run')!, NOTICE, { warn }); - deprecateCommand(cloud.commands.find((c) => c.name() === 'schedule')!, NOTICE, { warn }); + deprecateCommand( + cloud.commands.find((c) => c.name() === 'run')!, + NOTICE, + { warn } + ); + deprecateCommand( + cloud.commands.find((c) => c.name() === 'schedule')!, + NOTICE, + { warn } + ); const help = cloud.helpInformation(); expect(help).not.toMatch(/^\s+run\b/m); @@ -147,7 +167,11 @@ describe('deprecateCommand', () => { it('does not warn when a non-deprecated sibling runs', async () => { const { program, cloud } = programWithCloud(); const warn = vi.fn(); - deprecateCommand(cloud.commands.find((c) => c.name() === 'run')!, NOTICE, { warn }); + deprecateCommand( + cloud.commands.find((c) => c.name() === 'run')!, + NOTICE, + { warn } + ); await program.parseAsync(['cloud', 'login'], { from: 'user' }); diff --git a/packages/cli/src/cli/telemetry/events.ts b/packages/cli/src/cli/telemetry/events.ts index 744b4a931b..15bfd59635 100644 --- a/packages/cli/src/cli/telemetry/events.ts +++ b/packages/cli/src/cli/telemetry/events.ts @@ -280,11 +280,7 @@ export interface CliUpdateEvent { } export type AgentRelayToolCallType = - | 'agent.create' - | 'agent.release' - | 'agent.attach' - | 'agent.status' - | (string & {}); + 'agent.create' | 'agent.release' | 'agent.attach' | 'agent.status' | (string & {}); export type AgentRelayToolCallCategory = | 'spawn' diff --git a/packages/cloud/src/connect.ts b/packages/cloud/src/connect.ts index 5d4317935c..4b829b4c0f 100644 --- a/packages/cloud/src/connect.ts +++ b/packages/cloud/src/connect.ts @@ -150,8 +150,7 @@ export async function connectProvider(options: ConnectProviderOptions): Promise< auth = refreshedAuth; const start = (await createResponse.json().catch(() => null)) as - | (AuthSessionResponse & { error?: string; message?: string }) - | null; + (AuthSessionResponse & { error?: string; message?: string }) | null; if (!createResponse.ok || !start?.sessionId) { const detail = start?.error || start?.message || `${createResponse.status} ${createResponse.statusText}`; diff --git a/packages/cloud/src/device-auth.ts b/packages/cloud/src/device-auth.ts index d4f8293ddf..d2966e01f4 100644 --- a/packages/cloud/src/device-auth.ts +++ b/packages/cloud/src/device-auth.ts @@ -53,12 +53,7 @@ export type DeviceFlowHooks = { }; type DeviceTokenErrorCode = - | 'authorization_pending' - | 'slow_down' - | 'access_denied' - | 'expired_token' - | 'invalid_grant' - | (string & {}); + 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token' | 'invalid_grant' | (string & {}); function deviceError(message: string): CloudAuthError { return new CloudAuthError('AUTH_DEVICE_FLOW_FAILED', message); diff --git a/packages/cloud/src/fleet-sandbox.ts b/packages/cloud/src/fleet-sandbox.ts index 87de1660d6..13927cc52b 100644 --- a/packages/cloud/src/fleet-sandbox.ts +++ b/packages/cloud/src/fleet-sandbox.ts @@ -75,12 +75,7 @@ export type CloudRelayfileRepositoryMaterializeOptions = CloudFleetSandboxReques }; export type CloudFleetSandboxProviderId = - | 'daytona' - | 'e2b' - | 'vercel' - | 'freestyle' - | 'agent37' - | 'microsandbox'; + 'daytona' | 'e2b' | 'vercel' | 'freestyle' | 'agent37' | 'microsandbox'; /** Safe aggregate provider-capacity detail returned before allocation. */ export type CloudFleetSandboxCapacityExhaustion = { @@ -200,9 +195,7 @@ export type EnsureCloudFleetSandboxInput = { }; export type CloudFleetSandboxWorkloadProfile = - | 'standard' - | 'long-running-agent' - | 'standard-long-running-agent'; + 'standard' | 'long-running-agent' | 'standard-long-running-agent'; const CLOUD_FLEET_SANDBOX_PROVIDER_IDS: readonly CloudFleetSandboxProviderId[] = [ 'daytona', @@ -284,9 +277,7 @@ export type CloudFleetSandboxProvisioningTimeout = }); export type EnsureCloudFleetSandboxResult = - | CloudFleetSandboxReady - | CloudFleetSandboxReused - | CloudFleetSandboxProvisioningTimeout; + CloudFleetSandboxReady | CloudFleetSandboxReused | CloudFleetSandboxProvisioningTimeout; export type DeleteCloudFleetSandboxInput = { cloudWorkspaceId: string; diff --git a/packages/config/src/shadow-config.ts b/packages/config/src/shadow-config.ts index 8381a8b252..320d126cbf 100644 --- a/packages/config/src/shadow-config.ts +++ b/packages/config/src/shadow-config.ts @@ -22,11 +22,7 @@ import path from 'node:path'; /** Triggers for when a shadow agent should speak */ export type SpeakOnTrigger = - | 'ALL_MESSAGES' - | 'EXPLICIT_ASK' - | 'SESSION_END' - | 'CODE_WRITTEN' - | 'REVIEW_REQUEST'; + 'ALL_MESSAGES' | 'EXPLICIT_ASK' | 'SESSION_END' | 'CODE_WRITTEN' | 'REVIEW_REQUEST'; /** Shadow role definition */ export interface ShadowRoleConfig { diff --git a/packages/fleet/README.md b/packages/fleet/README.md index ead20d44d0..ddcee378d9 100644 --- a/packages/fleet/README.md +++ b/packages/fleet/README.md @@ -59,9 +59,7 @@ export default defineNode({ 'AgentWorkforce/factory': '/srv/repos/factory', 'AgentWorkforce/relay': '/srv/repos/relay', }, - capabilities: { - /* … */ - }, + capabilities: {/* … */}, }); ``` @@ -79,9 +77,7 @@ import { defineNode, serveNode, startServeNode } from '@agent-relay/fleet'; const definition = defineNode({ name: 'builder', - capabilities: { - /* … */ - }, + capabilities: {/* … */}, }); const connection = { url: 'http://127.0.0.1:8787' }; // broker HTTP API base URL (apiKey optional) diff --git a/packages/harness-driver/src/harness.ts b/packages/harness-driver/src/harness.ts index 7356ceee90..090625033d 100644 --- a/packages/harness-driver/src/harness.ts +++ b/packages/harness-driver/src/harness.ts @@ -94,9 +94,7 @@ export interface StaticNativeHarnessDefinition extends Omit (id: string) => unknown) | undefined { const proc = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process; const mod = proc?.getBuiltinModule?.('node:module') as - | { createRequire?: (path: string) => (id: string) => unknown } - | undefined; + { createRequire?: (path: string) => (id: string) => unknown } | undefined; return mod?.createRequire; } diff --git a/packages/sdk/src/actions/types.ts b/packages/sdk/src/actions/types.ts index 32febba5fd..8088174b5f 100644 --- a/packages/sdk/src/actions/types.ts +++ b/packages/sdk/src/actions/types.ts @@ -47,8 +47,7 @@ export interface ZodLikeError { } export type ZodLikeParseResult = - | { success: true; data: TOutput } - | { success: false; error: ZodLikeError }; + { success: true; data: TOutput } | { success: false; error: ZodLikeError }; export interface ZodLikeSchema { safeParse(input: unknown): ZodLikeParseResult; diff --git a/packages/sdk/src/listeners.ts b/packages/sdk/src/listeners.ts index 281b102d4a..ae20aae086 100644 --- a/packages/sdk/src/listeners.ts +++ b/packages/sdk/src/listeners.ts @@ -460,11 +460,7 @@ export interface RelayEventEnvelope { } export type RelayMessageEventType = - | 'message.created' - | 'message.updated' - | 'thread.reply' - | 'dm.received' - | 'group_dm.received'; + 'message.created' | 'message.updated' | 'thread.reply' | 'dm.received' | 'group_dm.received'; export interface RelayMessageEvent { type: TType; diff --git a/packages/sdk/src/messaging/relaycast-placement.ts b/packages/sdk/src/messaging/relaycast-placement.ts index 2b904788a4..548b8f4f38 100644 --- a/packages/sdk/src/messaging/relaycast-placement.ts +++ b/packages/sdk/src/messaging/relaycast-placement.ts @@ -8,10 +8,7 @@ import type { RelayNode } from './types.js'; export type PlacementReconcileReason = - | 'no_eligible_node' - | 'target_offline' - | 'unmapped_repo' - | 'sandbox_policy_mismatch'; + 'no_eligible_node' | 'target_offline' | 'unmapped_repo' | 'sandbox_policy_mismatch'; /** * Evidence state for a targeted spawn placement. diff --git a/packages/sdk/src/messaging/types.ts b/packages/sdk/src/messaging/types.ts index 7887211f86..9128cd2347 100644 --- a/packages/sdk/src/messaging/types.ts +++ b/packages/sdk/src/messaging/types.ts @@ -627,10 +627,7 @@ export type RelayPlacementRejectReason = | 'unmapped_repo'; export type RelayPlacementReconcileReason = - | 'no_eligible_node' - | 'target_offline' - | 'unmapped_repo' - | 'sandbox_policy_mismatch'; + 'no_eligible_node' | 'target_offline' | 'unmapped_repo' | 'sandbox_policy_mismatch'; export interface RelayPlacementReconcileEvent { action: 'queued' | 'failed'; @@ -761,9 +758,7 @@ export interface RelayWorkspaceInfo { * status). */ export type InboxItemState = - | Extract - | 'deferred' - | 'read'; + Extract | 'deferred' | 'read'; export interface InboxItem { id: string; diff --git a/packages/sdk/src/session/types.ts b/packages/sdk/src/session/types.ts index c3837c32e1..da6b0f3b67 100644 --- a/packages/sdk/src/session/types.ts +++ b/packages/sdk/src/session/types.ts @@ -10,8 +10,7 @@ export type ObservabilityFidelity = 'exact' | 'inferred'; export type ObservabilitySource = 'ai-sdk' | 'pty-structured' | 'pty-terminal' | 'broker'; export type ObservabilitySupport = - | { available: true; fidelities: readonly ObservabilityFidelity[] } - | { available: false }; + { available: true; fidelities: readonly ObservabilityFidelity[] } | { available: false }; export const OBSERVABILITY_UNAVAILABLE = { available: false } as const satisfies ObservabilitySupport; export const EXACT_OBSERVABILITY = { @@ -124,12 +123,7 @@ export type MessageAttachmentCapability = 'text' | 'image'; export type AgentSessionDeliveryMode = 'immediate' | 'next-message' | 'next-tool-call' | 'on-idle' | 'manual'; export type MessageDeliveryReason = - | 'message' - | 'mention' - | 'dm' - | 'thread-reply' - | 'action-result' - | 'notification'; + 'message' | 'mention' | 'dm' | 'thread-reply' | 'action-result' | 'notification'; export interface MessageContext { id: string; diff --git a/packages/session/src/client.test.ts b/packages/session/src/client.test.ts index a5d1ab6e7b..0a949f35c2 100644 --- a/packages/session/src/client.test.ts +++ b/packages/session/src/client.test.ts @@ -255,23 +255,21 @@ describe('SessionClient', () => { it('announces an aged-out Relaycast boundary instead of returning a confident partial replay', async () => { const backend = relayhistoryBackend(); - const bySessionRef = vi.fn( - async (sessionRef: string): Promise => ({ - sessionRef, - availability: 'aged_out', - reason: 'outside_retention_window', - retention: { - policy: 'window', - messageTtlDays: 30, - retainedSince: '2026-07-14T08:00:00.000Z', - source: 'deployment_default', - }, - sessionStartedAt: '2026-06-01T08:00:00.000Z', - sessionLastMessageAt: '2026-06-01T09:00:00.000Z', - messages: [], - page: { nextCursor: null, hasMore: false }, - }) - ); + const bySessionRef = vi.fn(async (sessionRef: string): Promise => ({ + sessionRef, + availability: 'aged_out', + reason: 'outside_retention_window', + retention: { + policy: 'window', + messageTtlDays: 30, + retainedSince: '2026-07-14T08:00:00.000Z', + source: 'deployment_default', + }, + sessionStartedAt: '2026-06-01T08:00:00.000Z', + sessionLastMessageAt: '2026-06-01T09:00:00.000Z', + messages: [], + page: { nextCursor: null, hasMore: false }, + })); const client = testClient(backend.fetch, { relaycast: { bySessionRef } }); const session = await client.createSession({ cli: 'codex', node: 'origin-node', owner: OWNER }); diff --git a/packages/session/src/types.ts b/packages/session/src/types.ts index 2c4431285f..e471414c1d 100644 --- a/packages/session/src/types.ts +++ b/packages/session/src/types.ts @@ -54,8 +54,7 @@ export interface Turn { } export type ResumeMode = - | { mode: 'native'; nativeResumeId: string } - | { mode: 'inject'; contextPrompt: string }; + { mode: 'native'; nativeResumeId: string } | { mode: 'inject'; contextPrompt: string }; export interface ResumeSessionResult { session: RelaySession; diff --git a/plans/001-adopt-ai-sdk-harnesses.md b/plans/001-adopt-ai-sdk-harnesses.md index 6fa40666b0..49748f8e8e 100644 --- a/plans/001-adopt-ai-sdk-harnesses.md +++ b/plans/001-adopt-ai-sdk-harnesses.md @@ -94,8 +94,7 @@ type ObservabilityFidelity = 'exact' | 'inferred'; type ObservabilitySource = 'ai-sdk' | 'pty-structured' | 'pty-terminal' | 'broker'; type ObservabilitySupport = - | { available: true; fidelities: readonly ObservabilityFidelity[] } - | { available: false }; + { available: true; fidelities: readonly ObservabilityFidelity[] } | { available: false }; ``` Relaycast publishes `agent.activity.changed` with agent id, current and previous activity, reason, turn id, sequence, timestamp, source, fidelity, and optional tool/approval details. `waiting` always includes a reason such as `tool_approval`, `turn_suspended`, or `external_dependency`. diff --git a/scripts/verify-features/escalation-status.d.mts b/scripts/verify-features/escalation-status.d.mts index 93741d0cab..f22abc2f2d 100644 --- a/scripts/verify-features/escalation-status.d.mts +++ b/scripts/verify-features/escalation-status.d.mts @@ -1,10 +1,5 @@ export type EscalationChannel = - | 'infra' - | 'posthog' - | 'github_issue' - | 'draft_pr' - | 'slack_primary' - | 'slack_followup'; + 'infra' | 'posthog' | 'github_issue' | 'draft_pr' | 'slack_primary' | 'slack_followup'; export type EscalationState = 'delivered' | 'failed' | 'disabled' | 'not_applicable' | 'pending'; diff --git a/scripts/verify-features/targeted-pr-plan.mjs b/scripts/verify-features/targeted-pr-plan.mjs index b53c219f99..01af1b249a 100644 --- a/scripts/verify-features/targeted-pr-plan.mjs +++ b/scripts/verify-features/targeted-pr-plan.mjs @@ -587,7 +587,7 @@ export function changedFilesFromGit(base, head, { cwd = process.cwd() } = {}) { if (result.status !== 0) throw new Error(`git diff failed: ${(result.stderr ?? '').trim()}`); const fields = result.stdout.split('\0').filter(Boolean); const files = []; - for (let index = 0; index < fields.length; ) { + for (let index = 0; index < fields.length;) { const status = fields[index++]; if (/^[RC]/.test(status)) { files.push(fields[index++], fields[index++]); diff --git a/tests/e2e/github-subscriptions/proof.mjs b/tests/e2e/github-subscriptions/proof.mjs index 5e2c1a848b..c485203c30 100644 --- a/tests/e2e/github-subscriptions/proof.mjs +++ b/tests/e2e/github-subscriptions/proof.mjs @@ -189,12 +189,10 @@ export function correlate({ if (actions.length > 1) return { pass: false, missing: 'duplicate actor actions for one unique nonce', ingestId: ingest.id }; if (!action) return { pass: false, missing: 'exact actor digest response', ingestId: ingest.id }; - if ( - !( - Date.parse(action.created_at) >= Date.parse(injected.observedAt) && - Date.parse(action.created_at) <= deadline - ) - ) + if (!( + Date.parse(action.created_at) >= Date.parse(injected.observedAt) && + Date.parse(action.created_at) <= deadline + )) return { pass: false, missing: 'actor action within injection/deadline boundaries', ingestId: ingest.id }; const idle = events.filter( (e) => diff --git a/tests/integration/broker/evals/scenarios/s05-phrasing-variants.ts b/tests/integration/broker/evals/scenarios/s05-phrasing-variants.ts index 4edb8af13a..85346594be 100644 --- a/tests/integration/broker/evals/scenarios/s05-phrasing-variants.ts +++ b/tests/integration/broker/evals/scenarios/s05-phrasing-variants.ts @@ -26,12 +26,7 @@ import { onboardingText } from './onboarding.js'; import { RESPONSE_MS, STARTUP_MS } from './helpers.js'; export type PhrasingVariant = - | 'neutral-worker' - | 'neutral-agent' - | 'relay-worker' - | 'relay-agent' - | 'arw-worker' - | 'arw-agent'; + 'neutral-worker' | 'neutral-agent' | 'relay-worker' | 'relay-agent' | 'arw-worker' | 'arw-agent'; export const PHRASING_VARIANTS: PhrasingVariant[] = [ 'neutral-worker',