# Proposal: Session-Bound Invocation Context

Current design authority now lives in
[`docs/architecture/session-context.md`](../architecture/session-context.md),
with endpoint transport details in
[`docs/architecture/ipc-endpoints.md`](../architecture/ipc-endpoints.md). This
proposal is retained as the archival decision record for why capOS replaced
caller-selected endpoint identity and the service-object migration with
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`, or
  `session` are 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`.
- `SessionContext` is installed only by trusted spawn, session-manager, or
  broker paths; request payloads, shell strings, manifest data, endpoint
  receiver metadata, and copied `UserSession` caps 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.
- Session liveness is a revocable lease state, not only a timestamp embedded
  in immutable process metadata. A session may be live, logged out, revoked,
  expired, or recovery-only.
- Renewal must not relabel an existing process to a different session subject
  and must not blindly revive all previously issued grants. Renewal either
  extends the existing session liveness record under policy or returns fresh
  broker grants with distinguishable grant/session epochs.
- 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_only` caps 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:

```text
May this process invoke this capability/interface at all?
```

It does not answer:

```text
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:

```text
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:

- `ChatRoot` means 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.
- `ChatModerator` means the holder may call moderator methods, again under the
  caller's live session.
- `TerminalSession` means 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:

```text
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:

```text
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 a
  `SessionDisclosure` capability as part of that call;
- `AuthorityBroker` or 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`:

```text
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.

## Session Lifecycle, Logout, And Renewal

The completed milestone proves fail-closed stale-session behavior for current
shell and endpoint authority. Follow-up lifecycle slices now provide a
kernel-backed mutable liveness record for `SessionManager`-minted sessions,
remote gateway logout/close propagation, and endpoint RETURN cleanup for
already-admitted calls after caller logout/session death. Fixed wall-clock
expiry is still not a usable long-running interactive policy by itself:
production session lifecycle also needs revocation, renewal/recovery, live
proxy cleanup, audit reason separation, and a dedicated result-cap move-source
rollback proof. Clean local owner-shell exit now calls the held
`UserSession.logout()` before process exit; richer shell replacement and
renewal UX remains future work.

The intended liveness model is:

```text
SessionContext {
    session_id,
    principal_id,
    principal_kind,
    auth_strength,
    policy_profile_id,
    resource_profile_id,
    created_at_ms,
    liveness_cell_id,
}

SessionLivenessCell {
    session_id,
    session_epoch,
    state: live | logged_out | revoked | expired | recovery_only,
    not_before_ms,
    not_after_ms,
    policy_epoch,
    resource_profile_epoch,
    audit_record_id,
}
```

`SessionContext` remains immutable for the process lifetime. The liveness cell
is trusted session-manager state that can be logged out today and later
revoked, expired, or renewed. This preserves the one-session-per-process
invariant while allowing usable session renewal and explicit logout. A process
cannot install a different session id into itself; if policy requires a new
subject, the broker launches a replacement process or shell with a new
`SessionContext`.

This splits lifetime checks into three composable layers:

```text
session liveness:
    Is this process's invocation subject still live?

grant lease:
    Is this broker-issued bundle or individual grant still valid?

object/facet epoch:
    Has the target live object/facet generation been revoked or replaced?
```

The kernel or trusted wrapper caps should check session liveness before normal
endpoint enqueue, before local non-endpoint shell-bundle operations, and before
installing fresh result caps into a caller. Broker-issued caps may additionally
bind to grant leases. Service objects and endpoint-backed facets keep using
object epochs or service-specific revocation for target invalidation.
The current endpoint RETURN path rechecks caller liveness before copying result
bytes, application-exception payloads, result-cap records, or returned caps into
the caller; stale returns cancel the in-flight call and notify the caller with
invoke-failed when a completion can be posted.

Renewal is a narrow recovery operation, not generic authority resurrection:

- pre-expiry renewal may extend the liveness cell when account state, policy
  epoch, resource profile, auth freshness, and maximum lifetime permit it;
- post-expiry calls are limited to explicit logout, renewal, recovery, and
  bounded self-diagnostic methods;
- renewal returns fresh grant leases or wrapper caps when existing grants need
  a new policy decision;
- old ordinary grants do not become fresh merely because the session renewed;
- explicit revocation beats renewal except for a separately named recovery
  policy;
- password-authenticated local shells should default to explicit logout,
  terminal/connection close, process-tree exit, or administrator revocation
  rather than an unavoidable short wall-clock TTL. Idle lock, step-up, or
  renewal prompts are policy options, not kernel authority rules.

Logout and clean owner-shell exit close the liveness cell for sessions owned by
that shell or gateway through `UserSession.logout()`. Closing the shell process
still releases local cap table edges through process-exit cleanup, but session
logout is the operation that makes the session no longer live for retained
session-bound grants, children, and future broker decisions.

## 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. `RestrictedLauncher` rejects spawn/list calls after
  the session it was minted for expires, and broker-issued `SystemInfo` results
  are session-bound wrappers. The current endpoint RETURN path also rejects
  already-admitted returns after caller logout/session death before installing
  result bytes, application-exception payloads, result-cap records, returned
  caps, or move-source commits into the stale caller. The session context
  itself is not the authority being invoked. Remaining lifecycle work should
  extend the mutable liveness cell from logout to administrator revocation,
  recovery-only state, and pre-expiry renewal without relabeling a running
  process.
- 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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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:

```text
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 completed migration stopped before
subject/proof root opening and shared-service conversion to service object
capabilities.

## Migration Plan

1. Record this proposal as the selected Stage 6 direction and mark Service Object
   Identity Migration as superseded.
2. Add the kernel/process invariant: every process has exactly one immutable
   session context, including explicit service/system sessions.
3. Thread caller session metadata through endpoint CALL delivery.
4. 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.
5. Define cap transfer scopes for `same_session`,
   `cross_session_shareable`, and `service_regrant_only`.
6. Replace chat's legacy receiver-selected member identity with session-keyed
   participant state and broker-granted `ChatRoot`/`ChatModerator` facets.
   The first chat migration is implemented for ordinary `Chat` membership:
   member records are keyed by the endpoint caller-session key, visible member
   labels are service-assigned, and join handles remain non-authority request
   data.
7. Apply the same pattern to adventure and terminal/stdio bridges. Aurelian
   ordinary player state is keyed by live endpoint caller-session metadata
   instead of receiver badges. Terminal output requires live caller-session
   dispatch, and shell-serviced stdio bridge waits bind to opaque live
   caller-session metadata while rejecting mismatched callers. Focused
   adventure NPC/chat authority is broker- or manifest-issued rather than
   caller-chosen.
8. 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/gid` authorization.
- 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.
  Future remote-capability transport is grounded separately in
  [Spritely, OCapN, and CapTP](../research/spritely-captp-ocapn.md) and must
  preserve this proposal's local rule that sessions are broker/kernel-attached,
  not chosen by request bytes.

## Open Questions

- Whether all caps are `same_session` by default, or whether every cap entry
  should carry an explicit `same_session`, `service_regrant_only`, or
  `cross_session_shareable` scope.
- How much session metadata should be copied into endpoint delivery headers
  versus looked up by `session_id` in 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.
