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
4 changes: 3 additions & 1 deletion desktop/src-tauri/src/managed_agents/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,12 @@ pub async fn restore_managed_agents_on_launch(
// mid-turn session is not resumed by an
// eager child — and silently reintroduces
// N idle brains on every launch.
// Requested URL, not the canonical
// key: the child dials this.
spawn_agent_child(
app,
record,
&key.relay_url,
&relay_url,
true,
owner_hex_ref,
)
Expand Down
29 changes: 22 additions & 7 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1692,9 +1692,17 @@ pub fn spawn_agent_child(
.map(|p| p.display().to_string())
.unwrap_or_else(|| effective_command.clone());

// The caller supplies the explicit canonical pair relay. This is the only
// relay this child may connect to, regardless of the record/workspace default.
let effective_relay_url = runtime_key.relay_url.clone();
// The caller supplies the explicit pair relay. This is the only relay this
// child may connect to, regardless of the record/workspace default.
//
// Dial the URL as requested, NOT `runtime_key.relay_url`. The key is an
// identity: `ManagedAgentRuntimeKey::new` canonicalizes it (folding loopback
// to 127.0.0.1) so `runtime_id()` stays stable, which is right for keying
// and wrong for addressing. The relay resolves a community from the literal
// request Host and fails closed on an unmapped one, so a deployment
// configured as `ws://localhost:PORT` rejects a canonicalized
// `ws://127.0.0.1:PORT` with a generic 404 and the harness never connects.
let effective_relay_url = relay_url.to_string();

// Augment PATH for DMG launches so child processes can find:
// - bundled CLI via ~/.local/bin symlink
Expand Down Expand Up @@ -2045,13 +2053,19 @@ pub fn spawn_agent_child(

// Stamp the effective spawn config so the summary builder can flag
// needs_restart when disk state drifts from what this process runs.
// `effective_relay_url` is already resolved, and resolution is idempotent,
// so it serves as the workspace-relay input here.
//
// Hash the CANONICAL pair URL, not the dialed one. `needs_restart`
// recomputes this hash from `key.relay_url` (the canonical identity), so
// feeding the requested spelling here would make the two disagree for any
// host the key folds (e.g. `localhost` vs `127.0.0.1`) and report
// needs_restart forever. The relay a child dials and the config it was
// spawned with are separate concerns: dial the request, fingerprint the
// identity.
let spawn_config_hash = super::spawn_hash::spawn_config_hash(
record,
&personas,
&teams,
&effective_relay_url,
&runtime_key.relay_url,
&global,
);

Expand Down Expand Up @@ -2134,7 +2148,8 @@ pub fn start_managed_agent_process(
// Scalar PIDs are migration-only and never establish pair liveness.
record.runtime_pid = None;

let mut process = spawn_agent_child(app, record, &key.relay_url, false, owner_hex)?;
// Pass the requested URL, not the canonical key: the child dials this.
let mut process = spawn_agent_child(app, record, &relay_url, false, owner_hex)?;
let now = now_iso();
let receipt = super::ManagedAgentRuntimeReceipt {
key: key.clone(),
Expand Down
12 changes: 9 additions & 3 deletions desktop/src-tauri/src/managed_agents/runtime_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ fn start_pair(
.lock()
.ok()
.map(|keys| keys.public_key().to_hex());
let mut process = spawn_agent_child(&app, record, &key.relay_url, lazy, owner.as_deref())?;
// Pass the requested URL, not the canonical key: the child dials this.
let mut process = spawn_agent_child(&app, record, &relay_url, lazy, owner.as_deref())?;
Comment thread
ahmetkca marked this conversation as resolved.
let now = crate::util::now_iso();
let receipt = ManagedAgentRuntimeReceipt {
key: key.clone(),
Expand Down Expand Up @@ -403,7 +404,11 @@ async fn probe_agent_relay_access(
let key = ManagedAgentRuntimeKey::new(record.pubkey.clone(), &requested_relay_url)?;
let keys = nostr::Keys::parse(record.private_key_nsec.trim())
.map_err(|error| format!("invalid managed-agent key: {error}"))?;
let api_base = crate::relay::relay_http_base_url(&key.relay_url);
// Probe the relay as requested, not via the canonical key: the relay
// resolves a community from the literal Host and fails closed on an
// unmapped one, so probing a canonicalized loopback host 404s on a
// deployment configured with `localhost`.
let api_base = crate::relay::relay_http_base_url(&requested_relay_url);
tokio::time::timeout(
std::time::Duration::from_secs(10),
crate::relay::query_relay_at_with_keys(
Expand Down Expand Up @@ -504,7 +509,8 @@ pub async fn reconcile_managed_agent_runtimes(
Ok((record, key, requested)) => {
match start_pair(
record.pubkey.clone(),
key.relay_url.clone(),
// Requested URL, not the canonical key: this is dialed.
requested.clone(),
true,
Some(&record.updated_at),
app.clone(),
Expand Down
37 changes: 37 additions & 0 deletions desktop/src-tauri/src/managed_agents/runtime_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,43 @@ impl ManagedAgentRuntimeKey {
}
}

#[cfg(test)]
mod tests {
use super::ManagedAgentRuntimeKey;

#[test]
fn runtime_key_relay_url_is_canonical_identity_not_a_dial_address() {
// `relay_url` here is canonicalized (loopback folds to 127.0.0.1) so
// `runtime_id()` is stable for one pair no matter how the operator spelled
// the host. That is correct for keying, and it is exactly why this field
// must never be handed to the harness as the URL to connect to: the relay
// resolves a community from the literal request Host and fails closed on
// an unmapped one, so a deployment configured as `ws://localhost:PORT`
// rejects `ws://127.0.0.1:PORT` with a generic 404. `spawn_agent_child`
// dials the URL as requested; see the comment there.
let pubkey = "a".repeat(64);
for spelling in [
"ws://localhost:3000",
"ws://127.0.0.1:3000",
"ws://[::1]:3000",
] {
let key = ManagedAgentRuntimeKey::new(pubkey.clone(), spelling)
.expect("loopback relay URL should be accepted");
assert_eq!(
key.relay_url, "ws://127.0.0.1:3000",
"spelling {spelling} should canonicalize"
);
}

// One identity across spellings, so the on-disk runtime path is stable.
let named = ManagedAgentRuntimeKey::new(pubkey.clone(), "ws://localhost:3000")
.expect("valid relay URL");
let numeric = ManagedAgentRuntimeKey::new(pubkey.clone(), "ws://127.0.0.1:3000")
.expect("valid relay URL");
assert_eq!(named.runtime_id(), numeric.runtime_id());
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ManagedAgentRuntimeLifecycle {
Expand Down
Loading