feat(desktop): add relation resolver + inbound-30177 canonical-linkage rule

Phase 2 (revised) of the cross-workspace agent library: the read side of
the definition-instance relation resolver and the inbound kind:30177
canonical-linkage rule, with the interim fail-closed new-link posture. No
instance-removal/insertion coordinator (that is Phase 4b).

Relation resolver: MutationRoute::for_linked_definition resolves an
instance's persona_id against the raw keyless definition store to classify
its linkage (persona_id IS the linked definition's slug). This is the one
canonical join every library mechanism uses; nothing re-derives the
persona_id join ad hoc.

Inbound 30177 canonical linkage: apply_inbound_managed_agent now consults
the resolver and freezes two linkage-authorship cases, applying only safe
per-instance fields (name, parallelism, respond_to, respond_to_allowlist):
- OwnedByLibrary: the matched instance is linked to a projected definition
  and the event would clear or re-point persona_id.
- InadmissibleNewLink: the event would newly link a plain instance to a
  projected definition (only the Phase-4b coordinator may admit).
A frozen linkage re-retains the local record at a monotonically newer
created_at (via retain_agent_record) so the relay head converges back to
the library-authoritative linkage, mirroring the 30175 rule. Plain-to-plain
relinks and no-op events apply exactly as at head. The round-2 projected
persona preflights stay intact in front of this.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-13 13:53:23 -04:00
co-authored by Will Pfleger
parent d5f777e39b
commit 49c027541f
4 changed files with 413 additions and 24 deletions
@@ -187,12 +187,40 @@ fn reconcile_inbound_persona_event_blocking(
}
KIND_MANAGED_AGENT => {
let mut agents = load_managed_agents(&app)?;
apply_inbound_managed_agent(
// §2.8 canonical linkage is resolved against the raw keyless
// definition store (`library_ref` is not wire-carried).
let definitions = load_agent_definitions(&app)?;
let linkage = apply_inbound_managed_agent(
&mut agents,
&definitions,
&d_tag,
managed_agent_content_from_event(&event)?,
);
save_managed_agents(&app, &agents)?;
// §2.8 convergence: a frozen linkage means the retained head (the
// inbound event, retained above) authored a library-owned or
// inadmissible linkage change. Re-retain the LOCAL record's
// projection at a monotonically newer `created_at` so the relay head
// converges back to the library-authoritative linkage — the same
// mechanism §2.7's 30175 rule uses. The frozen record still exists
// (only its linkage was left intact), so it always has a projection
// to reassert.
if let InboundAgentLinkage::Frozen(reason) = linkage {
if let Some(local) = agents.iter().find(|record| record.pubkey == d_tag) {
if let Err(e) = crate::managed_agents::reconcile::retain_agent_record(
&conn,
&scope.owner_keys,
local,
) {
eprintln!(
"buzz-desktop: inbound 30177 convergence re-retain failed for \
{d_tag}: {e}"
);
}
}
eprintln!("buzz-desktop: {}", reason.note(&d_tag));
}
}
_ => unreachable!("kind gated above"),
}
@@ -410,7 +438,60 @@ fn apply_inbound_persona(personas: &mut Vec<AgentDefinition>, inbound: AgentDefi
}
}
/// Merge an inbound kind:30177 managed-agent projection into the local set.
/// The outcome of applying an inbound kind:30177 event under the §2.8
/// canonical-linkage rule.
#[derive(Debug, Clone, PartialEq, Eq)]
enum InboundAgentLinkage {
/// No local match, or any linkage change the event carried was admissible:
/// the event applied exactly as at head.
Applied,
/// The event tried to author or clear a library-owned linkage, or to admit
/// a projected link that only the Phase-4b coordinator may admit. The
/// linkage change (and the definition quad it derives) was IGNORED and only
/// safe per-instance fields were applied. The caller must re-retain the
/// local record at a newer `created_at` so the relay head converges back
/// (§2.8, mirroring §2.7's 30175 rule) and surface the typed reason.
Frozen(LinkageFreezeReason),
}
/// Why an inbound kind:30177 event's linkage authorship was rejected (§2.8).
/// Typed so the convergence + degradation surfacing at the call site is not a
/// bare string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinkageFreezeReason {
/// The matched instance is currently linked to a library-projected
/// definition and the inbound event would clear or re-point its
/// `persona_id`. Linkage is owned by the library machinery — the same
/// OUTPUT-ONLY discipline §2.7 applies to projected definition metadata.
OwnedByLibrary,
/// The inbound event would newly link a plain instance to a projected
/// definition. Admitting a projected link requires the coordinator's
/// `admit_instance_link()` step (Phase 4b); until it lands the interim
/// posture fails the new link closed.
InadmissibleNewLink,
}
impl LinkageFreezeReason {
/// A human-readable degradation note for logging (§2.8 surfacing). The
/// coordinator (Phase 4b) will replace this interim posture with an
/// admission decision.
fn note(self, agent_pubkey: &str) -> String {
match self {
Self::OwnedByLibrary => format!(
"inbound kind:30177 for {agent_pubkey}: refused to re-point/clear a \
library-owned linkage; safe fields applied, re-retaining local head (§2.8)"
),
Self::InadmissibleNewLink => format!(
"inbound kind:30177 for {agent_pubkey}: refused to newly link a plain \
instance to a library-projected definition (needs the Phase-4b \
coordinator); safe fields applied, re-retaining local head (§2.8)"
),
}
}
}
/// Merge an inbound kind:30177 managed-agent projection into the local set
/// under the §2.8 canonical-linkage rule.
///
/// Matches the local record whose `pubkey` equals the event's d-tag (the d-tag
/// IS the agent pubkey — see `build_agent_event`). On match, overwrite ONLY the
@@ -420,35 +501,86 @@ fn apply_inbound_persona(personas: &mut Vec<AgentDefinition>, inbound: AgentDefi
/// untouched. The projection type carries none of them, so they cannot be
/// reached here even if a foreign event tried to inject them.
///
/// # §2.8 canonical linkage
///
/// A library-linked instance's `persona_id` is owned by the library machinery,
/// not the relay. `definitions` is the raw keyless definition store, resolved
/// through the [`MutationRoute::for_linked_definition`] read-side resolver so
/// this arm and every other library mechanism discover the instance↔definition
/// relationship one way. Two linkage changes fail closed (returning
/// [`InboundAgentLinkage::Frozen`], applying only safe per-instance fields):
/// - the matched instance is currently linked to a projected definition and the
/// event would clear or re-point `persona_id` ([`OwnedByLibrary`]);
/// - the event would newly link a currently-plain instance to a projected
/// definition ([`InadmissibleNewLink`]) — a Phase-4b coordinator admission.
///
/// Every other case (no local match, no linkage change, or a plain↔plain
/// relink) applies exactly as at head.
///
/// No match is a no-op: managed agents carry device-local secrets and are never
/// minted from a relay event — an agent that does not already exist locally has
/// no secret key to run with, so inserting a secretless shell would be useless
/// and misleading. This diverges from the persona path, which DOES insert on no
/// match (personas are secretless definitions). Flagged in the reconcile docs.
///
/// [`OwnedByLibrary`]: LinkageFreezeReason::OwnedByLibrary
/// [`InadmissibleNewLink`]: LinkageFreezeReason::InadmissibleNewLink
fn apply_inbound_managed_agent(
agents: &mut [ManagedAgentRecord],
definitions: &[ManagedAgentRecord],
d_tag: &str,
inbound: ManagedAgentEventContent,
) {
if let Some(local) = agents.iter_mut().find(|record| record.pubkey == d_tag) {
local.name = inbound.name;
// Mirror of the slimmed writer (agent_event_content): a
// definition-linked event omits the definition quad because those
// fields resolve through the kind:30175 definition — absent means
// "not carried", never "clear". Definition-less events still carry
// the quad and apply it unconditionally (including clears).
let definition_linked = inbound.persona_id.is_some();
local.persona_id = inbound.persona_id;
if !definition_linked {
local.system_prompt = inbound.system_prompt;
local.model = inbound.model;
local.provider = inbound.provider;
local.persona_source_version = inbound.persona_source_version;
}
local.parallelism = inbound.parallelism;
local.respond_to = inbound.respond_to;
local.respond_to_allowlist = inbound.respond_to_allowlist;
) -> InboundAgentLinkage {
let Some(local) = agents.iter_mut().find(|record| record.pubkey == d_tag) else {
return InboundAgentLinkage::Applied;
};
// Safe per-instance fields apply as at head regardless of the linkage
// decision — they are genuinely instance-local and never library-authored.
local.name = inbound.name;
local.parallelism = inbound.parallelism;
local.respond_to = inbound.respond_to;
local.respond_to_allowlist = inbound.respond_to_allowlist;
// §2.8 canonical-linkage classification, resolved through the read-side
// resolver against the raw keyless definition store. `library_ref` is not
// wire-carried; the linkage's library status can only be read from the
// definition the instance's `persona_id` resolves to.
let linkage_changes = local.persona_id.as_deref() != inbound.persona_id.as_deref();
let freeze = if !linkage_changes {
None
} else if MutationRoute::for_linked_definition(definitions, local.persona_id.as_deref())
== MutationRoute::LibraryProjected
{
Some(LinkageFreezeReason::OwnedByLibrary)
} else if MutationRoute::for_linked_definition(definitions, inbound.persona_id.as_deref())
== MutationRoute::LibraryProjected
{
Some(LinkageFreezeReason::InadmissibleNewLink)
} else {
None
};
if let Some(reason) = freeze {
// Linkage authorship rejected: leave `persona_id` and the
// definition-resolved quad exactly as the local record holds them. The
// caller re-retains this record so the relay head converges back.
return InboundAgentLinkage::Frozen(reason);
}
// Admissible: head behavior. A definition-linked event omits the definition
// quad because those fields resolve through the kind:30175 definition —
// absent means "not carried", never "clear". Definition-less events still
// carry the quad and apply it unconditionally (including clears).
let definition_linked = inbound.persona_id.is_some();
local.persona_id = inbound.persona_id;
if !definition_linked {
local.system_prompt = inbound.system_prompt;
local.model = inbound.model;
local.provider = inbound.provider;
local.persona_source_version = inbound.persona_source_version;
}
InboundAgentLinkage::Applied
}
/// Merge an inbound kind:30176 team projection into the local set.
@@ -265,7 +265,7 @@ fn inbound_managed_agent_drops_injected_secrets_and_harness() {
let content =
crate::managed_agents::agent_events::managed_agent_content_from_event(&event).unwrap();
let mut agents = vec![local_agent()];
apply_inbound_managed_agent(&mut agents, AGENT_PUBKEY, content);
apply_inbound_managed_agent(&mut agents, &[], AGENT_PUBKEY, content);
let a = &agents[0];
// Secrets / harness / runtime — every one preserved from the local record.
@@ -356,7 +356,7 @@ fn inbound_definition_less_agent_applies_quad() {
let content =
crate::managed_agents::agent_events::managed_agent_content_from_event(&event).unwrap();
let mut agents = vec![local_agent()];
apply_inbound_managed_agent(&mut agents, AGENT_PUBKEY, content);
apply_inbound_managed_agent(&mut agents, &[], AGENT_PUBKEY, content);
let a = &agents[0];
assert_eq!(a.persona_id, None);
@@ -376,7 +376,7 @@ fn inbound_managed_agent_no_match_is_noop() {
let content =
crate::managed_agents::agent_events::managed_agent_content_from_event(&event).unwrap();
let mut agents = vec![local_agent()];
apply_inbound_managed_agent(&mut agents, "someotheragentpubkey", content);
apply_inbound_managed_agent(&mut agents, &[], "someotheragentpubkey", content);
// No agent minted from a relay event — it would have no secret key.
assert_eq!(agents.len(), 1);
@@ -386,6 +386,177 @@ fn inbound_managed_agent_no_match_is_noop() {
);
}
// ── §2.8 canonical-linkage rule (kind:30177) ─────────────────────────────
/// A keyless definition (former persona) for the linkage resolver: `into_
/// agent_record` sets `slug = id`, and a projected one carries `library_ref`.
fn definition(slug: &str, projected: bool) -> ManagedAgentRecord {
let mut record = inbound_for(slug, "Definition").into_agent_record();
if projected {
record.library_ref = Some(format!("lib-{slug}"));
record.library_applied_revision = Some(1);
}
record
}
/// Inbound kind:30177 content carrying an explicit `persona_id` (or `None`).
/// Mirrors the wire shape `managed_agent_content_from_event` produces.
fn agent_content(name: &str, persona_id: Option<&str>) -> ManagedAgentEventContent {
ManagedAgentEventContent {
name: name.to_string(),
persona_id: persona_id.map(str::to_string),
system_prompt: None,
model: None,
provider: None,
persona_source_version: None,
parallelism: 4,
respond_to: crate::managed_agents::RespondTo::OwnerOnly,
respond_to_allowlist: vec![],
}
}
/// A local instance linked to `persona_id`, keyed by `AGENT_PUBKEY`.
fn linked_agent(persona_id: &str) -> ManagedAgentRecord {
let mut agent = local_agent();
agent.persona_id = Some(persona_id.to_string());
agent
}
/// An inbound event that would re-point a library-owned linkage is frozen:
/// `persona_id` stays on the local library-projected definition, safe fields
/// still apply, and the caller is told to converge the relay head back (§2.8).
#[test]
fn inbound_30177_freezes_repoint_of_library_owned_linkage() {
let definitions = vec![definition("shared-def", true)];
let mut agents = vec![linked_agent("shared-def")];
// Inbound tries to re-point the linkage to a different definition.
let outcome = apply_inbound_managed_agent(
&mut agents,
&definitions,
AGENT_PUBKEY,
agent_content("Renamed", Some("other-def")),
);
assert_eq!(
outcome,
InboundAgentLinkage::Frozen(LinkageFreezeReason::OwnedByLibrary),
"re-pointing a library-owned linkage must freeze",
);
let a = &agents[0];
assert_eq!(
a.persona_id,
Some("shared-def".to_string()),
"linkage must stay on the local library-owned definition",
);
assert_eq!(a.name, "Renamed", "safe per-instance fields still apply");
assert_eq!(a.parallelism, 4, "safe per-instance fields still apply");
}
/// An inbound event that would CLEAR a library-owned linkage
/// (`persona_id: None`) is frozen the same way — clearing is authorship too.
#[test]
fn inbound_30177_freezes_clear_of_library_owned_linkage() {
let definitions = vec![definition("shared-def", true)];
let mut agents = vec![linked_agent("shared-def")];
let outcome = apply_inbound_managed_agent(
&mut agents,
&definitions,
AGENT_PUBKEY,
agent_content("Renamed", None),
);
assert_eq!(
outcome,
InboundAgentLinkage::Frozen(LinkageFreezeReason::OwnedByLibrary),
);
assert_eq!(
agents[0].persona_id,
Some("shared-def".to_string()),
"a definition-less inbound must not clear a library-owned linkage",
);
}
/// An inbound event that would newly link a currently-plain instance to a
/// library-projected definition is frozen as an inadmissible new link — only
/// the Phase-4b coordinator may admit a projected link (§2.8, P6-C1 interim).
#[test]
fn inbound_30177_freezes_inadmissible_new_link_to_projected_definition() {
let definitions = vec![definition("shared-def", true)];
// Local instance is definition-less (plain).
let mut agents = vec![linked_agent("")];
agents[0].persona_id = None;
let outcome = apply_inbound_managed_agent(
&mut agents,
&definitions,
AGENT_PUBKEY,
agent_content("Renamed", Some("shared-def")),
);
assert_eq!(
outcome,
InboundAgentLinkage::Frozen(LinkageFreezeReason::InadmissibleNewLink),
"a new link to a projected definition must fail closed",
);
assert_eq!(
agents[0].persona_id, None,
"the inadmissible new link must not be authored",
);
}
/// A linkage change that touches only plain definitions applies as at head —
/// the §2.8 rule freezes ONLY library-owned or projected-target changes, never
/// an ordinary plain relink.
#[test]
fn inbound_30177_applies_plain_relink_unchanged() {
let definitions = vec![definition("plain-a", false), definition("plain-b", false)];
let mut agents = vec![linked_agent("plain-a")];
let outcome = apply_inbound_managed_agent(
&mut agents,
&definitions,
AGENT_PUBKEY,
agent_content("Renamed", Some("plain-b")),
);
assert_eq!(outcome, InboundAgentLinkage::Applied);
assert_eq!(
agents[0].persona_id,
Some("plain-b".to_string()),
"a plain→plain relink applies exactly as at head",
);
}
/// An inbound event that leaves the linkage unchanged is never frozen even when
/// the linked definition is library-projected — the definition quad is still
/// correctly omitted (linked), and safe fields apply. Freezing keys on a
/// linkage CHANGE, not on the linked definition's library status alone.
#[test]
fn inbound_30177_no_linkage_change_applies_even_when_projected() {
let definitions = vec![definition("shared-def", true)];
let mut agents = vec![linked_agent("shared-def")];
agents[0].system_prompt = Some("local prompt".to_string());
let outcome = apply_inbound_managed_agent(
&mut agents,
&definitions,
AGENT_PUBKEY,
agent_content("Renamed", Some("shared-def")),
);
assert_eq!(outcome, InboundAgentLinkage::Applied);
let a = &agents[0];
assert_eq!(a.persona_id, Some("shared-def".to_string()));
assert_eq!(a.name, "Renamed");
assert_eq!(
a.system_prompt,
Some("local prompt".to_string()),
"a linked inbound omits the definition quad — the local snapshot survives",
);
}
// ── Team (30176) inbound ─────────────────────────────────────────────────
const TEAM_ID: &str = "team-local-id";
@@ -561,6 +561,31 @@ impl MutationRoute {
}))
}
/// The route for the DEFINITION a keyed instance links to — the read side
/// of the §2.8 definition–instance relation resolver. An instance's
/// `persona_id` IS the linked definition's slug (they are assigned in
/// lockstep: `into_agent_record` sets `slug = id`, and every linked
/// instance carries `persona_id == definition.slug`). This is the ONE
/// canonical join every library mechanism uses to discover an
/// instance↔definition relationship — the inbound kind:30177 canonical-
/// linkage rule consults it to decide whether an instance's linkage is
/// library-owned; nothing re-derives the `persona_id` join ad hoc.
///
/// A definition-less instance (`persona_id == None`) links to no definition
/// and is always [`Plain`](Self::Plain), as is a `persona_id` that resolves
/// to a plain definition or to no definition at all (a create, or a stale
/// link). Only a `persona_id` resolving to a `library_ref`-carrying
/// definition is [`LibraryProjected`](Self::LibraryProjected).
pub(crate) fn for_linked_definition(
definitions: &[ManagedAgentRecord],
persona_id: Option<&str>,
) -> Self {
match persona_id {
Some(slug) => Self::for_slug(definitions, slug),
None => Self::Plain,
}
}
/// Refuse a plain-path mutation whose target `slug` resolves to a
/// library-projected record (§2.7). The command boundaries that key by slug
/// — `delete_persona` and snapshot/team import — call this on the RAW
@@ -649,6 +649,67 @@ fn mutation_route_for_persona_d_tag_catches_projected_team_slug_that_for_slug_mi
);
}
/// §2.8 relation-resolver read side: `for_linked_definition` classifies an
/// instance's linkage by resolving its `persona_id` against the raw keyless
/// definition store. A `persona_id` pointing at a projected definition is
/// `LibraryProjected`; one pointing at a plain definition, at no definition (a
/// stale link), or `None` (a definition-less instance) is `Plain`. This is the
/// canonical join the inbound kind:30177 rule consults — the same `persona_id →
/// slug` derivation every library mechanism uses, resolved one way.
#[test]
fn for_linked_definition_classifies_projected_plain_absent_and_none() {
let definitions = vec![
projected_record("shared", 3),
custom_persona("custom:plain", "Plain").into_agent_record(),
];
// Instance linked to a projected definition → LibraryProjected.
assert_eq!(
super::MutationRoute::for_linked_definition(&definitions, Some("shared")),
super::MutationRoute::LibraryProjected,
);
// Instance linked to a plain definition → Plain.
assert_eq!(
super::MutationRoute::for_linked_definition(&definitions, Some("custom:plain")),
super::MutationRoute::Plain,
);
// Instance whose persona_id resolves to no definition (a stale link) → Plain.
assert_eq!(
super::MutationRoute::for_linked_definition(&definitions, Some("does-not-exist")),
super::MutationRoute::Plain,
);
// Definition-less instance (`persona_id == None`) → Plain.
assert_eq!(
super::MutationRoute::for_linked_definition(&definitions, None),
super::MutationRoute::Plain,
);
}
/// §2.8 resolver vs. `persona_d_tag`: the instance→definition join keys on the
/// definition's UUID `slug` (== the instance `persona_id`), NOT the team-
/// derived d-tag. A team-sourced projected definition whose `persona_id`/`slug`
/// is a UUID but whose d-tag is the pack slug must still resolve as projected
/// through `for_linked_definition` — the resolver and the inbound d-tag
/// preflight key on different fields for different callers, and this pins that
/// the linkage join uses the slug.
#[test]
fn for_linked_definition_keys_on_slug_not_persona_d_tag() {
let mut definition = projected_record("uuid-shared", 5);
definition.source_team_persona_slug = Some("codereviewer".to_string());
let definitions = vec![definition];
// The instance's `persona_id` is the definition's UUID slug — resolves.
assert_eq!(
super::MutationRoute::for_linked_definition(&definitions, Some("uuid-shared")),
super::MutationRoute::LibraryProjected,
);
// The team d-tag is NOT the slug — it does not resolve the linkage join.
assert_eq!(
super::MutationRoute::for_linked_definition(&definitions, Some("codereviewer")),
super::MutationRoute::Plain,
);
}
/// The fingerprint narrowing (F3): a plain persona save may freely change a
/// projected record's SCOPE-LOCAL fields (`is_active`, `env_vars`) — these are
/// not library-authoritative, so the shared fingerprint is unchanged and the