Subscription Extension — Normative Specification

Version: 3.18

Status: Active Depends: ENTITY-CORE-PROTOCOL.md (v7.31+), EXTENSION-INBOX.md (v5.0+) Related: EXTENSION-NETWORK.md (v1.0+) — subscription restoration after reconnection Proposal: PROPOSAL-SUBSCRIPTION-BOUNDED-FANOUT.md (S1-S3), PROPOSAL-COHERENT-CAPABILITY-AUTHORITY.md (SB1-SB3, GR1)


Path notation. Paths in this document use peer-relative notation (without leading /{peer_id}/). All peer-relative paths resolve to the local peer's namespace: system/tree means /{local_peer_id}/system/tree. Every path in the entity tree is absolute at rest — rooted at a peer identity. See ENTITY-CORE-PROTOCOL.md §1.4 for the path model. Cross-peer examples use absolute paths with explicit peer identities.

1. Overview

The subscription extension enables peers to receive notifications when entity tree paths change. Subscriptions are EXECUTE operations with inbox delivery — a subscription is a standing delivery that fires on each matching change event.

This extension depends on the inbox extension (EXTENSION-INBOX.md). Subscription notifications are delivered as inbox EXECUTEs through the standard dispatch chain.

This extension defines:

1.1 Coherent Capability

system/subscription entities are load-bearing: their deliver_token field authorizes future async deliveries by the subscription engine. Use system/subscription:subscribe to create them. The subscribe operation validates that the subscriber's identity appears as a granter anywhere in the deliver_token's authority chain (§3.1, via check_creator_authority — ENTITY-CORE-PROTOCOL.md §5.5) before persisting.

In-chain, not chain-root (correction; v3.17). This sentence, the §1.2 table row below, and the §11.1 MUST all read "chain root against the subscriber's identity". The normative pseudocode in §3.1 step 2a has always done the in-chain check"checks whether the subscriber's identity appears as granter anywhere in the chain" — and ENTITY-CORE-PROTOCOL.md §5.5 states the rule generally: the check "requires only that the writer appear as a granter somewhere in the chain — not that the chain roots at the writer." The chain-root phrasing was pre-correction residue, identical to what EXTENSION-CONTINUATION.md §3.1a swept for dispatch_capability; this spec was not swept at the same time.

It is load-bearing at exactly one seam, and that seam is a feature this spec defines. For ordinary self-delivery the two readings coincide — the subscriber is both the author and the chain root, so nothing distinguishes them. For third-party delivery (§6.1), where A subscribes on B and delivery goes to C's inbox, the deliver_token MUST root at C (so C's peer verifies it at delivery time) with A in-chain as the delegating leaf — precisely the arrangement §6.2 describes. An implementation that took the chain-root phrasing literally would reject every third-party-delivery subscription: the feature would fail against the spec's own summary line while passing its pseudocode.

Direct tree:put to system/subscription/* is permitted but bypasses the subscribe operation's validation; subscriptions created that way are not guaranteed to satisfy the deliver_token authority invariant. Application-level capability grants should cover system/subscription:subscribe rather than direct tree:put to the namespace. Direct tree:put is appropriate for system-extension code and administrative/bootstrap contexts. See ENTITY-CORE-PROTOCOL.md §6.3 for the kernel-vs-handler principle.

1.2 The three capabilities in a subscribe flow

A subscribe request involves three distinct capabilities, each playing a different role and rooted at a different peer in cross-peer scenarios. Confusing them is a common source of design and review confusion. This is an instance of the general three-slot model in ENTITY-CORE-PROTOCOL.md §5.2 ("Cross-peer capability provenance — the three slots") — read that first if the root / grantee / in-chain-granter distinction is unclear; the caller capability is the canonical "root = resource owner B, grantee = EXECUTE author A" case:

CapabilityCarried inAuthorizesIssued byValidated by
Caller capabilityOuter EXECUTE capability fieldThe subscribe operation itself, including the pattern resource scopeThe subscribee's namespace authority (target peer that owns the data being observed)Dispatch chain (verify_request, check_permission)
Subscription deliver_tokenparams.deliver_tokenFuture async dispatch from the subscription engine to the subscriber's inboxThe authority of the peer that owns the target inbox (§6.2 for third-party delivery)Subscribe handler (grants_access + in-chain check, §3.1 / §1.1)
Inbox EXECUTE-level deliver_tokenEXECUTE deliver_token field (EXTENSION-INBOX.md §2.3)A specific async delivery on a specific EXECUTEThe deliverer's namespace authorityDispatch chain at the receiver

For Alice (peer A) subscribing to Bob's data (peer B):

The subscribe handler at peer B validates the caller cap via the standard dispatch chain, and validates the deliver_token's authority chain (per §3.1) against the EXECUTE author at subscribe time. The integrity validation of the deliver_token (signature chain, scope, grantee match, revocation) at delivery time happens at peer A via the standard ENTITY-CORE-PROTOCOL.md §5.2 dispatch chain — that's separate from and unchanged by the subscribe-time check.


2. Type Definitions

2.1 Subscription Entity

Stored at system/subscription/{subscription_id} on the server:

system/subscription := {
  fields: {
    subscription_id:     {type_ref: "primitive/string"}
    pattern:             {type_ref: "system/tree/path"}       ; Path pattern to watch
    events:              {array_of: {type_ref: "primitive/string"}}
    deliver_uri:         {type_ref: "system/tree/path"}
    deliver_operation:   {type_ref: "primitive/string"}       ; Typically "receive"
    subscriber_identity: {type_ref: "system/hash"}            ; Subscriber's identity hash
    deliver_token:       {type_ref: "system/hash"}            ; Hash of capability token
    include_payload:     {type_ref: "primitive/bool", optional: true}   ; Persisted from the subscribe request; engine reads it at delivery (§4.2). Default false.
    created_at:          {type_ref: "primitive/uint"}         ; ms since epoch
    limits:              {type_ref: "system/subscription/limits", optional: true}
  }
}

The subscription entity is the source of truth. Internal subscription registries or indexes are caches over these entities.

Informative — Tree Storage: Subscription entities at system/subscription/* are regular tree entities — stored via tree put, accessible via tree get, listable via trailing-slash get on system/subscription/ (EXTENSION-TREE.md §2.2). The notification index is an implementation-level cache over these tree entities, not a separate store.

2.2 Notification

The params type for a notification inbox EXECUTE:

RATIFIED 2026-08-10 — the rename stands, and it is in the same cut round as delivery. This type was renamed from system/protocol/inbox/notification (strip the mis-homing protocol/ prefix) and re-homed INBOX→SUBSCRIPTION (owner-not-problem-domain, SPECIFICATION-FORMAT.md §8.4.2). The §8.4.4 data-at-rest test was applied and passes, on the same two legs as its system/inbox/delivery sibling (EXTENSION-INBOX.md §2.1): condition 2 — no (no notification content_hash is referenced by anything, the leg on which system/encrypted failed), and condition 1 — durable by design, factually empty in a no-installed-base ecosystem. The narrow question this banner reserved — durable-at-rest with no installed base — is the one the delivery ruling answered generally; there is no ground on which these two types diverge.

Coordinated cohort cut [MUST] — one round, two strings. system/inbox/delivery and system/subscription/notification cut together. There is no dual-kind acceptance window for either (AGENTS.md: no back-compat, no migration windows), and a peer holding undelivered mail at cut time MUST drain it first — a notification written under the old string is not matched by an upgraded handler and stalls its continuation silently (the §3.2 silent-drop class, no loud error).

Correcting the record, because the sequencing was ours. EXTENSION-INBOX.md §2.1 and the 2026-08-10 release-catchup both reasoned from this re-home as already settled while this banner still read "not ratified" — the ratification edited INBOX and never came back here, and this file went untouched from 4fe5348 (2026-08-04) through both 08-10 packets. core-go read the two correctly as contradictory, cut delivery alone, and declined to cut a string the canonical spec marked unratified. That was the right call on our text, and the half-cut cohort it risked is exactly what §2.1's own argument invokes against leaving a sibling behind.

; CANONICAL — owned here by EXTENSION-SUBSCRIPTION (re-homed 2026-08-02 from
; system/protocol/inbox/notification; owner-not-problem-domain, SPECIFICATION-FORMAT §8.4.2/§8.4.4).
system/subscription/notification := {
  fields: {
    subscription_id: {type_ref: "primitive/string"}
    event:           {type_ref: "primitive/string"}           ; "created", "updated", "deleted"
    uri:             {type_ref: "system/tree/path"}            ; Tree path that changed
    hash:            {type_ref: "system/hash", optional: true}   ; New entity hash (absent on delete)
    previous_hash:   {type_ref: "system/hash", optional: true}   ; Previous hash (absent on create)
  }
}

Ownership (re-homed 2026-08-02, PROPOSAL-NAMESPACE-CLEANUP-AND-BROWSER-LEG §3.2). This type is now canonical here in EXTENSION-SUBSCRIPTION — a subscription event belongs to the spec that defines subscriptions (owner-not-problem-domain, SPECIFICATION-FORMAT.md §8.4.2). It was previously system/protocol/inbox/notification, mis-homed under INBOX by the protocol prefix; its sibling system/inbox/delivery (async op results) correctly stays INBOX-owned. EXTENSION-INBOX.md §2.2 now reproduces this block for reading convenience (its receive handles the type as a payload). Change the type here; update that reproduction to match. (Wire note: this is a type-string rename — a version-mismatched peer sees an unknown type, a loud failure, not the silent never-meet of the §3.1 flag day; impls still update in step.)

Notifications report what changed and where. By default they carry only hash / previous_hash, not entity data. When the subscription sets include_payload (§2.3), the server MUST bundle the changed entity into the delivery envelope's included map (§4.2) — so the subscriber has the bytes atomically with the notification and needs no follow-up cross-peer GET. This is what makes the cross-peer mirror recipe a single hop: the subscriber applies the change locally with tree:put + CAS (expected_hash = previous_hash), no fetch. The recipe's CAS pins are below; the properties a conformant deployment of it MUST exhibit are §6.3. Absent/false, notifications stay lean (the "tell me when, I'll decide whether to read" case).

include_payload precise semantics (normative).

Mirror recipe — CAS pins for convergence (normative). A receiver mirroring a source path applies each transition to its local mirror path with a single continuation: extract the entity from included, then tree:put with expected_hash = notification.previous_hash. CAS makes a stale lap fail (409 hash_mismatch) instead of rolling state back — this is the amplification fix.

2.3 Subscribe Request

The params type for a subscribe operation:

system/subscription/request := {
  fields: {
    events:          {array_of: {type_ref: "primitive/string"}, optional: true}
    deliver_to:      {type_ref: "system/delivery-spec"}
    deliver_token:   {type_ref: "system/hash"}            ; Hash of capability token for notification delivery
    include_payload: {type_ref: "primitive/bool", optional: true}   ; Bundle the changed entity in the notification envelope's included (default false)
    limits:          {type_ref: "system/subscription/limits", optional: true}
  }
}
; The subscription pattern comes from the EXECUTE's resource field (resource.targets),
; not from params. Same pattern as tree get/put.
; If events is absent, defaults to ["created", "updated", "deleted"]
; If limits is absent, server applies its own defaults.

include_payload requires read authorization (normative). Subscribing is a distinct capability from reading. The subscribe check (§4.x dispatch) verifies the caller's capability grants the subscribe operation on system/subscription for the resource — it does not by itself grant read access to the entity content at those paths (that is the get operation on system/tree). A caller may legitimately hold subscribe without get — e.g. cache-invalidation or coordination subscribers that react to the fact of a change without reading its content. Therefore, when include_payload is set, the subscribe handler MUST additionally verify the caller's capability covers the tree read — check_path_permission("get", resource_path, caller_capability, "system/tree", local_peer_id) — and MUST reject the subscribe with 403 payload_unauthorized if it does not. include_payload does not bypass read authorization; it moves enforcement from a subscriber-side tree:get pull (which the caller would otherwise need get to perform) to a server-side check before the content is pushed — the net authorization is identical. A caller with subscribe but not get still receives lean (hashes-only) notifications by omitting include_payload.

The deliver_token field carries the content hash of a system/capability/token authorizing the server to deliver notifications to the inbox URI. The token entity MUST be in the envelope's included map — this follows the general rule in ENTITY-CORE-PROTOCOL.md §3.1 / §3.2: all entities referenced by hash from the EXECUTE's data fields must be present in the envelope's included map. This is the standard deliver token from EXTENSION-INBOX.md §5. It is distinct from the EXECUTE-level deliver_token defined in EXTENSION-INBOX.md §2.3 — that field is for immediate async inbox delivery, while this field is in the subscribe request params for the server to store with the subscription entity.

2.4 Subscription Limits

system/subscription/limits := {
  fields: {
    max_events:       {type_ref: "primitive/uint", optional: true}   ; Auto-unsubscribe after N notifications
    max_duration_ms:  {type_ref: "primitive/uint", optional: true}   ; Maximum lifetime in ms
    rate_limit:       {type_ref: "primitive/uint", optional: true}   ; Max notifications per minute
    notification_budget: {type_ref: "primitive/uint", optional: true} ; Budget per notification dispatch
  }
}

Limits constrain the subscription's resource consumption. If the subscriber requests limits, the server MAY apply stricter limits but MUST NOT apply more permissive ones. If the subscriber omits limits, the server applies its own defaults.

FieldExhaustion behavior
max_eventsSubscription auto-terminates after N notifications delivered
max_duration_msSubscription auto-terminates after duration (independent of deliver token TTL)
rate_limitNotifications exceeding the rate are dropped (not queued)
notification_budgetPer-notification budget for the delivery dispatch chain

2.5 Unsubscribe Request

system/subscription/cancel := {
  fields: {
    subscription_id: {type_ref: "primitive/string"}
  }
}

2.6 Subscription Redirect

Returned by the subscribe handler when the server is at capacity for the requested prefix:

system/subscription/redirect := {
  fields: {
    reason:       {type_ref: "primitive/string"}
                  ; "at_capacity" — subscriber limit reached for this prefix.
    prefix:       {type_ref: "system/tree/path"}
                  ; The prefix that is at capacity.
    alternatives: {array_of: {type_ref: "system/hash"}, optional: true}
                  ; Identity hashes of current subscribers that may accept subscriptions.
                  ; The redirected subscriber MAY connect to any of these peers
                  ; and attempt to subscribe on them instead.
                  ; Optional — server MAY omit to avoid disclosing subscriber list.
    capacity:     {type_ref: "primitive/uint", optional: true}
                  ; The configured limit, for informational purposes.
  }
}

When alternatives is present, it lists identity hashes of peers currently subscribed to this prefix on this server. These peers receive notifications from this server and may accept downstream subscriptions. The list SHOULD contain at most 10 entries. The server SHOULD randomize the order to distribute load. A server that does not wish to disclose its subscriber list MAY omit alternatives entirely — the redirect still communicates "at capacity" without leaking subscriber information.

The server MUST NOT include the subscriber's own identity in the alternatives list. The server SHOULD NOT include peers with status "suspect" or "failed" in the alternatives.

2.7 Subscription Capacity

A peer MAY advertise its subscription capacity using the existing system/config type (ENTITY-CORE-PROTOCOL.md §3.13, O2):

system/config := {
  ; At path: system/config/subscription
  data: {
    concern: "subscription",
    parameters: {
      max_subscribers_per_prefix: {type_ref: "primitive/uint", optional: true}
        ; Maximum direct subscribers per watched prefix.
        ; Default: implementation-defined. SHOULD be documented.
        ; A value of 0 means no limit.
      current_subscriber_prefixes: {type_ref: "primitive/any", optional: true}
        ; Map of prefix → current subscriber count.
        ; Updated on subscribe/unsubscribe.
        ; Informative — peers MAY read this to check capacity before subscribing.
    }
  }
}

Stored at: system/config/subscription. No new type needed — this uses the open-ended parameters field of the existing config type.

The current_subscriber_prefixes field is a map where keys are path prefixes and values are current subscriber counts. The field is informative — it MAY lag behind the actual state. The authoritative capacity check is the redirect response (§3.1).


3. Operations

3.1 Subscribe

The subscription extension registers a dedicated handler at system/subscription:

system/handler := {
  pattern:    "system/subscription"
  name:       "subscriptions"
  operations: {
    subscribe:   {input_type: "system/subscription/request"}
    unsubscribe: {input_type: "system/subscription/cancel"}
  }
}

Subscribe EXECUTEs target system/subscription with the resource field carrying the paths being subscribed to. Standard dispatch (ENTITY-CORE-PROTOCOL.md §6.5) routes to the subscription handler and enforces capability access — the caller's capability MUST grant the subscribe operation on handler system/subscription for the resource paths in the EXECUTE. This is the same capability model as all other handlers: handlers scope identifies the handler, resources scope identifies the data paths. No additional access check is needed within the handler.

EXECUTE
  uri:       "system/subscription"
  operation: "subscribe"
  resource:  {targets: ["local/files/*"]}
  params:    {
    type: "system/subscription/request"
    data: {
      deliver_to:     {uri: "system/inbox/file-watcher"}
      deliver_token:  <hash>
    }
  }

The subscription pattern is resource.targets[0] — the same field that authorizes the operation at dispatch time. No duplication between authorization and semantics.

The handler stores the subscription entity with the provided deliver token. Notifications are delivered later via the inbox mechanism (EXTENSION-INBOX.md) — the stored deliver token authorizes the delivering peer to send to the inbox URI. The subscription and inbox extensions are thus complementary: subscriptions define what to watch; inbox defines how to deliver.

Revocation. Revocation of stored subscription deliver tokens is handled automatically by dispatch-layer verification per ENTITY-CORE-PROTOCOL.md §5.2 step 4 (integrated is_revoked check, per PROPOSAL-COMPUTE-AMENDMENTS-V3 C2). When a subscription fires and the handler attempts to deliver, the dispatch chain catches revoked tokens automatically — no subscription-specific revocation check is required.

handle_subscribe(ctx, params):
  ; params is system/subscription/request

  ; 1. Validate deliver token exists in envelope
  deliver_token = lookup(params.deliver_token, ctx.included)
  if deliver_token is null: return error(400, "missing_deliver_token")

  ; 2. Validate deliver token grants delivery to the inbox URI
  if !grants_access(deliver_token, params.deliver_to.uri, params.deliver_to.operation or "receive"):
    return error(403, "deliver_token_insufficient")

  ; 2a. SB1 (v3.10): R1 creator-authorization check on deliver_token.
  ; Uses check_creator_authority (V7 §5.5) which collects the full authority
  ; chain via collect_authority_chain, verifies reachability, then checks
  ; whether the subscriber's identity appears as granter anywhere in the chain.
  ; This is at SUBSCRIBE TIME — distinct from delivery-time integrity validation
  ; (V7 §5.2 verify_request validates signature, scope, grantee, revocation
  ; when the delivery EXECUTE arrives at the receiver).
  (found, chain, err) = check_creator_authority(deliver_token, ctx.execute.data.author, ctx)
  if err == ChainUnreachable:
    return error(404, "chain_unreachable",
      "Deliver token's authority chain is not fully resolvable")
  if not found:
    return error(403, "embedded_cap_unauthorized",
      "Subscriber identity not in deliver_token authority chain")

  ; 2b. Persist deliver_token + chain to local content store so future
  ; notification dispatch can resolve the cap by hash without the chain
  ; needing to travel again. Chain was already collected — no re-walk.
  for entity in chain:
    ctx.content_store.put(entity)

  ; 3. Resolve limits (subscriber request vs server defaults; server may tighten)
  limits = apply_server_limits(params.limits or server_defaults)

  ; 3a. Check subscriber capacity for this prefix (§2.7)
  prefix = ctx.resource.targets[0]
  if max_subscribers_per_prefix > 0 and subscriber_count(prefix) >= max_subscribers_per_prefix:
    ; At capacity — return redirect response
    alternatives = list_subscriber_peers(prefix)   ; MAY be empty or omitted
    return {
      status: 303,
      result: {
        type: "system/subscription/redirect",
        data: {
          reason:       "at_capacity",
          prefix:       prefix,
          alternatives: alternatives,
          capacity:     max_subscribers_per_prefix
        }
      }
    }

  pattern = ctx.resource.targets[0]                    ; From EXECUTE resource field

  ; 3b. include_payload read-authorization (§2.3): content delivery requires
  ;     read access — the subscribe grant alone does not authorize it.
  if params.include_payload:
    if not check_path_permission("get", pattern, ctx.caller_capability,
                                 "system/tree", ctx.local_peer_id):
      return error(403, "payload_unauthorized",
                   "include_payload requires tree:get on the subscribed resource")

  ; 4. Create subscription entity
  subscription_id = generate_id()
  events = params.events or ["created", "updated", "deleted"]

  subscription = {
    type: "system/subscription"
    data: {
      subscription_id:     subscription_id
      pattern:             pattern                      ; From EXECUTE resource target
      events:              events
      deliver_uri:         params.deliver_to.uri
      deliver_operation:   params.deliver_to.operation or "receive"
      subscriber_identity: ctx.execute.data.author    ; system/hash
      deliver_token:       deliver_token.content_hash  ; system/hash
      include_payload:     params.include_payload or false
      created_at:          now()
      limits:              limits
    }
  }

  ; 5. Store subscription entity
  ctx.entity_tree.put("system/subscription/" + subscription_id, subscription)

  ; 6. Store associated entities (deliver token, identities in delegation chain)
  for entity in ctx.included: ctx.content_store.put(entity)

  ; 7. Register in notification index (implementation detail)
  notification_index.register(subscription)

  return {
    status: 200
    result: {
      subscription_id: subscription_id
      pattern:         pattern
      events:          events
      limits:          limits              ; Effective limits (may differ from requested)
    }
  }

3.2 Unsubscribe

handle_unsubscribe(ctx, params):
  ; params is system/subscription/cancel

  subscription = ctx.entity_tree.get("system/subscription/" + params.subscription_id)
  if subscription is null: return error(404, "subscription_not_found")

  ; Only the original subscriber or admin can unsubscribe
  if not hash_equals(subscription.data.subscriber_identity, ctx.execute.data.author):
    return error(403, "not_subscription_owner")

  ; Remove subscription entity
  ctx.entity_tree.put("system/subscription/" + params.subscription_id, null)
  notification_index.remove(params.subscription_id)

  return {status: 200, result: null}

3.3 Inbox Handler — Notification Delivery

Subscription notifications are delivered through the inbox handler's receive operation (EXTENSION-INBOX.md §3.1). The notification entity type (system/subscription/notification) carries the semantic information — no separate operation is needed.

Processing follows the standard inbox receive pattern (EXTENSION-INBOX.md §3.2). The inbox handler receives the notification EXECUTE, validates the capability, and processes it through write-ahead storage. Continuation behavior — what the subscriber does with the notification — is determined by EXTENSION-INBOX.md §3.3 (delegation to continuation handler when EXTENSION-CONTINUATION is installed, tree storage otherwise).


4. Notification Delivery

4.1 Trigger

Entity tree changes produce internal change notifications. The notification layer matches changes against registered subscription patterns.

on_tree_change(event_type, uri, new_hash, previous_hash, emission_context):
  for subscription in notification_index.match(uri):
    if event_type in subscription.data.events:
      deliver_notification(subscription, event_type, uri, new_hash, previous_hash, emission_context)

emission_context carries the bounds context from the tree write that triggered the change. Used for chain_id inheritance in notification bounds (§4.5).

Subscription's position in the emit pathway consumer ordering (after compute, last synchronous consumer) is specified in SYSTEM-COMPOSITION.md §2.2. The cascade depth threshold at which same-peer notification delivery is suppressed is specified in SYSTEM-COMPOSITION.md §3.2 (RECOMMENDED default: 8).

4.2 Construction

deliver_notification(subscription, event_type, uri, new_hash, previous_hash, emission_context):
  params = {
    subscription_id: subscription.data.subscription_id
    event:           event_type
    uri:             uri
  }
  if new_hash != null:      params.hash = new_hash
  if previous_hash != null: params.previous_hash = previous_hash

  ; Determine delivery capability based on inbox target.
  ;
  ; The deliver token (granter = subscriber, grantee = server) is verified
  ; at subscribe time (§3.1 step 2) to confirm the subscriber authorized
  ; delivery to the inbox URI. For actual delivery dispatch, the capability
  ; depends on where the inbox handler lives:
  ;
  ; Same-peer: The inbox handler is on this peer. The server dispatches
  ;   locally using its own handler grant for system/inbox. The handler
  ;   grant's granter is the local peer, so VerifyChain passes the root
  ;   trust check. The deliver token is not used at dispatch time — it
  ;   served its authorization purpose during subscription creation.
  ;
  ; Cross-peer: The inbox handler is on the subscriber's peer (or a
  ;   third party). The deliver token accompanies the EXECUTE to the
  ;   remote peer, where the token's granter (the subscriber) matches
  ;   the remote peer's local_peer_id and VerifyChain passes.

  deliver_uri = subscription.data.deliver_uri

  if is_local(deliver_uri):
    ; Same-peer delivery — use server's handler grant
    delivery_capability = handler_grant("system/inbox")
    capability_chain = {}
  else:
    ; Cross-peer delivery — use subscriber's deliver token
    delivery_capability = content_store.get(subscription.data.deliver_token)
    capability_chain = resolve_delegation_chain(delivery_capability)

  ; Construct delivery EXECUTE per EXTENSION-INBOX.md §4.1
  delivery = {
    type: "system/protocol/execute"
    data: {
      request_id: generate_id()
      uri:        deliver_uri
      operation:  subscription.data.deliver_operation    ; From deliver_to.operation at subscribe time; typically "receive"
      resource:   {targets: [deliver_uri]}               ; Authorization target — see note below
      params:     params
      author:     local_identity.content_hash            ; system/hash
      capability: delivery_capability.content_hash       ; system/hash
    }
  }

  ; Create signature entity (target-matching)
  signature = {
    type: "system/signature"
    data: {
      target:    delivery.content_hash
      signer:    local_identity.content_hash
      algorithm: "ed25519"
      signature: sign(delivery.content_hash)
    }
  }

  ; Build included map
  included = {local_identity, signature, delivery_capability, ...capability_chain}
  ; include_payload (§2.3): when the subscription opted in, bundle the changed
  ; entity so the subscriber has the bytes atomically with the notification and
  ; needs no follow-up GET. Opt-in — absent/false keeps notifications lean
  ; (hashes only). When set, the entity MUST be attached if resolvable.
  ; A subscription carries include_payload only if the subscriber passed the
  ; read-authorization check at subscribe time (§2.3, 403 payload_unauthorized
  ; otherwise) — so attaching content here does not bypass read access.
  if subscription.data.include_payload and new_hash != null:
    changed_entity = content_store.get(new_hash)
    if changed_entity != null: included.add(changed_entity)

  dispatch(envelope(delivery, included))

Same-peer delivery shortcut. For same-peer delivery, the subscription engine MAY dispatch notifications via the server's handler grant for system/inbox and SHOULD NOT construct a delivery token. The delivery-token mechanism applies to cross-peer delivery where subscriber and server are distinct principals. The deliver token validated at subscribe time (§3.1 step 2) confirms authorization intent; same-peer dispatch does not require it at delivery time.

Resource target is deliver_uri, without suffix. The resource target MUST be deliver_uri as stored on the subscription entity — no subscription_id, event type, or other suffix appended. The inbox handler uses the resource target path to locate continuations stored at that path. Appending a suffix causes the path to not match the continuation location, breaking advancement.

Capability scoping for notification delivery uses the deliver_token (validated at subscribe time, §3.1 step 2), not the resource target path. The resource target's authorization role is defense-in-depth at the dispatch layer, where deliver_uri is sufficient scope.

4.3 Pattern Matching

Subscription patterns use the same matching rules as capability patterns (ENTITY-CORE-PROTOCOL.md §5.4):

4.4 Entity Inclusion

Inclusion is the subscription's decision, not the server's (normative; v3.17). Whether the changed entity rides in the notification envelope's included map is determined solely by the subscription's include_payload flag (§2.3), under the semantics pinned in §2.2: set ⇒ the server MUST bundle the direct entity at notification.hash; absent or false ⇒ the server MUST NOT attach it. The two carve-outs are also in §2.2 — nothing is bundled for a removed event, and a source that cannot resolve the entity at delivery time delivers hash-only rather than failing.

The subscriber detects inclusion by checking envelope.included[params.hash], and MUST tolerate its absence on an include_payload subscription (the resolution-failure fallback).

This section previously read the other way — "the server MAY include … inclusion decision is implementation-defined," with a size-threshold strategy and a subscriber hint named include_entities. That predates the v3.13/v3.14 include_payload arc and was left behind by it. It was wrong on three counts: it contradicted §2.2's MUST, it named a field this spec does not define (include_entities is EXTENSION-QUERY's, §5.1 there), and a server-side size threshold silently breaks the §6.3 mirror recipe — a receiver whose continuation expects the bundled entity gets a hash-only notification for exactly the large entities, and deref_included no-ops per its best-effort rule, so the mirror stops advancing with no error anywhere. An implementation-defined inclusion policy is a MAY whose two conformant readings diverge across a peer boundary; it is pinned, not preserved.

4.5 Notification Budget Model

Notification delivery uses independent budget — not the writer's remaining budget. This is the critical design distinction:

on_tree_change(event_type, uri, new_hash, previous_hash, emission_context):
  for subscription in notification_index.match(uri):
    if event_type in subscription.data.events:
      ; Each notification gets its own bounds — NOT derived from emission_context.budget
      notification_bounds = {
        ttl:            peer_default_ttl                     ; Fresh TTL
        budget:         subscription.data.limits.notification_budget or peer_default_budget
        chain_id:       emission_context.chain_id            ; Inherited for causal tracing
        cascade_depth:  emission_context.cascade_depth       ; Inherited for cross-peer cascade tracking
        visited:        []                                   ; Fresh visited list
      }
      deliver_notification(subscription, event_type, uri, new_hash, previous_hash, notification_bounds)

Rationale: A tree write that triggers N subscriptions should not divide the writer's budget among N notifications. The writer pays for its write; each subscription pays for its own notification delivery from its own budget allocation. This prevents a popular path from exhausting the writer's budget.

chain_id and cascade_depth ARE inherited from the emission context. chain_id links notifications to the causal chain that triggered them, enabling end-to-end tracing. cascade_depth carries the current cascade depth to the receiving peer, ensuring cross-peer cascades accumulate depth rather than resetting to 0 at each peer boundary (see SYSTEM-COMPOSITION.md §3.4). Budget, TTL, and visited are independent because the notification is a new dispatch chain branching from the write.

4.6 Limits Enforcement

Before delivering each notification, the server checks subscription limits:

check_subscription_limits(subscription):
  limits = subscription.data.limits
  if limits is null: return ALLOW

  if limits.max_events != null:
    if subscription.delivered_count >= limits.max_events:
      terminate_subscription(subscription, "max_events_reached")
      return DENY

  if limits.max_duration_ms != null:
    if now() - subscription.data.created_at >= limits.max_duration_ms:
      terminate_subscription(subscription, "max_duration_reached")
      return DENY

  if limits.rate_limit != null:
    if subscription.recent_delivery_rate > limits.rate_limit:
      return DENY   ; Drop notification, do NOT terminate subscription

  return ALLOW

delivered_count and recent_delivery_rate are internal tracking state, not stored on the subscription entity. max_events and max_duration_ms termination deletes the subscription entity. rate_limit drops individual notifications without terminating.

4.7 Chain-error marker emission on outbound dispatch failures

The subscription engine's outbound notification dispatches participate in the chain-error convention (EXTENSION-CONTINUATION.md §3.10). When a notification delivery fails terminally — limit-exceeded suppression (§4.6), transport failure, or capability rejection — the subscription engine MUST bind a lost marker at:

system/runtime/chain-errors/lost/{chain_id}/{subscription_id}/{reason}/{marker_hash}

where {step_index} is {subscription_id} (the trigger is a tree change rather than a chained EXECUTE, so no original-request-id is available). {chain_id} is inherited from the source change's chain causality per §4.5. {reason} follows EXTENSION-CONTINUATION.md §3.10.5 (= the code value from the responding handler's error appendix, or rate_limited / max_events_reached / max_duration_reached for limit suppression).

This is the §9 #8 completion contract (per GUIDE-INSPECTABILITY.md v1.1 §9 #8) for subscription-emitted chains:

The marker is informational; it MUST NOT trigger advancement, retry, or any reactive behavior beyond surfacing the failure for inspect tooling and validate-peer's CAT-CHAIN-COMPLETION conformance check. Per the chain-error marker convention, same-reason re-binding is idempotent; the path discriminator on {reason} keeps distinct failure modes coexistent rather than overwriting.

Rationale: F-CIMP-2 (Cohort B) was an outbound subscription dispatch failure that surfaced session-later as cross-impl attribution thrash rather than at validate-peer time, because the spec did not state whether the §3.10 chain-error marker convention applied to subscription engine outbound EXECUTEs. This subsection closes that gap.

Per proposals/implemented/PROPOSAL-CHAIN-PARTICIPATION-INVARIANTS.md §2.3.


5. Subscription Lifecycle

5.1 Creation

  1. Subscriber creates a continuation entity at its inbox path (optional, local)
  2. Subscriber creates a deliver token scoped to the inbox path
  3. Subscriber sends EXECUTE to system/subscription with operation: subscribe, pattern, delivery spec, and deliver token
  4. Server stores subscription entity, registers in notification index
  5. Server returns subscription_id

5.2 Active Period

Notifications fire on tree changes matching the subscription pattern. Each notification is an independent inbox delivery EXECUTE.

Per-subscription ordering (normative; v3.15). Within a single subscription, deliveries MUST arrive in tree-change order — the order in which the underlying tree changes were committed at the publisher. Subscribers MAY rely on this ordering: for a single subscription's matching changes, a notification with previous_hash = H_n arrives before a notification reporting the next change in that subscription's stream.

Cross-subscription ordering is impl-defined. Across different subscriptions (distinct subscription_id values), notification ordering is not guaranteed — the delivery substrate MAY parallelize delivery across subscriptions (e.g., shard-by-subscription_id worker pools, per-subscription queues) to scale throughput. This is the recommended impl pattern for production deployments serving multiple concurrent subscribers; workbench-go's Stage 5 K-worker microbench measured near-linear scaling to CPU count (K=4 → 3.8×, K=8 → 5×); shard-by-subscription_id preserves the within-subscription ordering MUST while parallelizing across subscriptions.

Rationale. Within-subscription ordering is the typical consumer expectation (matches state-synchronization, file-change, and audit-log use cases); it makes §5.5 gap detection meaningful (the previous_hash chain is well-formed within a subscription). Cross-subscription parallelism is the load-scaling lever — workbench-go's Stage 5 work surfaced 1/N per-subscriber degradation under single-loop delivery, fixed by sharded parallel workers. Per proposals/implemented/PROPOSAL-STAGE-5-SUBSCRIPTION-POSITIONS.md §1.

K-tuning advisory (v3.16, informative; round-3 evidence strengthened). Under FNV-style or general-purpose hash distribution of subscription_id across K shards, collisions are common when the steady-state subscription count N is close to K. Workbench-go round-3 5-seed re-measurement at K=8, N=4 random sub_ids: 3/5 seeds at 2K/sec hub hit a 2:1 collision; 5/5 seeds at 5K/sec hub hit collision (one seed hit a 3:1 collision). This matches P-theory: P(any 2:1 collision) ≈ 67% at K=8, N=4 (1 - 8!/(8-4)!/8^4). The K>2N rule of thumb is operationally important, not a curiosity — production deployments with N≥4 subscribers per hub will routinely observe ~50% per-spoke throughput spread without it. Operators expecting uniform per-subscription throughput SHOULD configure K > 2*N for the expected steady-state subscription count (e.g., K=16+ for N=8). This is tuning guidance, not a normative requirement — correctness is unaffected by hash collisions (within-sub FIFO + cross-sub parallelism still hold; collisions only cause throughput variance, not ordering or delivery loss). Round-robin or counter-mod-K assignment can give uniform distribution at the cost of subscription-ID-to-shard stability across restarts; impls choose. Per round-2 finding F10 (entity-workbench-go/docs/architecture/reviews/FEEDBACK-ARCH-STAGE-5-ROUND-2.md §2) + round-3 5-seed empirical validation (entity-workbench-go/docs/architecture/reviews/STAGE-5-TOPOLOGY-VALIDATION.md §1).

5.3 Renewal

The deliver token's expiry bounds the subscription's lifetime. To maintain a long-lived subscription:

renew_subscription(subscription_id, new_deliver_token):
  ; Send a new subscribe EXECUTE to system/subscription
  ; with the same pattern and inbox URI but a fresh deliver token.
  ; Server detects matching (subscriber, pattern, deliver_uri)
  ; and updates the existing subscription's deliver_token atomically.

The server identifies a renewal by matching: same subscriber identity + same pattern + same deliver URI. On match, the server updates the subscription entity's data.deliver_token rather than creating a duplicate. The subscription ID is preserved across renewals — only the token rotates.

5.4 Termination

A subscription terminates when:

TriggerBehavior
Explicit unsubscribeSubscriber sends EXECUTE with operation: unsubscribe. Server deletes subscription entity.
Deliver token expiryServer validates token before each delivery. On expiry, server deletes subscription entity.
max_events reachedNotification count equals limit. Server deletes subscription entity (§4.6).
max_duration_ms elapsedSubscription age exceeds limit. Server deletes subscription entity (§4.6).
Delivery failureAfter implementation-defined retries, server MAY delete subscription entity.
Server cleanupServer MAY evict subscriptions with no successful delivery for a configurable period.

On termination, the server deletes the subscription entity from system/subscription/. The subscriber detects termination by absence of notifications and can re-subscribe.

5.5 Gap Detection

Notifications are best-effort. The subscriber can detect gaps:

For guaranteed consistency, subscribers SHOULD periodically reconcile via GET on subscribed paths.

5.6 Connection Drops

Subscription entities survive connection drops. Subscriptions are tree entities stored at system/subscription/* — they are not connection state. When a transport connection to a subscriber's peer fails:

On reconnection:

When EXTENSION-NETWORK.md is installed, subscription restoration is handled by the network handler's maintain-peer continuation graph (EXTENSION-NETWORK.md §7). Without the network extension, implementations handle restoration through their own reconnection logic — the normative requirement is that subscription entities in the tree are the source of truth, and reconnection to a peer whose subscriptions are still valid resumes notification delivery.

Deliver token expiry during disconnect. If a deliver token expires while the subscriber is disconnected, the subscription is terminated on the next delivery attempt (§5.4). The subscriber must re-subscribe with a fresh deliver token after reconnecting.

5.7 Subscriber Process Restart (v3.15)

§5.6 covers transport disconnect/reconnect: the subscriber's process stays up, the TCP connection drops, and the publisher's subscription entity reactivates on reconnection. This subsection covers subscriber process restart: the subscriber's process restarts entirely, losing any in-memory inbox handler registrations.

Publisher-side state is durable (unchanged from §5.6). Subscription entities persist at system/subscription/* on the publisher across the subscriber's restart. The publisher continues attempting delivery to the subscribed inbox URI per its retention/cleanup policy (§5.4 Termination triggers).

Subscriber-side handler restoration is application-level (normative; v3.15). On subscriber process restart, the substrate MUST NOT automatically re-register inbox handlers for prior subscription entities — the MUST NOT keeps the substrate's scope predictable so application/SDK helpers can build restoration with known semantics. Re-registration is application-level responsibility — typical implementations expose an SDK helper (e.g., RestorePriorSubscriptions(channel)) that, on boot:

  1. Scans local tree for system/subscription/* entities created by this peer's identity (or matching a configured filter).
  2. Re-registers an inbox handler for each via the standard inbox-handler registration mechanism (EXTENSION-INBOX.md §3).
  3. Returns the count of restored handlers for operational visibility.

Until the SDK helper runs, the publisher's delivery attempts land at an inbox URI with no registered handler — deliveries fail (per ENTITY-CORE-PROTOCOL.md §5.2 dispatch chain return); the publisher's retention policy (§5.4) eventually evicts the subscription if delivery failure persists.

Gap detection (§5.5) covers missed-while-down notifications. After handler restoration, the first received notification's previous_hash SHOULD be compared against the subscriber's local last-known hash for that URI; mismatch indicates missed intermediate changes; reconcile via GET on the subscribed paths.

EXTENSION-NETWORK alternative. When EXTENSION-NETWORK is installed, its maintain-peer continuation graph (§7) MAY perform automatic subscriber-side restoration. The substrate-level requirement above applies when EXTENSION-NETWORK is not installed or its auto-restoration is disabled.

Rationale. Subscriber-side restoration is a recovery pattern, not a core substrate property. Content-addressed durable state (the subscription entity in the tree) is the spec boundary; in-memory handler registration is process-lifetime state. Distinguishing the two gives the substrate a clean persistence model + lets recovery patterns layer on top via SDK / EXTENSION-NETWORK / application logic without conflating spec scope. Per proposals/implemented/PROPOSAL-STAGE-5-SUBSCRIPTION-POSITIONS.md §2; workbench-go surfaced the gap during Stage 5 restart-equivalence work (entity-workbench-go/docs/architecture/reviews/STAGE-5-RESTART-MESH.md).


6. Cross-Peer Delivery

6.1 Third-Party Delivery

The delivery URI does not have to point back to the subscriber:

Peer A subscribes on Peer B:
  pattern:      "local/sensors/*"
  deliver_uri:  "entity://peer_c/system/inbox/sensor-data"
  deliver_token: grants Peer B → Peer C delivery

Peer B delivers notifications directly to Peer C. Peer A is not in the delivery path.

6.2 Capability Requirements

For third-party delivery, the deliver token MUST authorize the server (Peer B) to deliver to the third party (Peer C). The subscriber (Peer A) MUST have delegation authority — either:

The delegation chain is included in the subscribe EXECUTE's envelope. The server stores the deliver token with the subscription and uses it for every delivery.

6.3 Convergent mirroring — required properties (v3.17)

A mirror is a peer that reproduces another peer's subtree by subscribing to it and applying each reported transition locally. The mechanism is already normative and split across three specs — include_payload and the CAS pins (§2.2/§2.3 here), CAS-create on the zero hash (ENTITY-CORE-PROTOCOL.md §3.9), the deref_included transform_op that lets a plain continuation consume the bundled entity (EXTENSION-CONTINUATION.md §2.2), and request-side included preservation so the map reaches that continuation (ENTITY-CORE-PROTOCOL.md §3.3). This section pins what that mechanism must produce, because the mechanism being conformant per-message does not by itself establish that a ring of mirrors terminates.

Why it is stated as an observable property, not only as a recipe. The failure this prevents is stale-lap amplification: a slow lap carrying an old previous_hash arrives at a peer that has already advanced, an unconditional local tree:put rewrites that peer backwards, the rollback is itself a real tree change, and it propagates forward and collides with newer laps. Measured on a 4-peer ring, 20 external writes produced ~87 laps, bounded only by the cascade-depth backstop. Every individual write in that trace is conformant. The defect is only visible in the aggregate, across a peer boundary — so the aggregate is what gets pinned.

Drive. N external writes at one peer of a mirror topology, each write matching a subscription whose delivery reaches the mirroring peer.

Property (i) — bounded amplification (MUST). Over the drive, each peer MUST settle at most 1.5 × N writes on the mirrored path, within a bounded time. Under a correct CAS recipe a one-way mirror settles exactly N; the 1.5 multiplier is headroom for transient races, not a licence to re-settle. Exceeding it means a stale lap is being applied rather than rejected — the recipe is failing to terminate.

Property (ii) — convergence to latest (MUST). After the drive quiesces, every mirroring peer MUST hold the entity bound by the latest external write, byte-identical to the source (the mirrored entity's content_hash equals the source's — see ENTITY-CBOR-ENCODING.md §5.4). Convergence MUST hold under delivery loss: a peer that misses notifications MUST still reach the latest state, via §5.5 gap detection and re-read.

What is conformance-observable and what is not. Properties (i) and (ii) are cross-impl assertions — they are stated over externally visible tree state and settle counts, so any peer can be measured by any implementation's harness. Drop injection is implementation-internal: how a harness induces the loss for property (ii) depends on engine internals and is not specified here; only the assertion shape (converge-to-latest) is. A conformance run that cannot inject drops still MUST assert (ii) under normal delivery.

Scope. These properties bind a peer that offers the mirror recipe — include_payload subscriptions plus the §2.2 CAS-put chain. They do not oblige a peer to implement mirroring at all; they pin what mirroring means where it is offered, so two conformant peers mirroring each other terminate rather than amplify. Verified cross-impl (Go / Rust / Python, fresh peers per directional pair) before this section was written; the bound is a folded result, not a new requirement.


7. Security Considerations

7.1 Subscription Enumeration

Subscriptions at system/subscription/* are entities in the tree. Access to system/subscription/ SHOULD be capability-gated. Only the peer admin or the subscription's subscriber identity SHOULD be able to list or read subscription entities.

7.2 Notification Flooding

A subscriber could create subscriptions on high-frequency paths to generate excessive delivery traffic. Defenses:

7.3 Stale Subscriptions

Subscriptions with expired deliver tokens MUST NOT accumulate. The server MUST validate the deliver token before each delivery attempt and delete the subscription on expiry.


7.4 Privacy + cross-peer observability

Per GUIDE-INSPECTABILITY.md v1.2 §9 #4:

Per §9 #7:

Outbound dispatch failures bind chain-error markers per §4.7; those markers are themselves local-namespace per EXTENSION-CONTINUATION.md §6.5 (the canonical home for chain-error marker locality).

Per proposals/implemented/AUDIT-PRIVACY-AND-CROSS-PEER-OBSERVABILITY.md §2.2.

8. Dissemination Trees (Informative)

When a peer serves a popular prefix, bounded fan-out (§2.7, §3.1 step 3a) naturally produces a dissemination tree. This section describes the convention.

8.1 Tree Formation

A dissemination tree forms from bounded fan-out:

1. Source peer S publishes data at prefix P
2. Subscriber A subscribes to S for prefix P → accepted (S has capacity)
3. Subscriber B subscribes to S for prefix P → accepted
4. ... (up to K subscribers)
5. Subscriber K+1 subscribes to S for prefix P → redirect to [A, B, ...]
6. K+1 picks A, subscribes to A for prefix P → accepted (A has capacity)
7. A now receives notifications from S AND serves them to K+1

A receives data from S via its subscription. When A's local tree changes (notification received, written to local tree, local emit fires), K+1's subscription on A fires. The data propagates: S → A → K+1.

With K=10, tree depth is O(log_10(N)) for N total subscribers:

N (subscribers)Direct fan-outWith K=10 treeTree depth
1010 per source10 per node1
100100 per source10 per node2
1,0001,000 per source10 per node3
10,00010,000 per source10 per node4

8.2 Intermediate Peer Setup

For a peer to serve as an intermediate node in the dissemination tree, it needs:

  1. A subscription on its upstream peer for the prefix
  2. A mechanism to write received notification data to the local tree (so downstream subscriptions fire on local emit)
  3. Capacity to accept downstream subscriptions (advertised via system/config/subscription)

How the intermediate peer writes received data to its local tree is implementation-specific. A continuation at the subscription's inbox path is one approach (see EXTENSION-CONTINUATION.md). An imperative handler that processes notifications and writes to the tree is equally valid. The normative requirement is: notification data from the upstream peer ends up in the local tree, local emit fires, and downstream subscriptions trigger.

8.3 Client Behavior on Redirect

When a subscribe request returns status 303 with a system/subscription/redirect result:

  1. If alternatives is present: pick an alternative peer from the list (random or by preference).
  2. If alternatives is absent: the server declined without suggestions. The subscriber MAY query the source peer's subscriber list via other means, or retry later.
  3. Connect to the alternative peer (if not already connected).
  4. Subscribe on the alternative peer. If it also responds with 303: follow its redirect, or try another alternative from the original list.
  5. If no alternatives have capacity: retry later or accept the latency cost.

Subscribers SHOULD limit redirect following to a maximum depth of 10 hops. If no peer accepts the subscription within the redirect depth limit, the subscriber SHOULD back off and retry after a delay.

Reading the alternative peer's system/config/subscription before subscribing is an optional optimization — the redirect response (step 3a) is the authoritative mechanism for capacity enforcement.

8.4 Self-Repair

When an intermediate peer goes offline:

  1. Downstream subscribers detect the connection drop (network extension)
  2. The maintain-peer continuation triggers reconnection attempts
  3. If the intermediate peer doesn't recover within the timeout: a. Downstream subscribers read the alternatives from the redirect they originally received (cached locally) b. Or: downstream subscribers re-subscribe to the original source, which issues a new redirect with updated alternatives c. Or: downstream subscribers discover another intermediate peer via the source's subscriber list

The repair uses existing network extension mechanisms. No new repair protocol is needed. The tree rebalances incrementally as subscribers find new upstreams.

8.5 Sync Integration

Subscription dissemination and sync dissemination are structurally identical. Both propagate tree state changes through a chain of peers. A peer that serves as a dissemination node for subscriptions can also serve as a sync intermediary for the same prefix.

Peers SHOULD use the same topology for both subscription dissemination and sync propagation when targeting the same prefix. Configuring both through the same upstream peer avoids redundant connections and simplifies the tree structure.

8.6 Recommended K Values

The max subscribers per prefix (K) is configurable per peer via system/config/subscription. Recommended defaults:

Peer profileK valueRationale
Resource-constrained (mobile, IoT)3-5Minimal per-node load
Standard peer10-20Balance between depth and load
Hub/infrastructure50-100Shallow trees, higher capacity

No system-wide consensus on K is needed. Each peer sets its own limit.


9. Constants

9.1 Event Types

EventDescription
createdEntity stored at a path that had no entity
updatedEntity replaced at a path that already had an entity
deletedEntity removed from a path

9.2 Standard Operations

OperationHandlerDescription
subscribesystem/subscriptionCreate a subscription on a path
unsubscribesystem/subscriptionRemove a subscription
receivesystem/inboxDeliver a change notification

10. Write Authorization

All subscription handler tree writes are to the handler's managed namespace (system/subscription/*). The handler's own grant authorizes all writes.

OperationWrite targetAuthorizationCapability recorded
subscribesystem/subscription/{id}Handler grantHandler grant
unsubscribesystem/subscription/{id} (delete)Handler grantHandler grant
renewalsystem/subscription/{id} (update)Handler grantHandler grant

The caller's capability authorizes calling the subscription handler with the subscribe operation (validated at dispatch, ENTITY-CORE-PROTOCOL.md §6.5). The tree write that stores the subscription entity is authorized by the handler's own grant.

Delivery authorization. When the subscription engine delivers notifications:

See ENTITY-CORE-PROTOCOL.md §6.8 for the general write authorization model.


11. Conformance

11.1 MUST Implement

11.2 SHOULD Implement

11.3 MAY Implement

11.4 Implementation-Defined