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
Context
Section titled “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:
@dataclassclass 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:
-
On-Behalf-Of (OBO) delegation: Agents act on behalf of users. When
agent:support-botacts foruser:calvin, the effective permissions must be the intersection of both principals’ grants. A singleprincipalfield cannot represent two identities simultaneously. -
Permission intersection: Current permissions are additive per principal. With OBO, we need
effective = agent_grants INTERSECT user_grantsto prevent privilege escalation. This requires knowing which identity is the actor and which is the delegator. -
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.
-
Multi-tenancy: Standalone gateway deployment serves multiple tenants. Tenant isolation requires a
tenant_idon every request context, not encoded inside the principal string. -
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.
Decision
Section titled “Decision”Evolve AstrocyteContext from an opaque principal string to a structured identity context, using a three-phase backwards-compatible migration.
Structured AstrocyteContext
Section titled “Structured AstrocyteContext”@dataclassclass ActorIdentity: type: str # "user" | "agent" | "service" id: str # unique within type claims: dict[str, str] | None = None # JWT/OIDC claims
@dataclassclass 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 = NoneIdentity Resolution Logic
Section titled “Identity Resolution Logic”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).Permission Evaluation
Section titled “Permission Evaluation”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_grantsMigration Phases
Section titled “Migration Phases”| Phase | Version | Change | Breaking? |
|---|---|---|---|
| 1 | v0.5.0 (M1; same tag as M2 / ADR-003) / targets v1.0.0 | Add 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 |
| 2 | v1.1.0 | Integrations 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 |
| 3 | v2.0.0 | principal field removed. actor becomes required. AstrocyteContext(actor=...) is the only constructor. | Yes |
Implementation status (Phase 1)
Section titled “Implementation status (Phase 1)”Shipped in astrocyte Python core:
- Types:
ActorIdentity, extendedAstrocyteContext(actor,on_behalf_of,tenant_id, optionalclaimsonActorIdentity). - ACL:
effective_permissionswith OBO intersection;BankResolver+ optionalidentity.auto_resolve_banksfor recall/reflect when nobank_id/banksare passed (see ADR-003identitysubsection for config keys). - Not in core yet: validating JWT/OIDC or driving policy from
claims; multi-tenant enforcement usingtenant_id(field reserved for callers and future gateway work). Integration adapters still migrate incrementally (e.g. optionalcontexton selected integrations).
Alternatives Considered
Section titled “Alternatives Considered”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.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- 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;
claimsis carried onActorIdentityfor adapters). - Three-phase migration ensures no breaking change for Phase 1 adopters.
tenant_idfield unblocks standalone gateway multi-tenancy.
Negative
Section titled “Negative”AstrocyteContextgrows 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.