> ## Documentation Index
> Fetch the complete documentation index at: https://docs.praxa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Security model

> Scope: the trust boundaries between client, gateway, and runtime; what a

> **Status:** Trust-boundary architecture and the unified policy engine —
> **Developer preview** (designed, not built). The fail-closed
> approval-enforcement pattern and the credential-hashing pattern it
> generalizes — **Available today**, inside the product the fabric is built
> on top of.

Scope: the trust boundaries between client, gateway, and runtime; what a
client can never do regardless of what it asks for; the fail-closed
enforcement philosophy with concrete examples; and how the platform handles
the credentials it issues and the credentials tenants bring.

Sources: `docs/superpowers/specs/2026-07-30-maas-execution-fabric-design.md`
(design spec) §§1–4, 6; `docs/decisions/2026-07-24-ai-platform-extraction.md`
("Security decision" and the target-boundaries sections).

## Trust boundaries

The design draws three boundaries, and a request cannot skip one to reach
the next.

**Client.** Everything outside Praxa's infrastructure: the Expo app, web,
Praxa Control (desktop), and third-party API callers. A client holds a
credential and can make requests. It cannot make itself more than what that
credential already resolves to — see "What a client can never do" below.

**Gateway.** A separately deployable, versioned edge — the fabric gateway in the
design, served at the existing `api.praxa.io` custom domain behind a
flag-gated dispatch (design spec §4, workstream F1). It owns protocol/version
negotiation, OAuth/API-key authentication, audience/tenant/organization/scope
validation, CORS for explicitly registered origins, request-size and rate
limits, idempotency claim/replay, stream framing, normalized errors, and
trace correlation plus response redaction. It does **not** own model
selection, prompt construction, tool execution, memory, billing decisions, or
provider credentials — those stay one layer in, by design.

**Runtime.** The one privileged execution authority — the existing chat runtime today, per
the parent ADR's strangler migration. It owns authenticated actor/tenant
authority, model and provider selection, tool admission/approval/execution,
memory retrieval and writes, billing authorization and metering, durable
workflow state, and provider-specific error mapping. Provider and privileged
tool credentials live here, and only here — never in a client, never at the
gateway.

Everything past the runtime — model providers, tool/connector adapters,
Supabase/R2/Vectorize/Workflows/Durable Objects, observability — is reached
only from the runtime.

## What a client can never do

A client can *request* a capability. It cannot *grant itself* one. Per the
ADR's client boundary, no request, from any client, can self-assign:

* **Identity.** The request schema has no `userId` field, deliberately.
  Identity comes from the credential the gateway validated — a JWT's claims,
  or the tenant an API key resolves to — never from a body field.
* **Tenancy.** `organizationId` is a *requested resource*, not an assertion.
  The server still has to authorize the caller against real membership
  before honoring it.
* **Tools.** Tool admission is runtime-owned — capability metadata, policy
  evaluation, tenant scope. A request cannot add itself to an allowlist by
  asking.
* **Billing tier.** Spend ceilings and credit balance are runtime-derived. A
  request body cannot raise its own limit.
* **Approvals.** A client cannot self-approve a gated action, and cannot
  construct approval evidence — only present evidence it was actually
  issued. Full treatment in `receipts-and-audit.md`.
* **Memory consent.** Consent state is runtime-derived, not client-asserted.

Praxa Control and other IDE-shaped clients get one more layer before local
tools are even admitted: workspace trust, explicit file/terminal capability
grants, path confinement, and per-action audit. Running locally is not itself
a trust grant.

## Fail-closed philosophy

The rule, stated directly in the parent ADR: **authorization remains
server-derived and fail closed.** What that means in practice:

**Two independent checks, not one.** The gateway validates a token's
audience and expiry — is this credential real and current. The runtime then
independently re-authorizes tenant, conversation, data classes, tools, and
approvals — is this specific actor allowed to do this specific thing, right
now, against live state. Clearing the gateway is necessary; it is not
sufficient. A request that clears authentication still gets re-checked
before it can act.

**Default-deny policy output.** The design's unified policy engine
(`evaluateActionPolicy`, design spec §3.3) evaluates before execution, at two
chokepoints — the tool-runtime wrap and the workflow action phase — and
returns exactly one of `auto | peer_review | human_approval | deny`, plus a
TTL and approver requirement. There is no unlisted fifth outcome that means
"allow anyway."

**The pattern is proven, not hypothetical.** The policy engine itself is a
developer-preview design commitment — it does not exist as one engine yet.
What it collapses into a single evaluator already runs in production today:
11 fail-closed approval domains, each gated by a no-execute
`request_approval` tool against a shared TTL policy
(`src/lib/trust/approval-window-policy.ts`). The engine consolidates a
pattern already carrying real traffic; it does not introduce an untested one.

**Approval evidence is single-use.** A receipt that authorizes one action
cannot be replayed to authorize a second one, even the same action twice.
Why, and what "opaque, scoped, expiring, single-use" mean concretely:
`receipts-and-audit.md`.

**No implicit trust for local tools.** As above: file/terminal access on
Praxa Control requires an explicit, per-capability grant — being local isn't
a substitute for being granted.

## Credential handling

One server-side principal resolver sits behind three credential families —
Supabase JWT (Expo, web, Praxa Control), `aura_agent_*` (existing
custom-agent deployments), and `platform_api_keys`/`praxa_sk_` (the fabric,
developer preview) — with identical downstream authority derivation
regardless of which one authenticated the request (design spec §3.2). Full
key lifecycle, formats, and scopes live in `keys-and-access/api-keys.md` and
`keys-and-access/scopes-and-tenancy.md`. The properties that matter for the
security model specifically:

**Platform keys are hashed, never stored recoverable.** `aura_agent_*` keys
are SHA-256-hashed at rest today, hex-encoded, verified with a constant-time
comparison — available today, in production. `platform_api_keys`
(`praxa_sk_`) commit to the same hash-at-rest discipline (design spec §3.2);
the design does not name a specific hash algorithm for the new key family,
and this document does not assert one on its behalf. Either way, a hashed
key can be *verified* — compare a hash of what was presented against the
stored hash — and never *recovered*. The platform cannot hand your key back
to you after issuance, structurally, regardless of who asks or why.

**BYO provider keys are encrypted, not hashed, because they must be usable.**
A platform API key only ever needs to be verified; a bring-your-own provider
credential has to be decrypted and used to call a provider on the tenant's
behalf, so it needs reversible encryption, not a one-way hash. The design
reuses the existing connections-key envelope pattern for this (design spec
§3.4). BYO keys never leave the runtime boundary — in the vocabulary above,
that means never returned to a client and never visible at the gateway. A
BYO-funded tenant's provider failover ladder is constrained to only the
lanes that tenant actually funded; it cannot silently borrow another
tenant's spend or credentials.

**Never in logs, never in errors.** Per the ADR's security decision: logs and
errors expose stable codes and correlation IDs — never provider payloads,
prompts, secrets, stack traces, or private reasoning. A BYO provider key is a
secret under this rule and is redacted on the same basis as any platform
credential, not as a special case bolted on afterward.

## Isolation and deployment posture

No self-hosting is offered. The rejected-alternatives analysis is direct
about why: a self-hosted build would mean forking the CF-native substrate —
Durable Objects, Workflows, Vectorize — outside Cloudflare, not deploying a
container, and that path is rejected (design spec §6). Enterprise isolation
instead means a **dedicated Cloudflare account/region, BYO provider keys, and
full evidence export** (design spec §4, phase P3) — stated as that, not
implied to be equivalent to a customer running their own infrastructure. Full
treatment: `enterprise/deployment-and-isolation.md`.

One honest gap to carry forward rather than paper over: today's multi-tenant
isolation guarantees (personal agents never falling back to organization
resources, and the reverse) are shipped and verified for the existing
custom-agent product. The equivalent audit *for fabric traffic specifically*
— per-tenant isolation across memory/Vectorize namespaces, Durable Object
naming, and RLS — is explicit GA-hardening scope (design spec §4, workstream
F7, phase P3), which ships after the tenant/key model, not alongside it.
Treat fabric-specific multi-tenant isolation as unverified until that audit
lands and this document is updated to reflect it.

## See also

* `data-handling.md` — what's stored, the PII pseudonymization pipeline, and
  per-tenant vs. cross-tenant learning.
* `receipts-and-audit.md` — receipt contents, why approval evidence is
  single-use, and export paths.
* `keys-and-access/api-keys.md`, `keys-and-access/scopes-and-tenancy.md` —
  key formats, lifecycle, and the tenant/scope model in full.
