Copy page
View as Markdown View this page as plain text

Users

Status: draft proposal. Not yet part of the frozen 0.1.0 surface.

A user is a scope owned by a runtime principal. It comprises durable values, packaged HTTP endpoints, an event stream, and credential grants. Threads reference users; the surfaces in this section are addressable independently of any thread.

1. Identity

  1. A runtime MUST resolve every authenticated request to a stable user id.
  2. A runtime with exactly one principal MUST resolve all requests to a single constant user id.
  3. A thread MAY have no user. For such threads, state.user() MUST resolve null and every surface in this section MUST be unavailable.
  4. The identity record exposes only the following fields. Credential material MUST NOT appear in it.
interface UserIdentity {
  id: string
  username?: string | null
  email?: string | null
  display_name?: string | null
  avatar_url?: string | null
  role?: string | null
  permissions?: string[] | null
}

2. UserState

User endpoints (§3) receive a UserState:

interface UserState {
  userId: string
  identity(): Promise<UserIdentity>

  getValue<T = unknown>(key: string): Promise<T | null>
  setValue(key: string, value: unknown): Promise<void>
  listValues(options?: { prefix?: string; limit?: number; offset?: number }):
    Promise<{ entries: { key: string; value: unknown; updated_at: number }[]; total: number }>

  env(name: string): Promise<string>

  emit(event: string, data?: unknown): void

  listThreads(options?: { agentId?: string; limit?: number; offset?: number }):
    Promise<{ threads: { id: string; agent_id: string; tags: string[]; created_at: number }[]; total: number }>

  fetch(grant: string, input: string | URL, init?: RequestInit): Promise<Response>

  /** Raise one of this user's threads at rest (see 2.2). */
  thread(threadId: string): Promise<ThreadState>
}

2.1 Serialization

  1. A runtime MUST serialize user endpoint executions (§3) and UserHandle.call() invocations (§5) per user: at most one executes at a time, and each observes the completed writes of all prior executions for that user.
  2. Value access outside user endpoints (UserHandle.getValue/setValue, pre-existing user-value APIs) is NOT serialized. Runtimes MUST NOT extend the §2.1.1 guarantee to those surfaces.

2.2 Reaching Threads

thread(threadId) resolves a ThreadState for a thread at rest — the same materialization thread endpoints receive (state.execution is null), without requiring the runtime to address itself over the network.

  1. thread() MUST reject for threads not belonging to the user.
  2. Operations on the resolved ThreadState execute in the thread scope; the §2.1 guarantee does not extend across them.
  3. A runtime MUST fail — not deadlock — a reentrant user-scope invocation (state.user().call()) made from code reached through thread() within the same user execution.

3. User Endpoints

User endpoints expose HTTP APIs for user-scoped operations. They resolve the authenticated caller’s user and provide a UserState instance to the handler.

3.1 Defining User Endpoints

// users/quota.post.ts
import { defineUserEndpoint } from '@standardagents/spec';

export default defineUserEndpoint(async (req, user, params) => {
  const used = (await user.getValue<number>('quota.used')) ?? 0;
  const limit = Number(await user.getValue('quota.limit')) || 0;
  const { amount } = await req.json();
  if (used + amount > limit) {
    return Response.json({ ok: false, used, limit }, { status: 429 });
  }
  await user.setValue('quota.used', used + amount);
  user.emit('quota_changed', { used: used + amount, limit });
  return Response.json({ ok: true, used: used + amount, limit });
});

3.2 File-Based Routing

User endpoint files live in the project or package users/ directory — a resource directory parallel to thread endpoints, tools, and hooks. Routing follows the thread-endpoint rules (Thread Endpoints §2) — method suffixes, nested directories, [param] and [*] segments — mounted at:

{runtime_prefix}/users/me/{endpoint_path}

Nested directories namespace endpoints; packages SHOULD root their user endpoints in a directory named for the package so installed packages coexist by convention:

users/standardcode/lease_acquire.post.ts  →  {runtime_prefix}/users/me/standardcode/lease_acquire
  1. me routes MUST require the authenticated caller to be the addressed user.
  2. A runtime MUST serve the same endpoints beneath users/{id}/ to callers authorized to administer that user.
  3. The runtime MUST identify the caller channel to the handler via the X-Standard-Agents-Caller request header — user (the user’s own HTTP request), admin (administrative addressing), or thread (a runtime-internal UserHandle.call()) — and MUST overwrite any inbound value on public routes so the channel cannot be forged. Endpoints enforcing runtime-only semantics (entitlements, quotas) gate mutations on the thread channel.
  4. Requests beneath the user endpoint prefix that match no route MUST return HTTP 404.
  5. A ThreadState MUST NOT be available to user endpoint handlers.
  6. Packed user endpoints share the route namespace with local user endpoints, exactly as packed thread endpoints do.

4. Event Stream

GET {runtime_prefix}/users/me/stream        (WebSocket upgrade)
GET {runtime_prefix}/users/{id}/stream      (administrative callers)
  1. emit(event, data) MUST deliver the frame { "event": string, "data": unknown } to every socket attached to that user’s stream, in emit order per user.
  2. Delivery is at-most-once. The stream carries no history and no acknowledgements.
  3. A runtime MUST accept multiple concurrent sockets per user, and MUST serve the stream beneath users/{id}/ to callers authorized to administer that user.
  4. Event names are package-defined. Packages SHOULD namespace event names.

5. UserHandle

ThreadState.user() resolves to a UserHandle — the UserIdentity fields plus:

interface UserHandle extends UserIdentity {
  call(endpoint: string, payload?: unknown): Promise<Response>
  getValue<T = unknown>(key: string): Promise<T | null>
  setValue(key: string, value: unknown): Promise<void>
  emit(event: string, data?: unknown): void
  fetch(grant: string, input: string | URL, init?: RequestInit): Promise<Response>
}
  1. call(endpoint, payload) MUST invoke the named user endpoint for the thread’s user under the §2.1 guarantee, and MUST NOT require the runtime to address itself over the network.
  2. call() MUST reject for user-less threads.
  3. The additions to the state.user() return value are additive; the identity fields are unchanged.

6. Credential Grants

A grant is a named, user-scoped credential with a declared use policy.

interface CredentialGrant {
  name: string
  kind: 'bearer' | 'header' | 'query' | 'basic' | 'signing'
  origins: string[]                  // exact origins or suffix wildcards
  inject?: { header?: string; format?: string; query?: string }
  allow?: { tools?: string[]; endpoints?: string[]; userEndpoints?: string[]; agents?: string[] }
  metadata?: Record<string, unknown>
}
  1. Secret material is write-only: it MUST NOT be readable by packaged code, models, or clients. Only metadata and grant existence are readable.
  2. fetch(grant, input, init) MUST inject the credential per the grant’s kind and inject policy on the runtime side.
  3. The runtime MUST reject a fetch whose destination origin is not covered by origins.
  4. The runtime MUST reject a fetch from a caller not covered by allow. An absent allow denies all callers.
  5. The runtime MUST NOT expose injected material through errors, logs, or traces reachable by packaged code.
  6. Deleting a grant MUST cause subsequent fetch calls to fail with an error distinguishable from a network failure.
  7. For kind: 'signing', the runtime exposes signatures produced with the credential; the key itself is subject to §6.1.

7. Lifecycle Hooks

A runtime SHOULD support the following user-scope hooks. They execute in the user scope, receive a UserState, are observational, and MUST NOT block the triggering operation.

HookSignature
user_client_attached(user: UserState, client: { id: string; kind?: string }) => void
user_client_detached(user: UserState, client: { id: string; kind?: string }) => void
user_value_changed(user: UserState, key: string) => void

User-scope hooks are defined like thread hooks — a file in the hooks/ directory exporting defineHook — but their scoping differs. Thread hooks attach to agents or prompts (Hooks §1.3); the user scope has neither, so a user-scope hook is project-scoped: it is active for every user of the project whenever its definition is present. Any number of definitions of the same hook type may exist; all of them execute, in an order the runtime does not guarantee.

// hooks/track_presence.ts
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'user_client_attached',
  id: 'track_presence',
  execute: async (user, client) => {
    user.emit('presence_changed', { attached: client.id, kind: client.kind });
  },
});

8. Conformance

#RequirementLevel
1Stable user id per principal; constant id on single-principal runtimes (§1)MUST
2UserState value surface (§2)MUST
3Per-user serialized execution of user endpoints and call() (§2.1)MUST
4User endpoint routing and authorization (§3)MUST
5Event stream semantics (§4)MUST
6UserHandle on state.user() (§5)MUST
7Grant policy enforcement (§6) when grants are supportedMUST
8Grant support (§6)SHOULD
9Lifecycle hooks (§7)SHOULD
10Administrative addressing of endpoints and stream beneath users/{id}/ (§3.2, §4)MUST

Appendix A. Notes (Non-normative)

  • Serialization rationale. Quotas, leases, counters, and registries require read-modify-write atomicity; §2.1 places it on user endpoints so plain value writes stay cheap.
  • Reference mapping. The Cloudflare runtime implements the user scope on its per-user Durable Object: serialized execution is native, stream sockets attach to the object, and call() is object-to-object RPC.
  • Out of scope. Authentication flows, account and billing records, and identity semantics are runtime concerns. Products project their results into values, env, or grants.
  • Motivating consumers. Cross-device registries and presence; entitlement gating with live occupancy events; per-user integrations consumed by tools via user.fetch(grant, …) without secret exposure.