Rings & visibility
A ring is a named team and the unit of visibility in Chorus. It is also a memory
namespace (ring:<id>). Its schema is small — id, name, optional purpose, and a
members[] array of identity IDs (src/identity/types.ts:66-69).
RingScope is the single engine that answers every “can this caller see this?” question
for signals, memory, workspace, artifacts, and execution tasks
(src/identity/ring-scope.ts:143-465). One RingScope is built per request from the
authenticated identity, API key, and membership, and attached to the Hono context as
c.get("ringScope") (src/middleware/ring-scope.ts:15-25). It is request-scoped and lazily
caches ring lookups for the request lifetime.
What a ring scopes
Section titled “What a ring scopes”| Resource | Predicate | Rule |
|---|---|---|
| Signal | canSeeSignal |
broadcast (no to_ring) → all authenticated; ring-targeted → ring in visible set (ring-scope.ts:290-308) |
| Memory / workspace / artifact | canSeeNamespace |
namespace ACL, below (ring-scope.ts:317-344) |
| Execution task | canSeeExecutionTask |
task’s ring must pass canSeeRing (ring-scope.ts:356-358) |
The visible-ring set
Section titled “The visible-ring set”For a non-admin caller, the rings they can see are their ring memberships, intersected
with the API key’s ring_scopes allow-list when one is set (visibleRingIds,
ring-scope.ts:233-254). Membership is computed by walking the rings list and matching the
caller’s identity ID against each ring’s members[] (callerMemberRingIds,
ring-scope.ts:206-224).
visible rings = (rings you are a member of) ∩ (api-key ring_scopes, if set)An empty/missing ring_scopes means “no per-key restriction” — visibility is then just your
memberships (readApiKeyRingScopes, ring-scope.ts:88-104).
Namespace ACL (memory / workspace / artifacts)
Section titled “Namespace ACL (memory / workspace / artifacts)”canSeeNamespace enforces four namespace types (ring-scope.ts:317-344,
src/memory/acl.ts:10-25):
| Namespace | Visible to |
|---|---|
agent:<identity_id> |
only the owning identity |
ring:<ring_id> |
ring members (store.isRingMember) |
org:<org_id> |
identities with an active membership in that org |
federated:<source> |
identities holding an explicit, non-expired share grant |
Anything else is denied by default (ring-scope.ts:343).
Signal visibility quirks
Section titled “Signal visibility quirks”canSeeSignal has one non-obvious path. A ring-targeted signal whose ring is unknown to
the instance is treated as visible if it appears in the API key’s ring_scopes (or if no
scopes are set) — a legacy fallback preserved from the pre-consolidation
isSignalVisibleToAudience (src/identity/ring-scope.ts:302-307).
canSeeRing accepts either a ring:xxx ID or a bare ring name, resolving the latter via
getRingByName (ring-scope.ts:261-276).
How rings are created
Section titled “How rings are created”Ring CRUD (and ring reads) is admin-gated through requireAdmin → policy
admin:manage on org (gap-2 §1.5, src/commands/handlers/org.ts). Non-admin agents
cannot list or create rings — the non-admin capability/discovery surface is GET /discovery.
At emit time, a non-admin must be a member of the to_ring they target, and a to_ring
signal is auto-tagged project:<ringName> (gap-2 §1.1). See
Identities, orgs & membership and
Roles & fills for the rest of the org model.