LearnNewsExamplesServices
Frontmatter
id15027
titleConsolidate agent-identity @-normalization onto one shared canonicalizer
stateClosed
labels
enhancementairefactoringarchitecturemodel-experience
assigneesneo-gpt
createdAt1:06 PM
updatedAt2:43 PM
githubUrlhttps://github.com/neomjs/neo/issues/15027
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAt2:43 PM

Consolidate agent-identity @-normalization onto one shared canonicalizer

Closed Backlog/active-chunk-5 enhancementairefactoringarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on 1:06 PM

Context

Agent-identity matching — mailbox routing, AUTHORED_BY / OWNED_BY edges, wake/heartbeat targeting — rests on one convention: the canonical AgentIdentity graph-node id is @-prefixed (@neo-opus-ada), while the input handle (env NEO_AGENT_IDENTITY, GitHub login, or cloud OIDC/PAT userId) is bare. Normalization = add the @. But that convention is currently re-implemented by ~4 independent code paths, and the correctness-critical comparison sites do not normalize at all — they trust every upstream path to stay byte-for-byte in lockstep. This consolidates them onto one shared, provider-agnostic canonicalizer.

Surfaced while diagnosing a MailboxService.markRead "Unauthorized: you are not the recipient" error: that specific failure was an unrelated transient graph-projection visibility lag (self-heals on retry), but the investigation exposed that the read-path compare is a raw edgeTarget === me with no normalization. #10259 documents that exactly this class of @-prefix / alias drift has caused real mailbox mis-routing before — so hardening the boundary is well-motivated, not speculative.

The Problem

Four independent @-canonicalization mechanisms, converging on @-prefixed by lockstep luck rather than a shared contract:

  1. normalizeAgentIdentityNodeId()ai/scripts/lifecycle/harnessRouting.mjs:44!startsWith('@') ? '@'+v : v. Used by orchestrator, wake daemon, swarm-heartbeat, kb-alerting.
  2. normalizeMailboxTarget()ai/services/memory-core/MailboxService.mjs:98 — the mailbox send path (handles @me, AGENT:* sentinel, AGENT: strip, @@ strip, bare→@).
  3. Hardcoded '@' + loginai/mcp/server/memory-core/Server.mjs bindAgentIdentity (~:619) — the mailbox/graph auth-resolve path (me), shared by BOTH the local stdio (GitHub login) and cloud SSE (OIDC / PAT userId) identity paths.
  4. bare() / normalizeUserId — strip @ for bare contexts (ai/graph/assertExpectedIdentity.mjs:45, etc.).

The read-path comparison (markRead MailboxService.mjs:1711 edgeTarget === me, plus siblings) does no normalization — it relies entirely on (2) and (3) emitting identical @-prefixed strings. Any single drift (a bare target, an AGENT: form, a @@ typo, a resolver that skips the prefix) → a silent "not the recipient" for a message the agent owns: a confusing, high-diagnostic-cost Model-Experience failure.

The Architectural Reality

  • Canonical form: @-prefixed AgentIdentity node id (#10144 convention; #10232 self-seed; #10259 SQLite inventory). Bare = the input handle only.
  • Deployment-topology constraint — resolution must be provider-agnostic across both supported topologies (aligned-with ADR 0014):
    • Local (single-machine graph + orchestrator daemon): GitHub login via StdioIdentityResolverresolveStdioIdentity (Server.mjs:631) → bindAgentIdentity(githubLogin).
    • Cloud (containerized): OIDC / PAT userId via buildRequestContext(reqAuth) (Server.mjs:419) → bindAgentIdentity(reqAuth.userId) (:424; PAT provisioning at :520 / authProvider :555).
    • Both already funnel through bindAgentIdentity — the natural single chokepoint. Its hardcoded '@' + login must become the shared canonicalizer so a bare PAT/OIDC userId canonicalizes identically to a bare GitHub login, with no provider special-casing.
  • Compare sites: markRead (:1711), deleteMessage, and any edgeTarget === me / recipient checks in MailboxService + PermissionService.

The Fix

  1. Elevate one shared canonicalizer as the single source of truth for the @-prefix convention, in a graph/identity-appropriate module (it currently lives in ai/scripts/lifecycle/harnessRouting.mjs — an odd home for a graph-identity primitive; validate the target placement via structural-pre-flight).
  2. Route ALL sites through it: bindAgentIdentity's '@'+login (both stdio + SSE paths), normalizeMailboxTarget's bare→@ branch, and the read-path compares (normalize BOTH me and edgeTarget before ===).
  3. Preserve normalizeMailboxTarget's richer target grammar (@me, AGENT:* sentinel, AGENT:<family>/<model> alias resolution, role: / human: passthrough, @@-strip) — share only the prefix primitive; keep the mailbox-specific grammar in the mailbox layer.
  4. Provider-agnostic: no GitHub-vs-PAT/OIDC branching in the canonicalizer.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
shared identity canonicalizer (relocated normalizeAgentIdentityNodeId) #10144 @-convention single canonicalizer; bare→@, idempotent, provider-agnostic non-string passthrough unchanged helper JSDoc unit: bare / @ / @@ / AGENT: inputs
bindAgentIdentity (me resolve) Server auth (stdio + SSE) shared helper for GitHub AND PAT/OIDC logins null node → unbound (unchanged) method JSDoc local + cloud resolution specs
markRead / read-path compare MailboxService normalize BOTH sides before === no match → not-recipient (unchanged) method JSDoc drift-witness specs (bare / @@ / AGENT:)
normalizeMailboxTarget send path MailboxService shares the prefix primitive; keeps richer grammar invalid target rejected (unchanged) function JSDoc existing send specs + new

Decision Record impact

aligned-with #10144 (@-convention), aligned-with ADR 0014 (deployment topology — local vs cloud identity handling must not diverge), builds-on #10259 (prior alias/prefix normalization). No ADR amendment required.

Acceptance Criteria

  • Exactly one shared canonicalizer owns the @-prefix convention, in a graph/identity-appropriate module (not scripts/lifecycle); placement validated via structural-pre-flight.
  • bindAgentIdentity resolves me through the shared helper for BOTH the stdio/GitHub and SSE/OIDC-PAT paths — a bare login canonicalizes identically regardless of auth provider (no provider branching).
  • The mailbox read-path recipient comparison (markRead + siblings) normalizes BOTH me and the edge target before comparing; a @ / AGENT: / @@ / bare-drifted target still resolves to the correct recipient.
  • normalizeMailboxTarget's existing grammar (@me, AGENT:*, AGENT:<family>/<model>, role: / human:, @@-strip) is preserved — no send-path regression.
  • Regression witnesses for BOTH topologies: local GitHub-login resolution AND cloud OIDC/PAT-login resolution each bind to the canonical @-node and round-trip a mailbox send → markRead.
  • No net-new duplicate canonicalizer; the inline '@'+ / bare→@ reimplementations are removed in favor of the shared helper.

Out of Scope

  • The transient graph-projection visibility lag that surfaced this (separate cross-generation/cache concern; self-heals on retry).
  • Alias-node merge / test-fixture purge (#10259, done).
  • Changing the canonical form (stays @-prefixed) or the AGENT:* broadcast semantics.
  • Chroma metadata userId normalization (separate layer, per #10259 out-of-scope).

Avoided Traps

  • "Just normalize markRead" — insufficient; leaves four divergent canonicalizers to drift again. The consolidation is the fix.
  • "Branch on auth provider (GitHub vs PAT/OIDC)" — wrong; the canonicalizer must be provider-agnostic or the local and cloud identity paths diverge.
  • "Fold everything into normalizeMailboxTarget" — it carries mailbox-specific target grammar (AGENT:<family>/<model>, role:) that does not belong in a generic identity canonicalizer; share the prefix primitive, keep the grammar in the mailbox layer.

Related

Builds-on #10259 · #10144 · #10232 · aligned-with ADR 0014

Origin Session ID: 01f4cc68-8b8e-43e6-b51c-55b4f421f4e0

Retrieval Hint: "agent identity @-prefix canonicalization consolidation normalizeAgentIdentityNodeId bindAgentIdentity markRead provider-agnostic local cloud"

Live latest-open sweep: checked open issues + targeted identity normalize canonical @ prefix search at 2026-07-11T10:5xZ; no equivalent open ticket found.

tobiu referenced in commit 5d2cd01 - "fix(agent-os): centralize AgentIdentity node-id canonicalization (#15027) (#15032)" on 2:43 PM
tobiu closed this issue on 2:43 PM