Proposal: Service Object Capabilities
Status: Superseded by Session-Bound Invocation Context. This document remains as historical design context for the already-landed synthetic routing/lifecycle proof. Do not continue the subject/proof root-opening or shared-service service-object migration from this proposal.
Replace caller-selected endpoint identity with service-minted object capabilities.
Problem
Endpoint client metadata currently carries service identity. A client endpoint is a capability plus a caller-visible numeric tag; services can use that tag as a member, session, role, connection, or actor key. That is too close to a permission bitmask or ambient label: the generic IPC substrate accepts an untyped number, and each service has to assign security meaning by convention.
The pre-containment problem became concrete through shell spawn syntax. A shell
that held a delegated chat client endpoint could request:
run "chat-client" with { chat: client @chat badge 200 }
The launcher path could then pass the child a client facet with a different value than the shell originally held. Gate 0 now rejects that relabeling for ordinary delegated client facets, but the chat example remains the reason the broader migration exists: service authority should not depend on a caller-selected numeric tag.
capOS needs multi-client services, per-client state, service-created attenuation, audit subject binding, and shell-spawned children. It does not need caller-selected numeric identities.
Goals
- Make the capability object itself carry the service authority.
- Keep endpoint transport generic while avoiding generic service roles, permission bits, or caller-selected labels.
- Let services expose many logical objects through one resident process.
- Let services bind subject/audit information at object creation time without putting identity policy into the kernel IPC fast path.
- Preserve explicit transfer semantics: copy and move pass the same object authority unless a trusted minter creates a new one.
- Provide a staged migration path for current chat, adventure, stdio, and endpoint smokes.
Non-Goals
- A POSIX credential model.
- PID, PID@host, UID, role strings, or host names as service authority.
- Kernel interpretation of chat rooms, moderators, players, sessions, or principals.
- Generic per-capability permission bitmasks.
- Full network-transparent object references in the first slice.
Design
An Endpoint remains a transport queue owned by a server process. Ordinary
clients should not hold “endpoint plus badge”; they should hold a capability
to a service object exported by that server.
Design Grounding
Project files read for this design:
docs/capability-model.mddocs/architecture/ipc-endpoints.mddocs/proposals/service-architecture-proposal.mddocs/proposals/shell-proposal.mddocs/proposals/interactive-command-surface-proposal.mddocs/backlog/stage-6-capability-semantics.mddocs/backlog/runtime-network-shell.mddocs/backlog/shared-service-demos.mddocs/security/trust-boundaries.mddocs/authority-accounting-transfer-design.mdREVIEW_FINDINGS.md
Research files checked from the actual docs/research/ contents:
docs/research/sel4.mddocs/research/eros-capros-coyotos.mddocs/research/genode.md
The design deliberately supersedes the prior seL4-style badge/mint direction for service identity. Genode’s RPC/session object model is a closer fit for capOS services: clients hold capabilities to service-created objects, while delegation passes the same object authority. EROS/CapROS/Coyotos and the authority-accounting design reinforce the rule that authority should remain in the capability graph, not in caller-selected numeric metadata.
Examples by service:
Chat service:
ChatRoot
ChatParticipant
ChatRoom
ChatModerator
Terminal/child I/O service:
StdIO
Adventure service:
AdventurePlayer
AdventureNpc
Each object capability has one interface ID and one server-selected receiver selector. The receiver selector is opaque to the client. It is not a user field, not shell syntax, and not a policy label. It exists only so the kernel can route the call to the resident server and the server can dispatch it to the right object state.
Conceptually:
service object cap = target endpoint + interface id + opaque receiver selector
Only trusted minting paths may create a new receiver selector:
- the endpoint owner/server,
- a supervisor or broker that holds explicit mint authority from the server,
- transitional manifest/init wiring for boot services.
Copying or moving a service object cap preserves the same receiver selector. An ordinary client cannot relabel a delegated cap into a sibling object.
Subject / Proof Binding
A service should be able to learn who or what a service object represents, but that subject must be bound through trusted issuance rather than caller payload claims.
The general shape is:
interface Subject {
deriveProof @0 (request :DelegationRequest) -> (proof :SubjectProof);
}
interface SubjectProof {
attest @0 (challenge :Challenge) -> (statement :SubjectStatement);
}
interface ServiceRoot {
open @0 (proof :SubjectProof, request :OpenRequest)
-> (object :ServiceObject);
}
interface ServiceObject {
call @0 (request :Request) -> (response :Response);
}
UserSession is the interactive-user case and can derive a proof scoped to a
service root, request digest, audience, and freshness window. A service account,
workload identity, broker-issued proof, anonymous session, guest session, or
other typed subject cap can fill the same role when that is the right trust
boundary. The root/factory validates the proof through a verifier, broker,
account, audit, or application policy interface it was granted, stores verified
metadata in its own object table, and returns a service object cap.
Later calls on the returned object do not need caller-supplied identity. Possession of that object cap is the authority. The service can still record principal/session audit identifiers, display names, channel memberships, quota/accounting state, moderation state, workload labels, or other policy metadata internally, but those records are service state, not endpoint metadata that the caller can edit.
Example Chat Shape
The current chat service uses one Chat endpoint and maps legacy endpoint
metadata to members. The target model is a root/factory plus participant
objects.
interface ChatRoot {
join @0 (channel :Text, handle :Text, session :UserSession)
-> (participant :ChatParticipant);
}
interface ChatParticipant {
join @0 (channel :Text) -> (joined :Bool);
leave @1 (channel :Text) -> (left :Bool);
send @2 (channel :Text, text :Text) -> (sent :Bool);
who @3 (channel :Text) -> (members :List(Text));
poll @4 (maxEvents :UInt16) -> (events :List(ChatEvent));
close @5 () -> ();
}
interface ChatModerator {
kick @0 (participant :ChatParticipant, channel :Text) -> (kicked :Bool);
}
ChatParticipant is the participant authority. If a child process receives
that cap, it acts as that same participant. It cannot type another receiver
selector and become another participant.
Moderator behavior is a separate cap/interface. The service may internally associate participant and moderator state with the same subject, but the kernel does not provide a role field and the client does not choose one.
Chat does not need to know about AdventurePlayer or AdventureNpc.
Adventure-specific caps belong to the adventure service. Room speech should
cross into chat through ordinary chat object caps such as ChatParticipant or
a future room-scoped chat object; the chat service should see chat subjects
and channels, not adventure interfaces.
Kernel Contract
The kernel should enforce object-cap invariants and avoid service semantics.
Required invariants:
- Only endpoint owners or explicit mint-authority holders may create a service object cap for a new receiver selector.
- Delegating an existing service object cap preserves the receiver selector.
- Process spawning may copy or move service object caps but may not relabel them.
- Client-held object caps cannot receive or return endpoint messages unless their interface explicitly grants server authority.
- Receiver selectors are scoped to the target endpoint object; no global numeric namespace is part of the ABI.
- Process exit and cap release still drive endpoint cleanup for queued calls, in-flight returns, and server-visible cancellation.
The first compatibility step can keep the current u64 storage field but
change the rules: a delegated client endpoint’s numeric identity is preserved
on re-delegation. The target step renames and narrows the concept from
badge to an opaque receiver selector for service object caps.
Shell And Launcher Contract
Shell users should launch applications, not assign service identities.
Target user shape:
run "chat-client"
run "adventure-client"
Prototype explicit-grant shape while migration is incomplete:
run "chat-client" with { stdio: client @stdio, chat: @chat_participant }
The normal shell must not expose badge N as user-facing authority syntax.
If a grant parser keeps legacy badge syntax for manifest or smoke migration,
the kernel must still reject any delegated-client relabeling.
Omitting a badge in shell syntax preserves the source identity; low-level
legacy badge-zero encodings remain hostile-test inputs and must still fail
closed for nonzero delegated client facets.
External And Network Boundaries
External identity assertions do not open service objects directly. OIDC ID
tokens, passkey assertions, certificate chains, cloud workload tokens, and
remote gateway-authenticated claims first pass through an admission service
that normalizes provider kind, issuer, tenant, and subject; maps the result to
a local or pseudonymous principal when policy allows; and mints a local
subject/proof capability. Imported groups, roles, tenants, acr, amr, device
posture, source network, and token age are ABAC inputs to that mint decision,
not downstream object authority.
Network-transparent capability transport is also out of the first slice. A future bridge should maintain connection-local export/import tables and expose broken-reference semantics on disconnect. It must not serialize local cap-table handles, endpoint generations, receiver selectors, or server cookies as portable authority. Persistent restore, if needed, should go through a capability-bearing naming or persistence service that authorizes and mints a fresh live object.
Migration Plan
The current execution plan lives in
docs/backlog/service-object-identity-migration.md and uses four large chunks.
Gate 0 containment below is already historical substrate. It does not mean the
service-object model is implemented. This proposal records the design sequence;
the backlog owns task breakdown and verification gates.
0. Contain delegated-client relabeling, landed
The kernel and shell paths now reject ordinary delegated-client relabeling. This is containment, not the final model.
1. Core service-object routing and lifecycle, landed
Commit a4655f0 at 2026-04-28 14:10 UTC added the synthetic QEMU service
proof. It covers trusted serviceObject minting, receiver-cookie routing,
copy/move IPC transfer, nested spawn delegation, generation-checked service
receiver cookies, close/revoke rejection, and stale-cookie rejection after
record reuse.
2. Subject/proof root opening
Validate local subject/proof authority before object mint. External assertions must first normalize through admission into local or pseudonymous subject/proof caps.
3. Convert shared-service demos
Move chat, adventure, and stdio/terminal child bridges from caller-selected endpoint identity to root/factory-opened service object caps.
4. Retire legacy endpoint identity
Remove compatibility syntax and rename internal fields once normal smokes no longer depend on caller-selected endpoint identity.
Security Notes
This design keeps the kernel out of role and identity policy. The kernel only knows whether a caller holds a particular object cap and whether transfer rules allow that cap to move. Services decide what their object records mean.
PID, PID@host, and process names are diagnostics. They are not authority: process IDs recycle, hosts need cryptographic naming for federation, and a single subject can legitimately hold multiple service objects with different authority.
The broker and session services remain the right place to validate subjects and policy before a service object is minted. After minting, the object cap is the authority.