Matrix logo

Scope

Package matrix/cortex/scope is the cryptographic privacy boundary for cortex reads and writes issued by sub-agents. Merkle-proof scoping with signed CortexScope.

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). Sign / Encode helpers in the scope package are callable by the runtime and tests but are NOT a cortex API surface.

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 (schema, empty-include reject, expiry, signature, 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.

Empty Include matches nothing. An empty Include selector is default-deny (the more secure default per research/06-agents.md §12). Caller MUST populate at least one criterion to grant access. Verify rejects empty-include scopes at the boundary.

Wire format = canonical CBOR with SchemaVersion byte mixed in. Mirrors the snapshot.Manifest posture. The HPS envelope is a tools/attest concern at the chain boundary.


The Scope struct

type Scope struct {
    SchemaVersion uint8              // mixed into signed bytes; schema bump invalidates outstanding scopes
    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 (applied after Include)
    Proofs        *snapshot.MultiProof // nil for Type/Tag/Frame-only scopes
    ExpiresAt     time.Time          // zero = never expires (rare; production scopes always expire)
    BudgetTokens  int                // hard cap on cortex.Context budget; 0 = uncapped
    GrantedBy     string             // parent agent ref (who signed this scope)
    GrantedTo     string             // sub-agent ref (carried for audit)
    Writable      bool               // default false; required for UpdateHead/Write/Tombstone/AddEdge
    Signature     []byte             // ed25519 sig by GrantedBy over UnsignedBytes(s)
}

Selector

type Selector struct {
    Types  []memory.Type   // all memories of these types
    Tags   []memory.Tag    // all memories having any of these tags
    IDs    []memory.ID     // specific memory IDs; requires Proofs
    Frame  *FrameFilter    // memories matching (verb, obj_hash) tuples
}

Include matches if the memory satisfies ANY of the populated criteria (set union). Exclude is applied after Include matches to further restrict access.

FrameFilter

type FrameFilter struct {
    Verb      memory.Verb
    ObjHashes [][memory.ObjHashSize]byte
}

Matches when the memory's Head.Frames contains a FrameRef with matching Verb and Hash() in ObjHashes.


Creating and signing a scope

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

// Build unsigned bytes (canonical CBOR with Signature=nil)
unsigned := scope.UnsignedBytes(s)

// Sign with ed25519
s.Signature = ed25519.Sign(privKey, unsigned)

// Or use the helper
err := scope.Sign(s, privKey)

Encoding:

bytes, err := scope.EncodeScope(s)  // canonical CBOR including Signature
s, err := scope.DecodeScope(bytes)   // parse

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 the ID-derived hash, and the multi-proof verifies against the resolved manifest's memories SMT root.
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 (also caps BudgetTokens against Scope.BudgetTokens)
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 does not match package constant
ErrSnapshotUnresolvedSnapshotHash not found in any snap/<seq> manifest
ErrProofMismatchProof count/key-hash mismatch vs Include.IDs
ErrEmptyIncludeInclude has no populated criteria - nothing is allowed
ErrActorMismatchScope.Actor does not match store actor
ErrUnknownAgentKeyResolver.ResolveAgentKey returned unknown ref
ErrNoKeyResolverScoped call on a cortex without WithKeyResolver
ErrBudgetExceededContext request exceeds Scope.BudgetTokens
ErrSignatureInvalidEd25519 signature verification failed

Modifying scope

What to changeWhere
Selector membership criteriascope/match.go - Allows
Verification chain stepsscope/verify.go - Verify
Scope wire formatscope/scope.go - Scope struct; bump SchemaVersion
Scope violation rate limitscortex/ratelimit.go - DefaultRateLimits().ScopeViolation
Key resolver implementationAgent runtime, implement scope.KeyResolver