Skip to main content
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.