Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
89e37f4
docs: clarify Windows compile-parity requirements for CI checkpoints
Spenquatch Apr 25, 2026
12a3690
docs: start AHCSITC0 code+test
Spenquatch Apr 25, 2026
c8d7c94
test: add AHCSITC0 successor contract coverage
Spenquatch Apr 25, 2026
d7d1384
docs: allow AHCSITC0 task touches
Spenquatch Apr 25, 2026
e8801af
task: AHCSITC0-code
Spenquatch Apr 25, 2026
fcbdb8c
docs: finish AHCSITC0 code+test
Spenquatch Apr 25, 2026
e9e4552
docs: start AHCSITC0-integ
Spenquatch Apr 25, 2026
8941b49
merge: integrate AHCSITC0-code
Spenquatch Apr 25, 2026
f9342b4
merge: integrate AHCSITC0-test
Spenquatch Apr 25, 2026
60ce081
test: align gateway unavailable contract
Spenquatch Apr 25, 2026
49a2e51
docs: allow AHCSITC0 integration touch
Spenquatch Apr 25, 2026
db2467c
merge: AHCSITC0-integ (agent-hub-core-successor-identity-tuple-compat…
Spenquatch Apr 25, 2026
af1be75
docs: finish AHCSITC0-integ
Spenquatch Apr 25, 2026
c21ae91
docs: start AHCSITC1 code+test
Spenquatch Apr 25, 2026
7cb0030
docs: finish AHCSITC1 code+test
Spenquatch Apr 25, 2026
45864b8
docs: start AHCSITC1-integ
Spenquatch Apr 25, 2026
a1b10b1
docs: finish AHCSITC1-integ
Spenquatch Apr 25, 2026
a027770
docs: start AHCSITC2 code+test
Spenquatch Apr 25, 2026
66c56b7
feat: lock agent event telemetry fields
Spenquatch Apr 25, 2026
b7ec0f5
task: AHCSITC2-test
Spenquatch Apr 25, 2026
c37317d
docs: allow AHCSITC2 code touch set
Spenquatch Apr 25, 2026
846020b
docs: finish AHCSITC2 code+test
Spenquatch Apr 25, 2026
6844cd5
docs: start AHCSITC2-integ-core
Spenquatch Apr 25, 2026
b5f31f1
Merge branch 'agent-hub-core-successor-identity-tuple-compatible-ahcs…
Spenquatch Apr 25, 2026
d3f9928
Merge branch 'agent-hub-core-successor-identity-tuple-compatible-ahcs…
Spenquatch Apr 25, 2026
5d86445
fix: reconcile AHCSITC2 integration telemetry
Spenquatch Apr 25, 2026
277d48a
docs: finish AHCSITC2-integ-core
Spenquatch Apr 25, 2026
07383ed
docs: start CP1-ci-checkpoint
Spenquatch Apr 25, 2026
d29c131
docs: finish CP1-ci-checkpoint
Spenquatch Apr 25, 2026
486a3dc
docs: mark AHCSITC2 parity no-ops
Spenquatch Apr 25, 2026
61cdc89
merge: integrate AHCSITC2-integ-core
Spenquatch Apr 25, 2026
56b9e01
docs: finish AHCSITC2-integ
Spenquatch Apr 25, 2026
e6d68d7
docs: start AHCSITC3 code+test
Spenquatch Apr 25, 2026
19b972a
test: lock ahcsitc3 closeout surfaces
Spenquatch Apr 25, 2026
9a63080
fix: align agent doctor world boundary checks
Spenquatch Apr 25, 2026
01ee9d5
docs: finish AHCSITC3 code+test
Spenquatch Apr 25, 2026
d310b0a
docs: start AHCSITC3-integ-core
Spenquatch Apr 25, 2026
0a13096
merge: integrate AHCSITC3 code branch
Spenquatch Apr 25, 2026
b4cc988
merge: integrate AHCSITC3 test branch
Spenquatch Apr 25, 2026
c6466f0
fix: close AHCSITC3 integration drift
Spenquatch Apr 25, 2026
48919c4
docs: finish AHCSITC3-integ-core
Spenquatch Apr 25, 2026
725a62f
docs: start CP2-ci-checkpoint
Spenquatch Apr 25, 2026
ca77a80
docs: finish CP2-ci-checkpoint
Spenquatch Apr 25, 2026
d49efc6
docs: log CP2-ci-checkpoint evidence
Spenquatch Apr 25, 2026
0fbd1a3
docs: mark AHCSITC3 platform fixes noop
Spenquatch Apr 25, 2026
25c7b34
Merge branch 'agent-hub-core-successor-identity-tuple-compatible-ahcs…
Spenquatch Apr 25, 2026
fd3e72a
docs: finish AHCSITC3-integ
Spenquatch Apr 25, 2026
6083b3f
patch: enhance world_gateway_disabled_state test with fixture setup
Spenquatch Apr 25, 2026
ff6d5ed
fix: preserve nested agent status rows by run id
Spenquatch Apr 25, 2026
9aacc38
fix: preserve member roles in agent status
Spenquatch Apr 25, 2026
40f65fa
fix: harden agent status session projection
Spenquatch Apr 25, 2026
f2c62c7
Fix macOS gateway TCP fallback
Spenquatch Apr 25, 2026
6c104a0
Validate filtered world identity in agent status
Spenquatch Apr 25, 2026
cf82ae4
Preserve parent run IDs in agent event traces
Spenquatch Apr 25, 2026
7f5b07f
Fix macOS gateway tests to clear SUBSTRATE_HOME
Spenquatch Apr 25, 2026
b6a583d
Refine agent doctor policy allowlist handling
Spenquatch Apr 25, 2026
707baa3
Fix nested selected status rows to fail closed on missing provider/auth
Spenquatch Apr 25, 2026
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
102 changes: 100 additions & 2 deletions crates/common/src/agent_events.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use std::fmt;
use std::io;
use std::sync::OnceLock;

use chrono::{DateTime, Utc};
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::json;

use crate::identity::{
validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementPosture,
validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementExecution,
PlacementPosture,
};

pub const AGENT_EVENT_CHANNEL_MAX_BYTES: usize = 64;
const PURE_AGENT_ROUTER: &str = "agent_hub";
const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session";

/// Canonical set of agent event categories.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
Expand Down Expand Up @@ -73,6 +78,8 @@ pub struct AgentEvent {
pub agent_id: String,
pub orchestration_session_id: String,
pub run_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_run_id: Option<String>,

// Attribution + correlation (optional)
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand All @@ -84,6 +91,8 @@ pub struct AgentEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub world_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub world_generation: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cmd_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span_id: Option<String>,
Expand Down Expand Up @@ -116,6 +125,8 @@ struct AgentEventDef {
orchestration_session_id: String,
run_id: String,
#[serde(default)]
parent_run_id: Option<String>,
#[serde(default)]
backend_id: Option<String>,
#[serde(default)]
thread_id: Option<String>,
Expand All @@ -124,12 +135,24 @@ struct AgentEventDef {
#[serde(default)]
world_id: Option<String>,
#[serde(default)]
world_generation: Option<u64>,
#[serde(default)]
cmd_id: Option<String>,
#[serde(default)]
span_id: Option<String>,
#[serde(default, deserialize_with = "deserialize_sanitized_channel")]
channel: Option<String>,
#[serde(default)]
client: Option<String>,
#[serde(default)]
router: Option<String>,
#[serde(default)]
protocol: Option<String>,
#[serde(default)]
provider: Option<String>,
#[serde(default)]
auth_authority: Option<String>,
#[serde(default)]
identity_tuple: Option<IdentityTuple>,
#[serde(default)]
placement_posture: Option<PlacementPosture>,
Expand Down Expand Up @@ -173,11 +196,13 @@ impl AgentEvent {
kind,
orchestration_session_id: orchestration_session_id.into(),
run_id: run_id.into(),
parent_run_id: None,
data,
backend_id: None,
thread_id: None,
role: None,
world_id: None,
world_generation: None,
cmd_id: None,
span_id: None,
channel: None,
Expand Down Expand Up @@ -253,27 +278,100 @@ impl AgentEvent {
self.placement_posture.as_ref(),
)
}

pub fn set_pure_agent_telemetry_identity(&mut self, client: impl Into<String>) {
if self.identity_tuple.is_none() {
self.identity_tuple = Some(IdentityTuple {
client: client.into(),
router: PURE_AGENT_ROUTER.to_string(),
protocol: PURE_AGENT_PROTOCOL.to_string(),
provider: None,
auth_authority: None,
});
}

if self.placement_posture.is_none() {
self.placement_posture = Some(PlacementPosture {
execution: if self.world_id.is_some() {
PlacementExecution::InWorld
} else {
PlacementExecution::HostOnly
},
host_to_world_bridge: None,
});
}
}

pub fn to_trace_record(&self) -> Result<serde_json::Value, serde_json::Error> {
let mut entry = serde_json::to_value(self)?;
let Some(obj) = entry.as_object_mut() else {
return Err(serde_json::Error::io(io::Error::other(
"agent event must serialize as a JSON object",
)));
};

if let Some(tuple) = self.identity_tuple.as_ref() {
obj.insert("client".to_string(), json!(tuple.client));
obj.insert("router".to_string(), json!(tuple.router));
obj.insert("protocol".to_string(), json!(tuple.protocol));

match tuple.provider.as_deref() {
Some(provider) => {
obj.insert("provider".to_string(), json!(provider));
}
None => {
obj.remove("provider");
}
}

match tuple.auth_authority.as_deref() {
Some(auth_authority) => {
obj.insert("auth_authority".to_string(), json!(auth_authority));
}
None => {
obj.remove("auth_authority");
}
}
}

Ok(entry)
}
}

impl TryFrom<AgentEventDef> for AgentEvent {
type Error = String;

fn try_from(value: AgentEventDef) -> Result<Self, Self::Error> {
let identity_tuple = value.identity_tuple.or_else(|| {
let client = value.client?;
let router = value.router?;
let protocol = value.protocol?;
Some(IdentityTuple {
client,
router,
protocol,
provider: value.provider,
auth_authority: value.auth_authority,
})
});

let event = Self {
ts: value.ts,
kind: value.kind,
data: value.data,
agent_id: value.agent_id,
orchestration_session_id: value.orchestration_session_id,
run_id: value.run_id,
parent_run_id: value.parent_run_id,
backend_id: value.backend_id,
thread_id: value.thread_id,
role: value.role,
world_id: value.world_id,
world_generation: value.world_generation,
cmd_id: value.cmd_id,
span_id: value.span_id,
channel: value.channel,
identity_tuple: value.identity_tuple,
identity_tuple,
placement_posture: value.placement_posture,
project: value.project,
};
Expand Down
3 changes: 3 additions & 0 deletions crates/common/src/agent_identity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fn derive_agent_backend_id(kind: &str, agent_id: &str) -> String {
format!("{}:{}", kind.trim(), agent_id.trim())
}
2 changes: 2 additions & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};

pub mod agent_events;
pub mod agent_identity;
pub mod fs_diff;
pub mod identity;
pub mod manager_manifest;
Expand All @@ -12,6 +13,7 @@ pub mod settings;
pub mod world_exec_guard;

pub use agent_events::{AgentEvent, AgentEventKind};
pub use agent_identity::derive_agent_backend_id;
pub use fs_diff::FsDiff;
pub use identity::{
validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementExecution,
Expand Down
39 changes: 39 additions & 0 deletions crates/common/tests/agent_hub_event_envelope_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@ fn envelope_missing_required_fields_is_rejected() {
);
}

#[test]
fn parent_run_id_roundtrips_when_present() {
let mut value = minimal_valid_envelope_json();
value.as_object_mut().expect("envelope object").insert(
"parent_run_id".to_string(),
json!("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f11"),
);

let event: AgentEvent = serde_json::from_value(value).expect("deserialize AgentEvent");
let roundtrip = serde_json::to_value(&event).expect("serialize AgentEvent");

assert_eq!(
roundtrip.get("parent_run_id").and_then(Value::as_str),
Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f11"),
"expected parent_run_id to survive roundtrip; got: {roundtrip}"
);
}

#[test]
fn parent_run_id_omits_by_field_absence_when_unset() {
let event: AgentEvent =
serde_json::from_value(minimal_valid_envelope_json()).expect("deserialize AgentEvent");
let roundtrip = serde_json::to_value(&event).expect("serialize AgentEvent");

assert!(
roundtrip.get("parent_run_id").is_none(),
"expected parent_run_id to omit when unset; got: {roundtrip}"
);
assert!(
!roundtrip.to_string().contains("\"parent_run_id\":null"),
"parent_run_id must not serialize as null when unset: {roundtrip}"
);
}

#[test]
fn safe_channel_roundtrips() {
let mut value = minimal_valid_envelope_json();
Expand Down Expand Up @@ -360,6 +394,7 @@ fn trace_tuple_metadata_preserves_existing_join_keys() {
"agent_id": "demo-agent",
"orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12",
"run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13",
"parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f10",
"backend_id": "cli:codex",
"world_id": "wld_test",
"cmd_id": "cmd_test",
Expand Down Expand Up @@ -396,6 +431,10 @@ fn trace_tuple_metadata_preserves_existing_join_keys() {
roundtrip.get("span_id").and_then(Value::as_str),
Some("spn_test")
);
assert_eq!(
roundtrip.get("parent_run_id").and_then(Value::as_str),
Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f10")
);
assert_eq!(
roundtrip
.pointer("/identity_tuple/router")
Expand Down
Loading