Security model
Chorus is single-org per instance — there is no org-vs-org tenancy boundary to configure. Inside one instance, three primitives do the actual isolation: rings (visibility scope), namespace ACL (per-resource ownership), and federation peer scopes (cross-instance). The policy engine is real but coarse — it is not the isolation story on its own.
This page is generated from the code; claims point at file:line.
The policy engine is coarse on purpose
Section titled “The policy engine is coarse on purpose”evaluatePolicy(action, subject, resource) runs five rules in order
(src/policy/engine.ts:43-114): admin bypass → active-membership required → service-claw denied
admin:manage → non-admin denied admin:manage → default-allow everything else. The last rule
means any active org member passes every non-admin action (signal:*, task:*, memory:*,
workstream:*) at the engine level.
The fine-grained gating happens outside the engine. Don’t read evaluatePolicy as the whole
authorization picture — read it as the admin gate plus a membership check.
Layer 1 — RingScope (visibility)
Section titled “Layer 1 — RingScope (visibility)”RingScope (src/identity/ring-scope.ts) is the single authority for “which rings can this
identity see,” and it gates signals, execution tasks, and namespaces. Build one per request; the
middleware attaches it as c.get("ringScope") (src/middleware/ring-scope.ts).
The effective visible-ring set (visibleRingIds, ring-scope.ts:233-254):
| Caller | Visible rings |
|---|---|
Admin (is_admin) |
every known ring on the instance |
| Non-admin | rings they are a member of |
Non-admin + API-key ring_scopes |
memberships ∩ ring_scopes |
Signal visibility (canSeeSignal, ring-scope.ts:290-308): broadcast signals (no to_ring) are
visible to any authenticated caller; ring-targeted signals only when the ring is in the caller’s
visible set. Execution tasks gate the same way (canSeeExecutionTask, ring-scope.ts:356). See
Rings & visibility.
Layer 2 — namespace ACL
Section titled “Layer 2 — namespace ACL”Memory, workspace, and artifact items live in namespaces. canSeeNamespace
(ring-scope.ts:317-344; public wrappers in src/memory/acl.ts:43-96) enforces four shapes:
| Namespace | Access rule |
|---|---|
agent:<id> |
only the owning identity — private even from admins |
ring:<id> |
active members of that ring |
org:<id> |
identities with active membership in that org |
federated:<…> |
identities with an explicit, non-expired share grant |
Unknown formats are denied by default (acl.ts:16).
Layer 3 — federation (cross-instance)
Section titled “Layer 3 — federation (cross-instance)”Inbound federation routes are absent by default. They are mounted only when an operator sets
FEDERATION_ENABLED=true. Startup then requires an exact canonical public origin and a 32–4096
byte encryption-key file; invalid authority or key configuration aborts before database access.
Federation is v2-only. Its HMAC-SHA256 canonical form binds version, receiver-assigned peer id, key id/generation, method, public authority, raw path/query, RFC 9530 digest of the exact guarded bytes, database-time validity window, and nonce. Legacy/missing/ambiguous versions and v1 proof shapes are rejected before trust lookup. Peer keys are AES-256-GCM encrypted at rest with peer+generation AAD; only current and bounded previous generations verify.
After verification, one SurrealDB transaction claims a peer+hashed-nonce, applies the mutation or durable rejection, records dedicated provenance, and creates a signed receipt. Exact retries recover that receipt; different requests using the nonce fail. Scope checks cover action, signal type, ring, identity, urgency, and peer ownership of task resources.
Egress boundary — attacker-controlled HTTP destinations
Section titled “Egress boundary — attacker-controlled HTTP destinations”Webhook URLs, federation peer endpoints, configured SIWE/ERC RPC endpoints, and ERC-8004 registration URIs all pass through one process-wide safe outbound client. It normalizes HTTP(S) URLs, rejects credentials/fragments/ambiguous syntax and caller-controlled authority or framing headers, resolves the complete A/AAAA set, and rejects every non-global IANA special-purpose destination. Mixed public/private answers fail even when the private address would otherwise be operator-allowlisted.
For an admitted request, the transport connects directly to one of the vetted IPs while retaining
the original Host header and TLS SNI/certificate identity. It does not use HTTP_PROXY /
HTTPS_PROXY, does not re-resolve inside the transport, and does not follow redirects. DNS is
performed again for each delivery, so a URL accepted during create/update is checked against its
current addresses immediately before network I/O. One deadline and shared concurrency budget cover
DNS through the fully consumed response; URL, headers, request body, response body, and DNS answer
count are independently bounded.
Private connectivity is an explicit operator exception: CHORUS_OUTBOUND_PRIVATE_ALLOWLIST
binds an exact origin to private CIDRs in RFC1918, RFC6598, loopback, or ULA ranges. It cannot
enable link-local/cloud metadata destinations, and it cannot admit a mixed public/private answer.
Public error responses expose stable OUTBOUND_* codes without echoing destination queries,
response bodies, or low-level causes.
ERC-8004 treats chain ownership as live authorization: ownerOf and tokenURI are read on every
resolve. Only bounded, schema-validated registration metadata is cached by the current URI; a cache
hit cannot let a previous or incorrect wallet skip the ownership check. Its JSON-RPC client also
requires an exact JSON-RPC 2.0 response ID and disables automatic retries.
Known caveats
Section titled “Known caveats”The isolation primitives above are sound, but several edges are honest limitations. Document them, don’t paper over them.
See Rings & visibility, Memory, and Identities, orgs & membership.