Matrix logo

Scope

Package matrix/cortex/scope is the cryptographic privacy boundary for cortex reads and writes issued by sub-agents. A CortexScope binds a sub-agent's identity to a pinned snapsh...

Package matrix/cortex/scope is the cryptographic privacy boundary for cortex reads and writes issued by sub-agents. A CortexScope binds a sub-agent's identity to a pinned snapshot root, a set of allowed memories, a Merkle multi-proof, and an optional writable bit. The cortex verifies the scope on every scoped call — signature, expiry, snapshot resolvability, and proof validity — before applying per-candidate Allows checks.

Source files: cortex/scope/scope.go, cortex/scope/verify.go, cortex/scope/match.go, cortex/scope/errors.go, cortex/scope_enforce.go.


Design decisions

Cortex never signs. Scope creation and signing lives in the agent runtime or sub-dispatch executor. Cortex only verifies. Key material never touches the cortex layer (D4).

Merkle proofs, not API trust. The sub-agent receives a multi-proof against the pinned snapshot root alongside the scope. It can verify its allowed keys independently — no need to trust an API call. This is the "why Merkle proofs not API trust" principle from research/06-agents.md §7.

Verify once, filter per-candidate. VerifyScope runs the full chain (signature, expiry, snapshot, proofs) once at the start of Find, Context, or ResolveScoped. Per-candidate filtering then calls Scope.Allows(&head) cheaply without re-running the crypto chain.

Default deny for writes. A scope without Writable=true returns ErrNotWritable on any UpdateHead attempt, regardless of Allows. Sub-agents are read-only by default.


The Scope struct

type Scope struct {
    SchemaVersion uint8
    Actor         string           // whose cortex
    SnapshotHash  [32]byte         // OverallRoot at scope creation time
    Include       Selector         // what the sub-agent MAY read
    Exclude       Selector         // belt-and-suspenders deny list
    Proofs        *MultiProof      // nil for Type/Tag/Frame-only scopes
    GrantedTo     string           // sub-agent ref
    GrantedBy     string           // parent agent ref (who signed this scope)
    ExpiresAt     time.Time        // zero = never expires
    BudgetTokens  int              // hard cap on cortex.Context budget; 0 = uncapped
    Writable      bool             // default false; required for UpdateHead
    Signature     []byte           // ed25519 sig by GrantedBy over UnsignedBytes(s)
}

Selector

type Selector struct {
    IDs    []memory.ID     // specific memory IDs — requires Proofs
    Types  []memory.Type   // all memories of these types
    Tags   []memory.Tag    // all memories having any of these tags
    Frames []memory.FrameRef // all memories indexed on these FrameRefs
}

Include.IsEmpty() — an empty Include grants nothing. Exclude is applied after Include matches.


Creating and signing a scope

Scope creation lives in the agent runtime. The cortex package provides the helpers:

unsigned := scope.UnsignedBytes(s)  // canonical CBOR of all fields except Signature
sig := ed25519.Sign(privKey, unsigned)
s.Signature = sig

Encoding:

bytes, err := scope.Encode(s)   // canonical CBOR
s, err := scope.Decode(bytes)   // parse + validate shape

Verification chain

scope.Verify(s, snapState, resolver, opts) checks:

  1. SchemaVersion matches the package constant.
  2. Include is non-empty.
  3. now <= ExpiresAt (or ExpiresAt is zero).
  4. GrantedBy's public key is resolved via KeyResolver.
  5. Signature is a valid ed25519 signature over UnsignedBytes(s).
  6. SnapshotHash is resolvable — there exists a snap/<seq> manifest with that OverallRoot.
  7. If Proofs is non-nil: len(Proofs.Proofs) == len(Include.IDs), each proof's KeyHash matches snapshot.HashMemoryKey(id), and the multi-proof verifies against the resolved manifest.
err := c.VerifyScope(s, time.Now())  // cortex facade

KeyResolver

type KeyResolver interface {
    ResolveAgentKey(ref string) (ed25519.PublicKey, error)
}

Injected at cortex construction via cortex.WithKeyResolver(r). Cortex never holds key material; the resolver lives in the agent runtime / tools/registry layer. A cortex constructed without a resolver rejects all scoped calls with ErrNoKeyResolver.


Scope enforcement choke point

Three functions in cortex/scope_enforce.go are the sole choke points:

// Once per call — full crypto chain
func (c *Cortex) VerifyScope(s *scope.Scope, now time.Time) error

// Per-candidate in Find / Context / ResolveScoped — cheap Allows check
func (c *Cortex) enforceRead(s *scope.Scope, h *memory.Head) error

// Per-target in UpdateHead — requires Writable + Allows
func (c *Cortex) enforceWrite(s *scope.Scope, h *memory.Head) error

enforceRead on a miss journals a KindScopeViolation entry and returns scope.ErrViolation. Context and Find (multi-target reads) filter silently without journaling per-candidate violations. ResolveScoped (single-target) does journal the violation.


Scope violations

A violation is logged as a KindScopeViolation journal entry carrying:

  • GrantedTo — the sub-agent that violated
  • GrantedBy — the parent who issued the scope
  • MemoryID — the memory that was accessed or attempted
  • Reason"violation" or "not_writable"
  • Mode"read" or "write"

Rate limiting

Scope violation logging is protected by a per-(GrantedTo, GrantedBy) token bucket (10/sec, burst 20). Over-rate violations still return scope.ErrViolation to the caller, but the journal write and its MMR cascade are suppressed. This bounds the OverallRoot-moving + Pebble-sync cost a malicious sub-agent can impose by looping violations.


Using a scope

Scoped reads pass the scope on the query or resolve call:

// Single-target read
mem, err := c.ResolveScoped(uri, scope, time.Now())

// Multi-target Find
result, err := c.Find(query.Query{
    Type:  []memory.Type{memory.TypeFact},
    Scope: scope,
    Limit: 10,
})

// Context bundle
bundle, err := c.Context(cortex.ContextOpts{
    Verb:  memory.VerbFind,
    Scope: scope,
})

// Head-only write (requires Writable=true)
_, err = c.UpdateHead(uri, patch, cortex.UpdateHeadMeta{Scope: scope})

Error reference

ErrorCause
ErrViolationMemory outside Include or inside Exclude
ErrNotWritableUpdateHead attempted with Scope.Writable=false
ErrScopeExpirednow > ExpiresAt
ErrSchemaVersionScope SchemaVersion doesn't match package constant
ErrSnapshotUnresolvedSnapshotHash not found in any snap/<seq> manifest
ErrProofMismatchProof count/key-hash mismatch vs Include.IDs
ErrEmptyIncludeInclude.IsEmpty() — nothing is allowed
ErrActorMismatchScope.Actor != store actor
ErrUnknownAgentKeyResolver.ResolveAgentKey returned unknown ref
ErrNoKeyResolverScoped call on a cortex without WithKeyResolver
ErrBudgetExceededContext request exceeds Scope.BudgetTokens

Modifying scope

What to changeWhere
Selector membership criteriascope/match.goAllows
Verification chain stepsscope/verify.goVerify
Scope wire formatscope/scope.goScope struct; bump SchemaVersion
Scope violation rate limitscortex/ratelimit.goDefaultRateLimits().ScopeViolation
Key resolver implementationAgent runtime — implement scope.KeyResolver