JOLT REQUEST FOR COMMENTS · 0007

Capability-Scoped Local App Sessions

Jolt Project                                                JOLT-RFC-0007
Request for Comments: 0007                                  August 2026
Category: Experimental
Status: Experimental Draft
Updates: none
Obsoletes: none

Status of This Memo

This document specifies an experimental local authorization contract between Jolt applications and a Jolt daemon. It is not an IETF publication. Distribution of this memo is unlimited.

The v0 session store, capability parser, and HTTP surface are implemented. This memo remains a draft because app identity and device binding are transitional.

Abstract

This document defines capability-scoped sessions for untrusted local Jolt applications. An app requests authority for one Jolt identity. The trusted Jolt Console approves an equal or narrower set of capabilities. The daemon returns a bearer token and checks its status, expiry, identity, operation, and path scope before performing any action involving keys, signed state, storage, encryption, ingress, or the network.

Applications never receive long-term identity or device private keys. Localhost reachability alone grants no app authority.

1. Introduction

The Jolt daemon owns identities, signing keys, encryption keys, signed state, content storage, cache policy, and network access. A desktop or web application that can reach the daemon must not inherit all that authority merely because it runs on the same machine.

Jolt therefore separates three local trust classes:

Session checks occur before daemon operations. The protocol networking layer does not know app names, UI concepts, or session prompts.

2. Conventions and Requirements Language

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, and MAY are interpreted as BCP 14 [RFC2119] [RFC8174]. HTTP method and status semantics follow [RFC9110]. JSON member names use snake_case.

3. Scope

This memo defines:

This memo does not define:

4. Trust Classes and Terminology

App session
A local grant binding an app ID, user identity, local device, capabilities, status, and optional expiry to a bearer token.
App ID
A caller-declared local identifier. In v0 it is not cryptographic proof of an installed application.
Requested capability
Authority proposed by the app for user review.
Granted capability
Authority approved by Console. It MUST be equal to or narrower than a requested capability.
Admin API
The privileged loopback-only /admin/v1/* surface used by Console.
App API
The capability-checked /app/v1/* surface used by normal apps.

5. Session Data Model

The persistent v0 record contains:

AppSessionRecord {
  request_id: string,
  session_id: optional string,
  app_id: string,
  app_name: string,
  app_origin: optional string,
  requested_identity: optional IdentityId,
  identity: optional IdentityId,
  device_id: optional string,
  requested_capabilities: [string],
  granted_capabilities: [string],
  status: pending | active | rejected | revoked | expired,
  created_at: uint64,
  approved_at: optional uint64,
  rejected_at: optional uint64,
  revoked_at: optional uint64,
  expires_at: optional uint64,
  last_used_at: optional uint64,
  token_hash: optional hex(BLAKE3-256(token))
}

Secret bearer tokens MUST NOT appear in list or view representations. App name, origin, and app ID are display and policy inputs, not proof of code identity.

6. Session Lifecycle

The state machine is:

pending -> active -> revoked
        -> rejected
active  -> expired

An app creates a pending request. If no identity is supplied, the daemon binds the request to the currently selected local identity at request time.

Console may reject a pending request or approve it for one identity, an equal or narrower capability set, and an optional expiry. Approval creates a session ID and token and moves directly to active; there is no separate approved state.

Only active, unexpired sessions authenticate. Rejected, revoked, and expired records remain useful for identity-scoped history and diagnostics.

7. Capability Grammar

The v0 grantable grammar is:

resolve:public
fetch:public
ingress:send
ingress:read
ingress:decide
enumerate:self:<path-scope>
enumerate:any:<path-scope>
publish:<path-scope>
publish:encrypted:<path-scope>
inventory:<path-scope>
pin:own:<path-scope>
encrypt:<path-scope>
decrypt:<path-scope>

No other capability string is grantable. In particular, apps cannot receive private-key export, identity deletion, root/device rotation, arbitrary signing, grant approval, relay administration, security settings, or store-wipe authority.

Capability meanings are:

8. Path and Identity Scope

A path scope is either an exact absolute path or one trailing wildcard prefix:

/spoke/profile
/spoke/*

Scopes MUST begin with /, MUST NOT be /, contain whitespace, query, fragment, . or .. segments, and MAY contain at most one * only as the final /* suffix.

/spoke/* contains /spoke/posts/1 but not /spoke, /spoke-evil/x, or /profile. An approved scope must be contained by one requested scope of the same action. A requested prefix can be narrowed to an exact path or a narrower prefix; an exact request can only grant that exact path.

A requested enumerate:any may be narrowed to enumerate:self. A requested enumerate:self MUST NOT be broadened to enumerate:any.

Signing, inventory, pinning, encryption, and decryption operations use the session identity. An app MUST NOT substitute a different local identity in an operation body.

9. HTTP API Contract

9.1 Session Bootstrap

The following routes do not require an existing bearer token:

POST /app/v1/sessions/request
GET  /app/v1/sessions/{request_id}

The request body contains app ID, app name, optional origin, requested identity, and requested capabilities. Status polling returns state and, while locally available after approval, the newly issued token. Apps SHOULD store the token securely when first received.

9.2 Capability-Checked App API

GET  /app/v1/session                     active token
POST /app/v1/resolve                     resolve:public
POST /app/v1/fetch                       fetch:public
POST /app/v1/publish                     publish:<path>
POST /app/v1/append                      publish:<path>
POST /app/v1/enumerate                   enumerate:self|any:<path>
POST /app/v1/encrypted/publish           encrypt + publish:encrypted
POST /app/v1/encrypted/append            encrypt + publish:encrypted
POST /app/v1/encrypted/decrypt           decrypt:<path>
POST /app/v1/encrypted/open              decrypt:<path>
POST /app/v1/encrypted/rewrap            decrypt + encrypt + publish:encrypted
GET  /app/v1/published                   inventory:<path>
GET  /app/v1/ingress/pending             ingress:read
POST /app/v1/ingress/send                ingress:send
POST /app/v1/ingress/{id}/accept         ingress:decide
POST /app/v1/ingress/{id}/reject         ingress:decide
POST /app/v1/ingress/{id}/open           ingress:read
POST /app/v1/home-relay/pins             pin:own:<path>

When several capabilities are listed, every listed authority is required.

9.3 Admin API

Console lists, approves, rejects, and revokes sessions through /admin/v1/app-requests and /admin/v1/app-sessions. Listings and revocation are scoped to the currently selected local identity. Admin routes MUST remain loopback-only unless a future authenticated remote-admin RFC replaces that assumption.

10. Request Authorization

For every protected call, the daemon MUST:

  1. parse exactly one Authorization: Bearer <token> value;
  2. hash the token and find the matching persistent session record;
  3. require status active;
  4. expire the record if expires_at <= now;
  5. parse the required operation capability;
  6. require a matching granted action and containing scope;
  7. enforce self identity where applicable;
  8. require the session identity to be locally signable for local-authority operations;
  9. update last_used_at only after successful token authentication;
  10. perform the daemon operation without returning private key material.

An endpoint MUST NOT rely on UI behavior or app-supplied labels as an authorization check.

11. Token Generation and Storage

Request and session IDs contain 16 random octets rendered as lowercase hex with req_ and sess_ prefixes. Tokens contain 32 random octets rendered as:

jolt_app_<64 lowercase hexadecimal characters>

Randomness MUST come from a cryptographically secure operating-system source. The persistent store contains BLAKE3-256 of the full token string, never the plaintext token. Token comparison SHOULD avoid avoidable timing leakage.

The current daemon retains newly issued plaintext tokens only in process memory for status polling. Restarting does not invalidate a token already saved by an app, because its hash remains persistent, but may prevent the app from retrieving an uncollected plaintext token.

12. Identity and Device Binding

Every active session is bound to exactly one identity and one local device identifier. The same app requires distinct grants for different identities.

Capabilities involving signing, local inventory, owner pinning, encryption, or decryption may be approved only for an identity the daemon can currently act for. Public resolve/fetch authority does not imply local signing authority.

Device revocation MUST revoke active sessions bound to that device for future writes. The v0 implementation binds sessions to dev_legacy_root; binding to separately generated local device writers remains incomplete and is called out in Section 19.

13. Revocation and Expiry

Console revocation changes a matching active session to revoked and records the time. It applies only to the selected identity's session.

Device revocation invokes session revocation for records with the same device ID. Expiry is evaluated during token authentication; an expired token changes the persistent state to expired and is rejected.

Revocation stops later daemon authorization. It cannot undo already published signed state, remove cached content, or erase plaintext previously returned to an app.

14. Error Conditions

The HTTP surface SHOULD distinguish missing/invalid bearer token (401), unknown request/session (404), malformed or ungrantable request (400), identity not locally signable (400), capability/path denial (403), and daemon/network failure (5xx).

Authentication errors SHOULD avoid revealing whether a guessed token hash exists in another non-active state.

15. Compatibility and Versioning

Adding a new capability does not grant it to existing sessions. Apps MUST request it and users MUST approve it. Broadening the meaning of an existing capability is a compatibility and security change requiring RFC review.

Existing sessions do not silently inherit newly introduced enumeration or encryption authority. Path parsing changes MUST preserve strict containment or require reapproval.

16. Security Considerations

Bearer tokens are secrets with the authority of their grant. Apps and daemon logs MUST avoid exposing them. Local malware running as the user remains a threat; localhost is not a complete sandbox boundary.

The v0 app ID and origin are self-declared. Users SHOULD evaluate the visible capability request, not treat the app name as authenticated provenance.

Admin routes have no independent bearer authentication and therefore MUST be unreachable from non-loopback clients. Public ingress is intentionally separate and requires rate, size, and abuse controls.

17. Privacy Considerations

Session records reveal installed/used app names, origins, identity association, capability scopes, and activity timing to anyone who can read the local store or admin API. The store SHOULD use restrictive filesystem permissions.

Capability scoping reduces accidental cross-app and cross-identity disclosure but does not prevent an authorized app from retaining data it legitimately received.

18. IANA Considerations

This document requests no IANA actions. HTTP paths, token prefixes, and capability strings are project-local experimental identifiers.

19. Implementation Status

The persistent session state, lifecycle, identity-scoped Console listings, random bearer tokens, hashed storage, expiry, strict path capability parser, endpoint checks, encryption/ingress capabilities, and selected-identity isolation are implemented in jolt-server; Spoke exercises the app boundary.

Known gaps include self-declared app identity, no browser-origin proof, no remote-admin authentication, in-memory delivery of newly issued tokens, and transitional dev_legacy_root binding. In particular, generated device IDs and session device IDs are not yet universally aligned, so device revocation must be audited before this memo advances beyond Draft. The identity-scoped revoke operation does not require the session to be active first, so it also rewrites rejected, expired, or already revoked records to Revoked. Stored token hashes are not compared in constant time during bearer-token lookup. Work was tracked by cards 042, 052, and 095 and architecture document 15.

20. References

20.1 Normative References

20.2 Informative References

Appendix A. Spoke Grant Example

A Spoke session requests public resolve/fetch, /spoke/* publish, encrypted publish, inventory, pinning, encryption/decryption, self/any enumeration, and recipient ingress capabilities. Console may approve all or a narrower subset for one selected identity. Spoke receives only a bearer token. Signing and private keys remain inside the local daemon, and every later request is checked against that identity and capability set.