Skip to content

Access control setup

Practical guide to configuring principals, grants, and per-bank permissions. Authentication identifies who is calling — access control decides what they can do.


Access control is disabled by default. When disabled, all operations succeed regardless of context.

access_control:
enabled: true
default_policy: owner_only # "owner_only" | "open" | "deny"
PolicyNo context (anonymous)Context with no matching grant
owner_onlyDeniedDenied
openAllowedAllowed
denyDeniedDenied

Recommendation: Use deny for production (explicit grants only) or owner_only for multi-tenant setups where users own their banks.


PermissionOperations allowed
readrecall(), reflect()
writeretain()
forgetforget() with specific memory_ids or tags
adminforget(scope="all"), bank config, export/import
*All of the above

Apply across the entire instance. Use bank_id: "*" for all banks, or a specific bank ID.

access_grants:
# Alice has full control of her bank
- bank_id: "user-alice"
principal: "user:alice"
permissions: [read, write, forget, admin]
# Support bot can read and write to any shared bank
- bank_id: "shared-*"
principal: "agent:support-bot"
permissions: [read, write]
# Everyone can read the public bank
- bank_id: "public"
principal: "*"
permissions: [read]
# Admin has full access everywhere
- bank_id: "*"
principal: "user:admin"
permissions: ["*"]

Define access directly on a bank — useful for bank-specific overrides:

banks:
team-engineering:
access:
- principal: "user:*"
permissions: [read, write]
- principal: "agent:code-reviewer"
permissions: [read]
sensitive-data:
access:
- principal: "user:compliance-officer"
permissions: [read, forget, admin]

Top-level access_grants and per-bank banks.*.access are merged — both contribute grants. There’s no precedence; all grants are unioned together.

Register agents with bank access and optional rate hints. This generates implicit grants:

agents:
ingester:
principal: "agent:ingester"
banks: [raw-data, processed-data]
permissions: [write]
analyst:
principal: "agent:analyst"
banks: [processed-data, reports]
permissions: [read]
default_bank: processed-data
admin-bot:
principal: "agent:admin-bot"
banks: ["*"] # all banks
permissions: [read, write, forget, admin]
max_retain_per_minute: 120
max_recall_per_minute: 240

If permissions is omitted, agents default to [read, write].


Principals follow the type:id format:

TypeExampleUse for
user:user:alice, user:u-12345Human users
agent:agent:support-bot, agent:ingesterAI agents and automated services
service:service:etl-workerBackend services
**Any principal (wildcard)

In dev and api_key auth modes, the client sets the principal via the X-Astrocyte-Principal header. In jwt_oidc mode, the principal is computed from JWT claims. See authentication setup.


When an operation is called (e.g. brain.retain(content, bank_id="b1", context=ctx)):

  1. If access_control.enabled is falseallow (skip all checks)
  2. If context is None (anonymous):
    • open policy → allow
    • owner_only or denydeny (403)
  3. If context is provided:
    • Collect all grants where bank_id matches ("*" or exact) and principal matches ("*" or exact)
    • Union all matching permissions
    • If the required permission is in the set → allow
    • If not, and policy is openallow
    • Otherwise → deny (403)
access_grants:
- bank_id: "*"
principal: "user:alice"
permissions: [read]
- bank_id: "user-alice"
principal: "user:alice"
permissions: [read, write, forget, admin]
OperationBankResultWhy
recall()user-aliceAllowedread from both grants
retain()user-aliceAllowedwrite from bank-specific grant
retain()other-bankDeniedOnly read from wildcard grant
recall()other-bankAllowedread from wildcard grant
forget(scope="all")user-aliceAllowedadmin from bank-specific grant
forget(scope="all")other-bankDeniedNo admin on wildcard grant

When an agent acts on behalf of a user, effective permissions are the intersection of both principals’ grants. This prevents privilege escalation.

Enable OBO:

identity:
obo_enabled: true
access_grants:
- bank_id: "shared"
principal: "agent:support-bot"
permissions: [read, write, forget]
- bank_id: "shared"
principal: "user:alice"
permissions: [read]

When support-bot acts on behalf of alice:

ctx = AstrocyteContext(
actor=ActorIdentity(type="agent", id="support-bot"),
on_behalf_of=ActorIdentity(type="user", id="alice"),
)
# Agent permissions: {read, write, forget}
# Alice permissions: {read}
# Intersection: {read}
await brain.recall("query", bank_id="shared", context=ctx) # Allowed (read)
await brain.retain("data", bank_id="shared", context=ctx) # Denied (write not in intersection)

The agent cannot write on behalf of Alice because Alice only has read permission — even though the agent itself has write.


Automatically resolve bank IDs from the caller’s identity:

identity:
auto_resolve_banks: true
user_bank_prefix: "user-"
agent_bank_prefix: "agent-"
service_bank_prefix: "service-"
resolver: convention

With convention-based resolution, a user with actor.id = "alice" gets a default bank of user-alice. An agent with actor.id = "ingester" gets agent-ingester.


When access is denied, the gateway returns 403 Forbidden:

{
"detail": "Principal 'agent:bot' denied 'write' on bank 'bank-1'"
}

The error message includes the principal, the denied permission, and the bank — useful for debugging grant configuration.


Each user owns their own bank. No cross-access without explicit grants.

access_control:
enabled: true
default_policy: owner_only
identity:
auto_resolve_banks: true
resolver: convention
access_grants:
# Users get full access to their own bank (convention: user-{id})
- bank_id: "*"
principal: "*"
permissions: [read, write, forget]

With owner_only policy, this only grants access when the bank ID matches the principal’s convention bank. A user user:alice can access user-alice but not user-bob.

A team shares a bank; individual agents have limited access.

access_grants:
# All team members can read and write
- bank_id: "team-engineering"
principal: "user:*"
permissions: [read, write]
# Only the lead can delete
- bank_id: "team-engineering"
principal: "user:team-lead"
permissions: [read, write, forget, admin]
# CI bot can write but not read
- bank_id: "team-engineering"
principal: "agent:ci-bot"
permissions: [write]

An analytics agent can read from multiple banks but never write or delete.

agents:
analytics:
principal: "agent:analytics"
banks: [user-*, team-*, shared-*]
permissions: [read]

A compliance officer can read and purge any bank. Regular users cannot forget.

access_control:
enabled: true
default_policy: deny
access_grants:
- bank_id: "*"
principal: "user:compliance-officer"
permissions: [read, forget, admin]
- bank_id: "*"
principal: "user:*"
permissions: [read, write]

Terminal window
# This should succeed (alice has read on her bank)
curl -X POST http://localhost:8080/v1/recall \
-H "Content-Type: application/json" \
-H "X-Api-Key: $API_KEY" \
-H "X-Astrocyte-Principal: user:alice" \
-d '{"query": "preferences", "bank_id": "user-alice"}'
# This should return 403 (alice has no write on other-bank)
curl -X POST http://localhost:8080/v1/retain \
-H "Content-Type: application/json" \
-H "X-Api-Key: $API_KEY" \
-H "X-Astrocyte-Principal: user:alice" \
-d '{"content": "test", "bank_id": "other-bank"}'
from astrocyte import Astrocyte
from astrocyte.types import AstrocyteContext, ActorIdentity
from astrocyte.errors import AccessDenied
brain = Astrocyte.from_config("astrocyte.yaml")
ctx = AstrocyteContext(
principal="user:alice",
actor=ActorIdentity(type="user", id="alice"),
)
# Should succeed
await brain.recall("test", bank_id="user-alice", context=ctx)
# Should raise AccessDenied
try:
await brain.retain("test", bank_id="other-bank", context=ctx)
except AccessDenied as e:
print(f"Denied: {e}") # "Principal 'user:alice' denied 'write' on bank 'other-bank'"