Proposal 018: Layered capability_limited Participant Restrictions¶
Context¶
The current Node runtime already uses capability_limited as a peer-quality
state when a remote peer is missing required transport capabilities. That solves
one narrow networking problem, but it does not yet define how participant-level
sanctions should work above transport.
This leaves an architectural gap:
- soft degradation is underspecified,
- hard operation blocks are ad hoc,
- and the system lacks one auditable artifact binding sanctions to a concrete participant subject, author, and expiry.
The gap matters because Orbiplex explicitly rejects both extremes:
- a harmless soft downgrade that leaves every privileged operation effectively available,
- and an opaque binary kill switch that collapses all escalation into permanent exclusion.
Goals¶
- freeze a participant-scoped artifact for layered restriction state,
- keep soft degradation separate from hard blocked operations,
- preserve a non-blockable minimum participation floor,
- define the first minimal Node enforcement hooks without overloading the transport governor,
- and keep the blocked-operation namespace open for later growth.
Non-Goals¶
- This proposal does not redefine transport-layer capability advertisement.
- This proposal does not make
capability_limiteda permanent exclusion state. - This proposal does not yet define a global sanction registry or federation-wide propagation rail.
- This proposal does not yet standardize graduated time-based soft degradation curves.
Decision¶
Orbiplex should model participant-scoped limited participation through one layered artifact:
participant-capability-limits.v1
The artifact represents one participant-level state:
status = capability_limited
That state always carries a soft layer:
soft.priority-factorsoft.rate-limit-factor
and may additionally carry a hard layer:
hard.blocked-operationshard.reason/refhard.decision/authorhard.expires-at
The hard layer is therefore:
- explicit,
- authored,
- bounded in time,
- and narrower than full removal from the network.
Proposed Model¶
1. Separation of Layers¶
capability_limited is a composite state, not a single switch.
- The
softlayer adds friction. - The
hardlayer removes selected operational permissions.
This lets the system degrade participation without collapsing immediately into ban semantics.
2. Scope¶
The first artifact is participant-scoped:
participant/id = participant:did:key:...
This is deliberate. The participant is the right MVP target for service-facing
restrictions such as procurement/offer or endorsement/emit.
The proposal does not yet extend the same artifact family to org or pod-user.
3. Soft Layer¶
The MVP soft layer freezes two factors:
priority-factorrate-limit-factor
Both are normalized to (0.0, 1.0], where 1.0 means no degradation.
The first Node runtime SHOULD consume priority-factor on procurement ranking
and SHOULD consume rate-limit-factor through a small deterministic per-participant
operation cooldown hook for the first admitted daemon operations.
4. Hard Layer¶
The MVP hard layer is an open namespace of blocked operations rather than a closed enum of privilege names.
Examples:
procurement/offernym/issueendorsement/emitrelay/servearbitration/participate
This keeps operation semantics above the transport layer and avoids confusing blocked service permissions with handshake-time advertised capabilities.
5. Protected Operations¶
Some operations remain outside the admissible hard-block set in MVP:
core/messagingkeepalivedispute/fileubc/claimsignal-marker/send
These form the minimum floor of continued presence, communication, and appeal.
6. Relationship to Existing Peer Governor State¶
This proposal complements the existing peer-quality downgrade path.
- peer governor state remains transport- and session-facing,
- participant restriction state remains actor- and operation-facing.
The two MAY correlate in later policy, but they are not the same layer and should not be collapsed into one mutable flag.
First Runtime Hooks¶
The first Node runtime rollout should stay small but real:
- reject
procurement/requestwhen the asking participant is hard-blocked for that operation, - reject
procurement/offerwhen the responder participant is hard-blocked for that operation, - reject
response/deliverwhen the responding participant is hard-blocked for that operation, - reject
procurement/contract-acceptwhen the asking participant is hard-blocked for that operation, - reject
response/acceptwhen the asking participant is hard-blocked for that operation, - reject
response/rejectwhen the asking participant is hard-blocked for that operation, - apply
soft.priority-factoras a deterministic procurement-ranking penalty, - apply
soft.rate-limit-factoras a deterministic per-participant cooldown on currently admitted daemon operations such asprocurement/request,procurement/offer,procurement/contract-accept,response/deliver,response/accept,response/reject, andsignal-marker/send, - keep
signal-marker/sendexplicitly admissible as a protected-floor operation even when the participant is otherwise limited, - expose participant-side
response/acceptandresponse/rejectthrough a dedicated participant-scoped daemon control contract instead of reusing operator action endpoints.
The MVP rollout also persists imported restriction records in the daemon commit
log, replays them into the local read model after restart, exposes local
operator import/list/detail/clear control surfaces, and records clear operations
as daemon-local tombstones with optional reason/ref. The daemon keeps a
monotonic last_cleared_at read model so imports with recorded-at at or before
the latest clear tombstone cannot resurrect stale sanctions, including after
commit-log replay. Import rejects already-dead hard blocks where
hard.expires-at <= recorded-at or where hard.expires-at has already passed at
import time, rejects stale recorded-at overwrites of a newer local record, and
applies Rust-side defense-in-depth validation for participant id shape,
reason/ref bounds, and soft factors in (0.0, 1.0]. List/detail export paths
validate each emitted record through the same schema-gate family before returning
operator JSON, while import/clear bodies are bounded to a small local control
payload size. Import and clear also emit a metadata-only
participant-capability-limits-changed SSE event so operator surfaces can
refresh without scraping the commit log. Discovery visibility, broader operation
coverage, maximum hard-block renewal policy, and richer graduated governor
policies remain deferred to later work.
Trade-offs¶
Benefits¶
- sanctions become auditable rather than folkloric,
- hard blocks become narrow and time-bounded,
- the project preserves a protected minimum participation floor,
- runtime policy can grow incrementally without redesigning identifiers.
Costs¶
- one more artifact family to keep in sync across docs and runtime,
- temporary asymmetry where soft factors are frozen before every soft hook is equally mature,
- a new distinction operators must understand between peer downgrade and participant restriction.
Risks¶
- if the blocked-operation namespace drifts semantically, enforcement becomes confusing,
- if protected operations are not frozen early, ad hoc sanctions may overreach,
- if soft factors are not consumed by real runtime hooks, the artifact becomes a paper tiger.
Open Questions¶
- Should the first cooldown grain stay at
(participant, operation), or move later toward(participant, operation, question)for multi-contract concurrency? - Which artifact should carry later review history: this restriction record directly, or a separate review/case family?
- Should
orgeventually use a sibling artifact or the same family withsubject/kind? - Which operation ids should be elevated into a more formal registry first?
Implementation Status¶
| ID | Feature | Status | Notes |
|---|---|---|---|
| P018-01 | Canonical participant-capability-limits.v1 schema and Node contract mirror |
done | The schema is synchronized into Node protocol contracts and closed at the top-level security gate. |
| P018-02 | Schema-gated daemon import | done | Invalid JSON, oversized local bodies, invalid restriction records, already-dead or already-expired hard blocks, stale recorded-at overwrites, stale imports behind a clear tombstone, invalid participant ids, unsafe reason/ref values, and out-of-range soft factors are rejected before mutating runtime state. |
| P018-03 | Hard-block enforcement for first privileged operation set | done | Procurement and response operations fail closed when an active hard block matches the operation. |
| P018-04 | Protected-floor enforcement | done | core/messaging, keepalive, dispute/file, ubc/claim, and signal-marker/send are rejected at schema level as hard-block targets; runtime keeps signal-marker/send and participant-side dispute/file admissible. |
| P018-05 | Soft priority-factor ranking penalty |
done | Procurement offer scoring consumes the imported participant restriction state deterministically. |
| P018-06 | Soft rate-limit-factor cooldown hook |
done | The daemon applies per-participant operation cooldowns for the current admitted operation set. |
| P018-07 | Durable daemon replay | done | Imports and clear tombstones are appended to the daemon commit log; replay preserves monotonic recorded-at and last_cleared_at read models. |
| P018-08 | Operator control-plane surface | done | Local HTTP exposes import, schema-gated list/detail export, and clear operations; clear accepts optional bounded reason/ref, and import/clear emit metadata-only SSE refresh events. |
| P018-09 | Participant-scoped lifecycle controls stay separate from operator execution actions | done | Participant accept/dispute/reject surfaces remain distinct from infrastructure-operator execution controls. |