diff --git a/crates/agentkeys-broker-server/src/handlers/agent/claim.rs b/crates/agentkeys-broker-server/src/handlers/agent/claim.rs index 5ccbe3a7..84b2ad3c 100644 --- a/crates/agentkeys-broker-server/src/handlers/agent/claim.rs +++ b/crates/agentkeys-broker-server/src/handlers/agent/claim.rs @@ -6,7 +6,8 @@ //! named a master, so an unclaimed request is inert (Sybil-safe). On claim the //! broker: //! -//! 1. derives the HDKD child omni `O_agent = SHA256(HDKD_DOMAIN || O_master || "//label")` +//! 1. derives the HDKD child omni for the initial generation path +//! `//label/0`; //! — the master "adopts" the agent under its own omni tree; //! 2. assigns `operator_omni` + `child_omni` + `label` + `requested_scope` onto //! the (previously unbound) row, marking it claimed; @@ -52,8 +53,12 @@ pub async fn pairing_claim( agentkeys_core::actor_omni::validate_label(&body.label) .map_err(|e| BrokerError::BadRequest(format!("invalid label: {e}")))?; - let child_omni = agentkeys_core::actor_omni::child_omni_hex(&master_omni, &body.label) - .map_err(|e| BrokerError::BadRequest(format!("derive child omni: {e}")))?; + let child_omni = agentkeys_core::actor_omni::child_omni_generation_hex( + &master_omni, + &body.label, + agentkeys_core::actor_omni::INITIAL_CHILD_GENERATION, + ) + .map_err(|e| BrokerError::BadRequest(format!("derive child omni: {e}")))?; let requested_scope = body .requested_scope diff --git a/crates/agentkeys-broker-server/src/handlers/agent/mod.rs b/crates/agentkeys-broker-server/src/handlers/agent/mod.rs index d81491a5..4745625e 100644 --- a/crates/agentkeys-broker-server/src/handlers/agent/mod.rs +++ b/crates/agentkeys-broker-server/src/handlers/agent/mod.rs @@ -10,7 +10,7 @@ //! `pop_sig`, store an UNBOUND request (naming no master), return a //! `pairing_code` to display + a secret `request_id` retrieval ticket. //! - `POST /v1/agent/pairing/claim` (master, `J1_master`-gated) — claim the -//! code; derive the HDKD child omni `O_agent = SHA256(.. || O_master || "//label")`, +//! code; derive the HDKD child omni `O_agent = SHA256(.. || O_master || "//label/0")`, //! mark the request claimed, and stash the device artifact as a pending binding. //! - `POST /v1/agent/pairing/poll` (agent, no bearer) — once claimed, re-prove //! device-key possession (fresh `pop_sig`) and mint + retrieve `J1_agent`. diff --git a/crates/agentkeys-broker-server/src/handlers/agent/poll.rs b/crates/agentkeys-broker-server/src/handlers/agent/poll.rs index 78c23b86..473faf44 100644 --- a/crates/agentkeys-broker-server/src/handlers/agent/poll.rs +++ b/crates/agentkeys-broker-server/src/handlers/agent/poll.rs @@ -96,7 +96,11 @@ pub async fn pairing_poll( // 3. Mint J1_agent fresh (HDKD omni + lineage). The agent authenticates with // this immediately, but has NO scope until the master approves the binding. - let derivation_path = format!("//{label}"); + let derivation_path = agentkeys_core::actor_omni::generation_derivation_path( + &label, + agentkeys_core::actor_omni::INITIAL_CHILD_GENERATION, + ) + .map_err(|e| BrokerError::Internal(format!("derive J1_agent path: {e}")))?; let session_jwt = mint_agent_session_jwt( &state.session_keypair, &state.config.oidc_issuer, diff --git a/crates/agentkeys-broker-server/src/jwt/verify.rs b/crates/agentkeys-broker-server/src/jwt/verify.rs index 1f1e1586..620d431f 100644 --- a/crates/agentkeys-broker-server/src/jwt/verify.rs +++ b/crates/agentkeys-broker-server/src/jwt/verify.rs @@ -39,7 +39,7 @@ pub struct AgentKeysClaims { /// Agent only: the parent (master) omni this child was HDKD-derived from. #[serde(default, skip_serializing_if = "Option::is_none")] pub parent_omni: Option, - /// Agent only: the HDKD path, e.g. `"//agent-a"`. + /// Agent only: the HDKD path, e.g. `"//agent-a/0"`. #[serde(default, skip_serializing_if = "Option::is_none")] pub derivation_path: Option, /// Agent only: the K10 device address whose pop_sig redeemed the link code. diff --git a/crates/agentkeys-broker-server/tests/agent_bootstrap_flow.rs b/crates/agentkeys-broker-server/tests/agent_bootstrap_flow.rs index 651209b3..2e471143 100644 --- a/crates/agentkeys-broker-server/tests/agent_bootstrap_flow.rs +++ b/crates/agentkeys-broker-server/tests/agent_bootstrap_flow.rs @@ -244,7 +244,7 @@ async fn full_request_claim_poll_pending_flow() { // Public recomputability (acceptance criterion). assert_eq!( child_omni, - agentkeys_core::actor_omni::child_omni_hex(&master_omni, "agent-a").unwrap() + agentkeys_core::actor_omni::child_omni_generation_hex(&master_omni, "agent-a", 0).unwrap() ); assert_eq!(claim["operator_omni"], master_omni); assert_eq!(claim["request_id"], request_id); @@ -277,8 +277,9 @@ async fn full_request_claim_poll_pending_flow() { ); assert_eq!( claims.agentkeys.derivation_path.as_deref(), - Some("//agent-a") + Some("//agent-a/0") ); + assert_eq!(claimed_poll["derivation_path"], "//agent-a/0"); assert_eq!( claims.agentkeys.device_pubkey.as_deref(), Some(dk.address()) diff --git a/crates/agentkeys-core/src/actor_omni.rs b/crates/agentkeys-core/src/actor_omni.rs index 8f83bcc3..6c2e7f8d 100644 --- a/crates/agentkeys-core/src/actor_omni.rs +++ b/crates/agentkeys-core/src/actor_omni.rs @@ -66,9 +66,13 @@ pub fn actor_omni_hex(wallet: &WalletAddress) -> String { /// Distinct from `DOMAIN` so a wallet-omni and a child-omni can never collide. const HDKD_DOMAIN: &[u8] = b"agentkeys-hdkd-v1"; -/// Validate an HDKD child label (`^[a-z0-9-]{1,32}$`). The label is spliced into -/// the child-omni digest AND stored/echoed on chain + in JWT claims, so it must -/// be a tight charset (no path separators, no whitespace, no uppercase). +/// Initial child-key generation. The first pair flow always reserves the +/// generation suffix by deriving at `