Rejected Proposal: Endpoint Badges as Service Identity
Status
Rejected. This was the short-lived seL4-style model where a capability hold
edge carried a u64 badge and endpoint servers used that badge as the
service-visible caller identity.
The model was superseded by Service Object Capabilities, which reframed the badge field as an opaque receiver selector owned by a service object capability. That proposal is also superseded: the active direction is Session-Bound Invocation Context, where each process has one immutable session context and endpoint calls expose privacy-preserving caller-session metadata instead of caller-selected badges or service-object identity migration.
This document records what badges were, how they were intended to be used, what was implemented, and why the design was rejected.
Proposal
Add a word-sized badge to each capability hold edge and deliver that value to an endpoint server whenever the holder invokes the endpoint. Multiple clients could therefore share one endpoint object while the server still distinguished them:
endpoint object
client cap hold badge 100 -> chat participant 100
client cap hold badge 200 -> chat participant 200
client cap hold badge 302 -> adventure player 302
The model came from seL4’s endpoint badge and mint pattern. A trusted holder of an endpoint owner capability could mint differently badged client facets for children or services. Copy and move transfer preserved the badge, so delegation kept the same service-visible identity unless a trusted mint path created a fresh one.
Intended Use
Badges were intended to solve a real early shared-service problem: chat, adventure, stdio bridges, and endpoint smokes needed more than one logical client on a resident service endpoint. Creating one kernel endpoint per client was unnecessary overhead for the demo stage, and putting a caller name or role in request bytes would have been trivial to spoof.
The intended rules were:
- a badge is not a generic rights bitmask;
- a badge is hold-edge metadata, not part of the endpoint object;
- endpoint CALL delivery reports the invoked hold badge to the server;
- copy and move transfer preserve the badge;
- raw spawn grants preserve the source badge;
- endpoint owners and ProcessSpawner-created parent endpoint result facets may mint a requested child client badge;
- delegated client facets may be passed on only with the same badge.
Under that model, a chat server could key membership by badge and an adventure server could key per-player room/inventory state by badge. The badge was meant to be server-visible caller identity, not a user-facing permission flag.
Implementation Specifics
The concrete implementation landed in several steps:
- Commit
3ee5240(feat: propagate endpoint capability badges, 2026-04-22) addedCapRef.badgeto the manifest schema, parsed optional CUEbadgefields, stored the value inCapHold, and changed endpoint CALL dispatch socall.badgecame from the invoked capability slot. The cross-process IPC smoke asserted a nonzero badge on RECV and RETURN completions. - Commit
df0d140(feat: add spawn grant badge attenuation, 2026-04-22) addedCapGrant.badgeto the ProcessSpawner ABI. Raw grants failed closed if the requested badge differed from the source hold.ClientEndpointgrants could mint the requested badge only from an endpoint owner source. The init spawn proof printed[init] Spawn badge attenuation ok.after exercising the path. - Commit
2face05(demos: extract badged endpoint service loop, 2026-04-24) extractedserve_badged_endpointintodemos/service-common/. The helper performed endpoint RECV, released unexpected transferred caps, decoded params, and called service handlers ashandle_request(state, badge, method_id, params). - Chat and adventure used that helper to route per-client service state by
badge. Manifest examples such as
system-chat.cueandsystem-adventure.cuecarried explicit badge values for shared-service clients and NPC/client identities. - Commit
3e59540(fix: narrow endpoint result badge minting, 2026-04-25) stopped treating every endpointResultCapas trusted badge minting authority. Only endpoint owners and ProcessSpawner-created parent endpoint result facets retained mint authority; ordinary IPC result transfers stayedResultCapand could not become a badge-mint path. - Commit
f955cd5(fix: reject delegated endpoint relabeling, 2026-04-25) fixed the first containment failure: already-delegated client facets could no longer request a different badge throughClientEndpointspawn grants. - Commit
a64c216(spawn: preserve delegated endpoint identities, 2026-04-25) fixed the shell/defaulting case. Omitted shell badge syntax began preserving the source badge viaPRESERVE_CLIENT_ENDPOINT_BADGE = u64::MAX, while explicit relabel attempts and low-level legacy badge-zero encodings failed closed for delegated client facets.
The final contained implementation still has a badge field in several ABI and
implementation structs. Current docs call it legacy receiver metadata or a
receiver selector when it is still needed for low-level tests, service-object
history, or non-identity parameters such as scoped TCP listen ports. It is no
longer the target identity model.
What Failed
The design gave too much meaning to an untyped number. Even when the kernel preserved badges across copy/move transfer, shell and spawn surfaces could still turn a caller-selected integer into service-visible identity unless every grant path handled mint authority perfectly.
The concrete failure was delegated endpoint relabeling. A shell holding a
delegated chat client endpoint could request:
run "chat-client" with { chat: client @chat badge 200 }
Before the containment fixes, that could produce a child client facet whose
service-visible identity differed from the delegated source. Omitted badge
syntax was also dangerous because the old parser defaulted it to badge 0,
which was another relabeling path for a nonzero source client.
The bug was narrow, but it exposed the wrong abstraction. The server was being
asked to treat a generic transport field as identity. The kernel could enforce
some mint rules, but the meaning of 100, 200, 302, or 0 lived in each
service by convention. That made ordinary shell syntax look like an authority
selector and made future network-backed shell exposure too easy to get wrong.
Rationale For Rejection
Endpoint badges are a useful low-level routing mechanism, but not a good service identity model for capOS.
Problems:
- Caller-selected identity pressure. The natural user-facing syntax was
client @service badge N, which invited users and tests to select service identity directly. - Untyped service semantics. The same
u64field could mean a chat member, an adventure player, an NPC, a stdio bridge, a TCP port, or a test fixture. The kernel could not validate those meanings. - Policy by convention. Each service had to remember whether a badge was a participant, a session, a role, a receiver cookie, or just a transport tag.
- Delegation hazards. Copy/move propagation was straightforward, but spawn minting needed subtle distinctions between endpoint owners, ProcessSpawner-created parent endpoint result facets, ordinary IPC result caps, and delegated client facets.
- Bad privacy shape. A server-visible endpoint field encouraged exposing stable caller identity by default, while the active model wants privacy-preserving session references and explicit bounded disclosure.
- Poor long-term composition. Cross-service and network-transparent designs need typed roots/facets, session context, transfer policy, and disclosure policy. A single badge value cannot carry those contracts.
The accepted historical fix was first to contain relabeling, then to stop treating badges as the target architecture. Service Object Capabilities moved identity into service-minted object capabilities and receiver selectors. That was still too much machinery for normal workload identity and was replaced by Session-Bound Invocation Context.
Replacement Direction
The active replacement is:
- capabilities answer whether the process may invoke a service at all;
- each process has exactly one immutable session context;
- endpoint delivery carries privacy-preserving caller-session metadata by default;
- richer subject disclosure requires an explicit request and a matching broker/service disclosure scope;
- shared services key user-facing state by broker-granted service capabilities plus service-scoped session references, not by caller-selected badges.
Legacy badge fields may remain as internal receiver metadata, hostile-test fixtures, or non-identity configuration encodings until the corresponding code paths are migrated. They should not appear as normal user-facing service identity syntax.
Design Grounding
Project files read for this post-mortem:
docs/capability-model.mddocs/architecture/ipc-endpoints.mddocs/proposals/service-object-capabilities-proposal.mddocs/proposals/session-bound-invocation-context-proposal.mddocs/authority-accounting-transfer-design.mddocs/research.mddocs/security/trust-boundaries.mdWORKPLAN.mdREVIEW_FINDINGS.md
Research files checked from the actual docs/research/ contents:
docs/research/sel4.md
The historical badge model followed the seL4 badge/mint precedent recorded in the repo research notes. The rejection is capOS-specific: schema-typed interfaces, session-bound process identity, broker-issued service authority, and privacy-bounded disclosure fit the project better than making a generic endpoint metadata word carry service identity.