Skip to content

ADR-002: Identity Model Evolution

  • Status: Accepted (Phase 1 implemented in core)
  • Date: 2026-04-11
  • Decision Makers: Platform team
  • Context Area: Domain — Identity & Access bounded context

Terminology — OBO: On-behalf-of (OBO) means delegated access: one identity acts for another (e.g. an agent for an end user). The abbreviation is widely used in OAuth and enterprise identity (including “on-behalf-of” token flows). In this ADR, OBO refers to that authorization pattern (intersecting grants for actor and delegator), not to any one vendor’s OAuth API. For non-technical readers: read OBO as on-behalf-of (delegated) access.

Astrocyte’s current identity model is a single opaque string:

@dataclass
class AstrocyteContext:
principal: str # e.g. "agent:support-bot-1", "user:calvin"

This was sufficient for single-agent, single-user deployments. v1.0.0 introduces requirements that the opaque string cannot support:

  1. On-Behalf-Of (OBO) delegation: Agents act on behalf of users. When agent:support-bot acts for user:calvin, the effective permissions must be the intersection of both principals’ grants. A single principal field cannot represent two identities simultaneously.

  2. Permission intersection: Current permissions are additive per principal. With OBO, we need effective = agent_grants INTERSECT user_grants to prevent privilege escalation. This requires knowing which identity is the actor and which is the delegator.

  3. Structured claims: External IdPs provide JWT claims and OIDC attributes (roles, groups, tenant membership). The opaque string discards this information, forcing downstream systems to re-resolve it.

  4. Multi-tenancy: Standalone gateway deployment serves multiple tenants. Tenant isolation requires a tenant_id on every request context, not encoded inside the principal string.

  5. Integration migration: All 19 framework integrations currently pass no context at all. Any identity model change must work when no context is provided (the zero-context case) and allow incremental migration.

Evolve AstrocyteContext from an opaque principal string to a structured identity context, using a three-phase backwards-compatible migration.

@dataclass
class ActorIdentity:
type: str # "user" | "agent" | "service"
id: str # unique within type
claims: dict[str, str] | None = None # JWT/OIDC claims
@dataclass
class AstrocyteContext:
# Phase 1: principal remains primary, new fields optional
principal: str # backwards-compatible
actor: ActorIdentity | None = None
on_behalf_of: ActorIdentity | None = None
tenant_id: str | None = None
IF actor is provided:
Use actor directly.
principal is available for logging but not identity resolution.
ELIF principal is provided:
Parse convention: "agent:X" -> ActorIdentity(type="agent", id="X")
"user:X" -> ActorIdentity(type="user", id="X")
"service:X" -> ActorIdentity(type="service", id="X")
"X" (no prefix) -> ActorIdentity(type="user", id="X")
ELSE:
Anonymous context. Default policy applies (owner_only -> deny, open -> allow).
effective_permissions(context, bank_id) =
actor_grants = grants_for(context.actor, bank_id)
IF context.on_behalf_of:
obo_grants = grants_for(context.on_behalf_of, bank_id)
RETURN actor_grants INTERSECT obo_grants
ELSE:
RETURN actor_grants
PhaseVersionChangeBreaking?
1v0.5.0 (M1; same tag as M2 / ADR-003) / targets v1.0.0Add optional actor, on_behalf_of, tenant_id to AstrocyteContext. principal remains the primary field. Internal code uses actor if present, falls back to parsing principal. OBO = permission intersection per bank in ACL.No
2v1.1.0Integrations updated to pass structured context. principal becomes computed from actor when actor is set. Deprecation warning emitted when actor is None but principal is set.No
3v2.0.0principal field removed. actor becomes required. AstrocyteContext(actor=...) is the only constructor.Yes

Shipped in astrocyte Python core:

  • Types: ActorIdentity, extended AstrocyteContext (actor, on_behalf_of, tenant_id, optional claims on ActorIdentity).
  • ACL: effective_permissions with OBO intersection; BankResolver + optional identity.auto_resolve_banks for recall/reflect when no bank_id / banks are passed (see ADR-003 identity subsection for config keys).
  • Not in core yet: validating JWT/OIDC or driving policy from claims; multi-tenant enforcement using tenant_id (field reserved for callers and future gateway work). Integration adapters still migrate incrementally (e.g. optional context on selected integrations).

Alternative A: Replace principal immediately

Section titled “Alternative A: Replace principal immediately”

Remove principal: str, make actor: ActorIdentity required in v1.0.0.

Rejected because: Breaks all 19 integrations and every existing user simultaneously. The migration burden is too high for a v1.0.0 release. Users who just pip install astrocyte and pass principal="user:me" would get immediate TypeError.

Alternative B: Separate IdentityContext object

Section titled “Alternative B: Separate IdentityContext object”

Create a new IdentityContext class and pass it alongside AstrocyteContext as a separate parameter on every API method.

Rejected because: Doubles the number of parameters on every API method (retain(request, context, identity)). The identity IS part of the request context — separating them creates an artificial boundary. Every call site must remember to pass both objects.

Alternative C: Keep opaque strings, encode OBO in string format

Section titled “Alternative C: Keep opaque strings, encode OBO in string format”

Use principal="agent:support-bot|obo:user:calvin" — encode delegation in the string.

Rejected because: Requires custom parsing at every consumption point. No type safety. Easy to construct malformed strings. Claims (JWT attributes) cannot be represented. This is primitive obsession elevated to an architecture anti-pattern.

  • OBO delegation enables secure agent-on-behalf-of-user workflows without privilege escalation.
  • Structured claims enable fine-grained policy decisions (role-based, group-based, attribute-based) without re-resolving against the IdP (consumption in core policy is future; claims is carried on ActorIdentity for adapters).
  • Three-phase migration ensures no breaking change for Phase 1 adopters.
  • tenant_id field unblocks standalone gateway multi-tenancy.
  • AstrocyteContext grows from 1 field to 4. Complexity increases.
  • Integration adapters must be updated (19 adapters, Phase 2). This is labor-intensive but can be parallelized.
  • Two code paths coexist during Phase 1-2: “parse principal” and “use actor directly”. Unit tests must cover both paths.
  • Risk: Integration authors (community contributors) may not update their adapters promptly, extending the Phase 1 period indefinitely. Mitigation: Provide a helper function AstrocyteContext.from_principal(s) that handles parsing, making the structured path easy to adopt. Emit deprecation warnings in Phase 2 to create urgency.
  • Risk: Permission intersection logic is more complex than additive grants. Bugs in intersection could either over-permit (security vulnerability) or under-permit (broken functionality). Mitigation: Comprehensive Given/When/Then test suite for permission evaluation covering all combinations: actor-only, OBO with overlapping grants, OBO with disjoint grants, wildcard grants, no grants.