Proposal: Session-Bound Invocation Context
Replace caller-selected endpoint identity and the Service Object Identity Migration with a simpler invariant: every process runs in exactly one live session context. The kernel attaches that context to invocations and enforces privacy/transfer invariants, but does not reveal subject details to endpoint servers unless the call explicitly requests disclosure and policy allows the requested fields through a broker/service disclosure scope.
Capabilities decide what a process may call. The calling process’s session context says who invokes, subject to privacy rules. Services receive only the minimum routing/privacy metadata required by the invoked capability; request fields remain ordinary data and must not select authority or caller identity.
Problem
The prior service-object direction fixed a real bug: clients must not be able to choose a service-visible numeric badge during spawn or IPC delegation. The design then added service-minted object capabilities and a subject/proof open protocol so services could bind identity without trusting request payloads.
That is too much machinery for the intended capOS process model. Normal workload processes should not be bags of unrelated user sessions. They should have one immutable session context, assigned at spawn, and all invocations from that process should be attributable to that context. Delegated-subject on-behalf-of behavior is a separate design and is intentionally out of this first implementation path.
The target should therefore remove the caller-selected badge without replacing
it with a second service-object identity system. For a service such as chat,
holding ChatRoot already means the process may attempt to join chat under its
own session. More granular authority can come from narrower capabilities
granted by AuthorityBroker, not from client-selected receiver selectors or
local proof tokens on every open call.
Decision
capOS adopts these invariants:
- Each process has exactly one immutable
SessionContext. - The session context is assigned at spawn and shared by all threads in that process.
- System services run under explicit service/system sessions.
- Network gateways create or select a session for each admitted connection and spawn per-session workers or shells; they do not run multiple user sessions as ambient subject context inside one ordinary workload process.
- Endpoint CALL delivery includes a privacy-preserving caller-session reference and optional freshness result, not full subject metadata by default.
- A held capability is the authority to invoke service root methods such as
ChatRoot.join; the caller session supplies the invocation subject context. Services learn principal, profile, or display metadata only through explicit disclosure. - Request fields such as
user,role,participant,principal, orsessionare data. Services may validate them against the caller session, but they do not identify the caller or authorize by themselves. - Subject disclosure is opt-in and policy-bounded. A call must explicitly ask for disclosure, and the requested fields must be allowed by a service-specific disclosure capability/scope. Without both signals, the server gets only an opaque session-local handle suitable for same-session state and audit correlation within that service.
- Cross-session capability transfer is supported when the transferred cap’s transfer scope permits it. The transferred cap carries invoke authority; the receiver’s session remains the invocation subject. Session-local caps require an explicit broker or service regrant operation.
The existing synthetic service-object routing proof remains useful as evidence that request bytes cannot spoof endpoint receiver metadata, but the service object identity model is no longer the active design direction.
Normative Invariants
- Every normal workload process has exactly one immutable
SessionContext. SessionContextis installed only by trusted spawn, session-manager, or broker paths; request payloads, shell strings, manifest data, endpoint receiver metadata, and copiedUserSessioncaps cannot mutate or replace it.- Capability possession remains the authority to invoke an interface. A live session without the target capability cannot call the target service.
- A normal endpoint call from a dead, revoked, or stale workload session fails closed, except for explicitly designated recovery, logout, or renewal caps.
- Endpoint default delivery never includes global principal, profile, account, role, tenant, external-claim, auth-factor, display-name, or source-network fields.
- Subject-detail disclosure requires both an explicit method/call disclosure request and a matching service-scoped disclosure scope.
- Disclosure is field-granular and service-scoped; an opaque session reference from one service is non-portable and non-authority-bearing in another.
- Cross-session raw cap transfer is rejected unless the cap’s transfer scope permits it.
- After an allowed cross-session transfer, the receiver process session is the invocation context; raw transfer never implies act-on-behalf-of source session semantics.
service_regrant_onlycaps cannot cross sessions through raw copy, move, IPC, or spawn grants. A service or broker regrant path must mint the target session authority explicitly.- Legacy receiver metadata remains internal transport state. It must not be user-facing syntax, manifest policy, subject disclosure, or service identity.
Authority And Context
Capability possession answers one question:
May this process invoke this capability/interface at all?
It does not answer:
Which live session is this invocation attributable to?
Is that session still fresh?
Which resource/profile bucket should pay for server-side state?
What subject facts may this service learn?
May this capability be transferred into another session?
Those are invocation-context and disclosure questions. The split is deliberate.
ChatRoot can mean “the holder may ask chat to join”; it does not by itself
tell chat whether the call is from an operator, a guest, an anonymous Telnet
session, or an expired session, nor whether chat may see a global principal id.
A service decision has three layers:
capability authority
+ invocation subject context
+ service-local policy/state
Only the first layer is authority to invoke. The session layer supplies information about who invokes, freshness, resource/accounting labels, and what may be disclosed to the service. Service-local policy may accept or reject the operation based on that information, but the session context is not a second capability.
Examples:
ChatRootmeans the holder may ask chat to join, subject to chat policy and whatever session facts the call explicitly requests and broker/service policy makes available to chat.ChatModeratormeans the holder may call moderator methods, again under the caller’s live session.TerminalSessionmeans the holder may read/write that terminal endpoint, but audit and policy still see the process session.
Session-bound invocation context exists so services can make those second-order decisions without trusting payload fields and without forcing the kernel to reveal private subject metadata to every endpoint server. The kernel can say “this call came from a live session and here is an opaque service-scoped reference”; the service or broker can decide whether that is enough, whether a guest-specific facet is required, or whether the user must explicitly disclose bounded subject facts.
The kernel enforces capability possession, process session assignment, and disclosure invariants. It may report freshness/liveness as invocation context. Session expiry should bound behavior through capability lifecycle, broker refusal, or service policy, not by treating the session context itself as a second authority. The kernel still does not interpret chat rooms, handles, moderator state, adventure players, account roles, OIDC claims, or tenant groups.
Privacy And Disclosure
Session-bound invocation context must not become ambient subject leakage. A service should not receive global principal identifiers, account names, display names, profile names, external issuer keys, group claims, auth factors, source network, or tenant metadata merely because a process called an endpoint.
The default endpoint metadata is privacy-preserving:
caller_session_ref = opaque, service-scoped, non-portable reference
session_live = true/false or epoch/freshness result
That is enough for a service to keep per-session state, reject stale sessions, and correlate its own audit events without learning a broader identity.
Current proof implementation:
scoped_ref: low 64-bit ABI field of the opaque reference
scoped_ref_hi: high 64-bit ABI field of the opaque reference
epoch: u64
derivation: HMAC-SHA256 with an entropy-backed boot key, a non-reused endpoint
service-scope id, and the kernel session id
The ABI layout is preserved, but the old unkeyed low-half value is not. Both
scoped_ref and scoped_ref_hi are halves of the keyed opaque reference.
epoch is a separate domain-separated keyed value so service-local
freshness/audit correlation rotates with the same boot key and endpoint scope
without being folded into the opaque reference itself.
Current caller_session_ref derivation rules:
width:
128 bits minimum for the opaque reference, separate from freshness epoch.
derivation:
keyed opaque value over boot secret, service scope, and kernel session id.
scope:
a non-reused endpoint service-scope id plus the boot-scoped key. Endpoint
object replacement or boot-key replacement intentionally rotates the
reference. Stable service-audit identity across upgrades remains future work.
reuse:
logout/login or session recreation gets a new kernel session id and therefore
a new service-scoped reference.
stale epoch:
stale references may remain recognizable to the same service for bounded
audit/denial correlation, but they must not become live again after expiry.
service move/upgrade:
endpoint replacement currently breaks correlation. Retaining correlation
across service replacement requires a future stable service-audit scope.
privacy:
global principal, account, profile, display name, auth source, and tenant
metadata are not derivable from the opaque reference without broker/audit
disclosure authority.
Richer disclosure requires both an explicit act and an allowed policy scope:
- the client calls a method whose contract requests disclosure, such as
ChatRoot.join(discloseProfile = true, handle = "alice"), or transfers aSessionDisclosurecapability as part of that call; AuthorityBrokeror service policy grants a root/facet with a matching disclosure scope, such as “chat may see display name and profile class”;- an administrator-configured system service may expose methods whose contract explicitly requests audit disclosure, but those methods still need bounded service policy for the fields they receive.
Disclosure should be minimized and service-scoped. A chat service may need a display name, guest/operator class, and per-service audit pseudonym. It does not need raw OIDC claims, credential identifiers, account-store records, or global principal ids unless a later policy explicitly grants that.
Session Context
A SessionContext is kernel-carried metadata minted through trusted session
creation paths and installed by ProcessSpawner:
SessionContext {
session_id,
principal_id,
principal_kind,
auth_strength,
policy_profile_id,
resource_profile_id,
created_at_ms,
expires_at_ms,
epoch,
}
The exact ABI can be smaller in the first implementation. The required properties are immutability for the process lifetime, a stable kernel-visible session id for enforcement, a service-scoped opaque reference for default endpoint delivery, and enough freshness metadata for brokers/services to fail closed or revoke/withhold capabilities when a session expires or is revoked. These conceptual fields may exist in trusted session storage. They are not endpoint-delivered default metadata. Endpoint delivery gets only a service-scoped opaque session reference and liveness/freshness result unless an explicit disclosure request and matching disclosure scope allow named fields.
The session context is not a replacement for capabilities. A process with a
valid operator session but no ChatRoot cannot join chat. A process with
ChatRoot but an expired session should lose or fail to refresh the
capability authority that was issued for that session.
Kernel Contract
The kernel should enforce generic mechanics only:
- A process has one session context pointer or compact session descriptor.
- Spawning a child requires selecting the child’s session context. The default is to inherit the parent’s session; creating a different session is broker or session-manager capability authority.
- Session expiry is represented as freshness metadata and capability lifecycle:
normal workload endpoint calls from dead, revoked, or stale sessions fail
closed except for explicit recovery, logout, or renewal caps. The current
implementation rejects stale normal endpoint invocations before transfer
preparation or enqueue, rejects fresh shell-bundle minting for stale sessions,
and expires retained broker-issued non-endpoint shell bundle caps at their
bound session boundary.
RestrictedLauncherrejects spawn/list calls after the session it was minted for expires, and broker-issuedSystemInforesults are session-bound wrappers. The session context itself is not the authority being invoked. - Endpoint delivery includes privacy-preserving caller session metadata alongside the existing method, params, transfer descriptors, and result target. It must not include subject details unless the SQE/method contract explicitly requests them and a granted disclosure scope permits them. The current implementation uses a CALL SQE disclosure mask intersected with cap-held disclosure scope for field-granular delivery; unsupported fields are rejected or narrowed, and global principal ids and display names remain absent from default endpoint metadata.
- Capability transfer checks session scope. Same-session transfer preserves the held cap. Cross-session transfer is rejected unless the cap is explicitly cross-session-shareable or the transfer is the result of a broker/service delegation method.
- Legacy receiver metadata remains transport state only. It must not be exposed as user-facing identity syntax, manifest policy, service capability, or a workaround for subject disclosure.
The kernel should not validate external tokens, parse account stores, evaluate roles, or choose application objects.
Broker And Service Contract
AuthorityBroker and related session services decide which capabilities a
session receives:
SessionManager.login/guest/anonymous -> UserSession metadata/control cap
trusted broker/session-manager spawn path -> child SessionContext
AuthorityBroker.shellBundle(session) -> launcher fixed to that SessionContext,
ChatRoot, SystemInfo, ...
For basic local service access, no additional subject/proof token is required. The process session context supplies caller information and a default service-scoped session reference, and the held capability supplies access to the service. Human-readable or policy-rich subject details are separate disclosure, not automatic endpoint metadata.
UserSession remains useful as an informational/control capability and broker
input. It is not itself the ambient invocation subject, and copying it into a
process cannot install a second process session. A trusted broker or
session-manager path may use a verified UserSession to spawn a child with a
matching immutable SessionContext; ordinary cap transfer only transfers that
capability object.
External assertions still stop at the admission boundary. OIDC, passkey,
certificate, cloud workload, or SSH-authenticated claims are validated by
admission/session services, normalized into a local or pseudonymous session,
and then disappear from ordinary application calls. Chat should not parse OIDC
claims, and ChatRoot.join should not require a bearer proof object merely to
learn who the caller is.
Chat Flow
The target chat flow is:
login/setup/guest
-> UserSession metadata/control cap
trusted broker/session-manager spawn path
-> child process with SessionContext(operator or guest)
AuthorityBroker.shellBundle(session)
-> ChatRoot if the profile may use chat
spawn chat-client with inherited session and ChatRoot
chat-client:
ChatRoot.join(channel = "general", handle = "alice")
The kernel delivers the endpoint call with privacy-preserving caller session metadata:
target = ChatRoot
method = join
caller_session_ref = chat-scoped opaque session reference
session_live = true
payload = { channel = "general", handle = "alice" }
chat-service checks:
- the caller holds
ChatRoot; - the caller session is live;
- the requested channel and handle are syntactically valid request data.
Then it stores service-local state keyed by the caller session:
ParticipantRecord {
caller_session_ref,
service_assigned_member_label,
optional_disclosed_display_name,
joined_channels,
quota_bucket,
audit_context,
}
If chat needs to distinguish operator from guest, use explicit disclosure with
a matching disclosure scope. If chat only needs narrower behavior, the broker
may grant GuestChatRoot with behavior that encodes the policy without
revealing subject fields. The service should not receive the global principal
id by default.
Later calls can use the same root/facet capability:
Chat.send(channel = "general", text = "hi")
Chat.poll(max_events = 32)
Chat.who(channel = "general")
If the service permits multiple handles for one session, it may return a
server-issued participant_id as data. That id must be scoped to the caller
session and validated on every use:
Chat.send(participant_id = 7, channel = "general", text = "hi")
participant_id = 7 is not transferable authority. A different session cannot
use it unless chat or the broker performs an explicit share/delegation
operation.
Moderator behavior is a narrower capability, not a generic role bit in a payload:
AuthorityBroker.shellBundle(operator_session) -> ChatModerator
ChatModerator.kick(participant_id, channel)
The call still carries the operator session for audit and policy.
Transfer Rules
Same-session delegation is ordinary capability transfer:
operator shell -> child helper in the same session
transfers ChatRoot or ChatModerator
The child acts under the same session context, so no subject ambiguity exists.
Cross-session transfer is where the distinction matters most:
capability transfer carries authority to invoke;
the receiver process session supplies who invokes.
If session A transfers a cap to session B and the transfer is allowed, later calls are made by session B, not by session A. The service sees the transferred capability as the invoked authority and session B as the invocation subject context. It must not infer that session B is impersonating session A merely because the cap originally came from A.
This is acceptable for caps whose semantics are deliberately shareable, such as a read-only document, a public chat invite, or a scoped terminal endpoint intended for handoff. It is wrong for caps that encode session-local standing, such as “my chat participant”, “my account settings”, or “my active adventure player”, unless the service explicitly defines what sharing means.
Therefore caps need an explicit transfer scope:
same_session: may move/copy only to processes with the same session context;cross_session_shareable: may be transferred to another session and then invoked as the receiver’s session;service_regrant_only: cannot be raw-transferred across sessions; the holder must ask the service or broker to issue a new cap for the target session.
Session-local services that want to share state across sessions should use an explicit regrant/share path:
Chat.share(participant_id, target_session_or_invitation)
AuthorityBroker.delegate(source_session, target_session, requested_cap)
The service or broker records the policy decision and mints or grants the appropriate capability for the target session. Raw transfer of a session-scoped cap across sessions must fail closed unless the cap has an explicit cross-session-shareable scope.
This keeps privacy and accountability aligned. The transferred cap is not a portable identity token for the source session. If the receiver invokes it, the receiver’s session context is used for audit/disclosure by default. If the service needs to preserve source attribution, it should encode that as service-local state during an explicit share/regrant operation, not rely on the kernel to attach source-session subject data to future receiver calls.
The useful matrix is:
cap transfer only:
receiver gets authority to invoke;
receiver invokes as its own process session.
service regrant:
service or broker issues a new target-session capability;
future calls still invoke as the target process session.
What Happens To Service Object Routing
The synthetic service-object routing proof added in commit a4655f0 should not
drive the next design step. Its useful artifacts are narrower:
- delegated-client relabeling is contained;
- receiver-cookie spoofing through request bytes is tested;
- close/revoke/stale-cookie paths have coverage;
- internal receiver metadata can be generation-checked.
Those mechanics can remain as low-level transport tests. They are not the application authority model. The active migration should stop before subject/proof root opening and shared-service conversion to service object capabilities.
Migration Plan
- Record this proposal as the active Stage 6 direction and mark Service Object Identity Migration as superseded.
- Add the kernel/process invariant: every process has exactly one immutable session context, including explicit service/system sessions.
- Thread caller session metadata through endpoint CALL delivery.
- Define session freshness propagation and the cap lifecycle rule needed to close the open review finding: expired sessions must not continue to receive or refresh interactive capability authority.
- Define cap transfer scopes for
same_session,cross_session_shareable, andservice_regrant_only. - Replace chat’s legacy receiver-selected member identity with session-keyed
participant state and broker-granted
ChatRoot/ChatModeratorfacets. The first chat migration is implemented for ordinaryChatmembership: member records are keyed by the endpoint caller-session key, visible member labels are service-assigned, and join handles remain non-authority request data. - Apply the same pattern to adventure and terminal/stdio bridges. The first Aurelian migration is implemented for adventure player state: ordinary player records are keyed by live endpoint caller-session metadata instead of receiver badges. Terminal output now requires live caller-session dispatch, and shell-serviced stdio bridge waits bind to opaque live caller-session metadata while rejecting mismatched callers.
- Retire user-facing badge/receiver selector syntax. Keep receiver metadata only as internal endpoint transport state or hostile-test fixture.
Non-Goals
- Reintroducing POSIX
uid/gidauthorization. - Allowing clients to choose identity through request bytes.
- Making external tokens ordinary application-service credentials.
- Delegated-subject or act-on-behalf-of semantics; those belong in a separate proposal and should not block this first implementation path.
- Preserving Service Object Identity Migration as the active design.
- Building network-transparent object references in this slice.
Open Questions
- Whether all caps are
same_sessionby default, or whether every cap entry should carry an explicitsame_session,service_regrant_only, orcross_session_shareablescope. - How much session metadata should be copied into endpoint delivery headers
versus looked up by
session_idin a kernel/session table. - Whether multi-connection gateways must always spawn per-session workers, or may multiplex unauthenticated transport while delegating all session-bearing work to child processes.