# Security model

## Where this lives

- `services/registry/src/keys.ts` — the signing keys, and what may never be
  extracted from them.
- `services/registry/src/apps/client-assertion.ts` and
  `services/registry/src/apps/routes.ts` — how an App proves its publisher, and
  what the session it receives is allowed to claim.
- `packages/backend/src/app-session.ts` — the verification a backend performs on
  the other side of that session.
- `services/registry/src/http/route.ts` — the plane and the ceiling every route
  declares.
- `services/registry/src/apps/capability-authorization.ts` and
  `services/registry/src/apps/consent-page.ts` — capability authority, and the
  consent ceremony that only a Registry origin may render.

## Desktop entry

The desktop Shell is the only consumer OAuth client. It enrols a device key,
holds the person's session outside every product renderer, and signs native
Runtime requests on the person's behalf. An Environment registers no redirect
URI, handles no callback, and receives no access, ID, or refresh token. It sees
only the bounded `window.alamr` projection the main process supplies.

An Environment-level return destination carries no source-local navigation
state. Hub may keep an optional one-shot Zone/pose/camera checkpoint in its own
product partition, validates it against current host-owned navigation, and
consumes it after restoration. Coordinates never enter Registry requests,
tokens, or the destination Environment, and unavailable or malformed local
storage falls back to the authored spawn without weakening safe return rules.

Registry owns account authority while the Shell holds one device-bound session
and every Environment gets a separate fenced Runtime session represented only
through the bridge. **Every entry requires an account.** There is no guest
entry mode and no Environment that admits an unnamed visitor. Hub
(`environment-zero`) follows exactly the same flow.

There was an entry policy here — `entry.access`, one of `guest_allowed` or
`account_required`, together with an `auto | login | guest` mode — and it is
deleted rather than defaulted. What it decided was whether the Registry would
mint a `guest_…` subject, which meant two kinds of subject existed and every
reader downstream had to ask which it held. One kind is not a simplification of
two; it is a different system.

An unpublished local bundle receives an explicit authority-free preview
snapshot. It does not become a draft OAuth client and cannot manufacture a
Registry Runtime. Production entry remains publication- and catalog-gated.

Registry derives `environmentRoles: ["admin"]` only when the authenticated
Runtime user owns that Environment's Project. Guests and non-owners receive no
derived Environment role. The claim contains no owner user ID and is not
inferred from Publisher attribution or browser state. An atomic Project
ownership transfer changes this derivation for newly issued grants immediately;
existing short-lived grants remain fenced by expiry, lease state, revocation,
and live introspection rather than by a client-authored role.

Platform-managed role definitions extend this boundary (ADR-0055).
Registry administrators define roles scoped to every Environment or to an
explicit Environment set and assign them to accounts. At session issue and
re-derivation, Registry resolves the actor's in-scope assigned keys, merges
them after the derived `"admin"` key, and attaches an
`environmentRoleCatalog` describing every role that exists in that
Environment. Definitions and assignments live only in Registry state, change
only through the audited `/v1/admin/*` management API, and archived roles stop
resolving for newly derived sessions immediately.

An Environment backend never accepts the browser Runtime token or a claimed
role from request JSON. The SDK exchanges the active Runtime for a short-lived
`EnvironmentBackendGrant`; Registry restricts it to the immutable backend
audience, endpoint, scopes, context schema, entry origin and Runtime fence. The
backend verifies Registry JWKS and performs live introspection by default.
Environment-specific delegated roles and records are stored by that backend,
while Registry remains responsible only for platform identity, the initial
Environment admin boundary and revocation.

Signing out from the desktop ends that device session and revokes its linked
Environment Runtimes and App sessions. It does not revoke another device, a
separate Developer Console or CLI login, or a CI project token.

## App sessions

An App `WebContentsView` receives no Environment access token, refresh token,
raw account subject, device identity, or Plugin Grant. The App creates a PKCE
verifier and sends only its challenge and a random bridge nonce through the
injected platform bridge. The desktop host validates the exact App surface,
product, revision, document generation, and current host before requesting a
one-time launch code.

Registry exchange requires the exact verifier, nonce, unconsumed code,
revision, host generation, and permission-set digest. An environment-hosted
session additionally requires the complete active parent Runtime lease; a
platform-hosted session has no parent and receives no Environment-scoped
capability. The result is memory-only, short-lived, and App-pairwise. App
backends verify it by Registry introspection. The product-specific scheme and
partition, Electron sandbox, context isolation, disabled Node integration,
injected CSP, and narrow bridge are layered controls; they are not described as
a complete JavaScript sandbox.

Every App session carries `appAuthMethod`. A frontend-only App exchanges the
launch code itself and receives `none`; possession of the code and verifier
does not authenticate a publisher. An App that declares a backend is a
confidential client; its own backend signs the exchange with
`private_key_jwt` against a public key and Environment admission policy frozen
into the published manifest. Introspection refuses a method the backend did
not declare via `acceptedAuthMethods`.

`private_key_jwt` proves that the publisher participated, not that the person
made a fresh gesture at that backend. Notes therefore exposes its
actor-portable personal corpus only to an explicit reviewed first-party
Environment list, still refuses sessions with no introspected Environment, and
accepts no actor or Environment selector from the client. Optional
Environment/location attachments are immutable provenance, not authority.

Its `createAppBackend` configuration adds `/v2/*` to the validated protected
route prefixes (`/v1/*` is always protected), so shared bearer admission runs
before either API generation's handlers.

Opening that corpus to arbitrary or third-party Environments waits for a
Registry-origin user-presence ceremony; `backend.environments: "any"` is not an
acceptable substitute.

No App receives Environment context by default. Requests use the closed
versioned capability catalog and are split into required and optional sets.
Registry binds the assessment and user consent to the exact manifest digest,
App revision, principal policy, Environment scope where applicable, and
authority epoch. Launch rechecks all of them; manifest declaration alone is not
a grant.

The persistent Host Services `MessagePort` is generation-bound. Frames are
schema-validated and byte-bounded, request IDs cannot collide, concurrency and
deadlines are bounded, and pending calls are cancelled when the generation
ends. Unknown methods, missing capabilities, missing user activation, stale
generation, unavailable provider, and malformed replies fail closed.

Spatial context contains stable Environment semantics, not raw account identity
or arbitrary scene access. Capture and navigation assessment require a
transient user action. Navigation uses an expiring caller-bound assess/commit
pair; the Environment owns access policy and movement. Only a destination
revision's declared semantic location may cross an Environment launch.
Captured locators are opaque, size-bounded, revision-bound, and never placed in
a URL.

Changing Compact/Workspace mode changes host presentation in place and does not
mint authority. Explicit close and other destructive exits use a bounded
lifecycle response and cannot trap the user. On desktop, an
Environment-to-Environment switch preserves the App document but ends the old
Runtime authority before renewal against the successor; a stale response or
old lease cannot authorize the preserved document.

## Catalog presentation media

Bundle-relative image paths in App and Environment manifests are publication
inputs, not delivery URLs. Registry reads those files from the already verified
archive; admits only bounded PNG and JPEG rasters; validates format headers and
dimensions and allowlist-rewrites the container metadata without claiming a
pixel re-encode; and binds the resulting private object version to the exact
immutable revision and presentation slot.
The public content-addressed route serves bytes only while a published binding
authorizes them.

Panel and catalog contracts expose Registry-managed `cardIcon` and
`cardCover` descriptors. Platform chrome must not fall back to the publisher
publisher URL, because that would disclose the visitor's address and panel-open
timing and would render mutable unreviewed bytes. Running-App marks remain a closed
platform-drawn vocabulary and never use the richer App card icon.

## Notification authority

Notification sources do not address raw users. An open App session implicitly
selects its current principal, host scope, and exact source revision. A V1
Occurrence supplies a stable source-scoped `eventId`, reviewed effect, and —
for a keyed Subject — an opaque key plus source-domain monotonic revision.
Registry validates those fields against the event policy frozen into the
immutable App revision. Exact retry and stale revision are accepted no-ops, so
retry cannot reopen Attention or replay a Pulse.

Source-authored content remains bounded plain text plus the closed semantic
action contract. HTML, script, arbitrary URLs, raw account subjects, mutable
source artwork, and callbacks are rejected. A named platform template takes
only its reviewed bounded parameters. For `message_waiting`, Registry resolves
an optional App-pairwise actor through the recipient's own roster and composes
the title itself; the App does not supply title, preview, category, or sound.

An App background workload presents two independent credentials: an expiring
Project token scoped to `notifications:publish` and an opaque per-user
delegation created from an account App session. Registry stores only the
delegation hash and rechecks App project ownership, active published
revision/digest, publisher authority epoch, both notification capabilities,
consent, expiry, and revocation for every signal. A Project token alone cannot
select or correlate a user, and a delegation is useless to another App project.
After the workload and caller-owned request are admitted, every
recipient-dependent outcome answers only `202 {accepted: true}` with no count,
treatment, consent, recipient existence, or read state.

Source acknowledgement is likewise accepted-only. The App names one of its own
reviewed keyed Subjects and a `throughRevision`; Registry derives source and
recipient from the session and stores the maximum watermark even before a
delayed Occurrence exists. An App open, notification click, or AppHost launch
outcome is not acknowledgement — the product advances the watermark only after
it has presented authoritative content through that revision.

An Environment backend uses a short-lived Environment Backend Grant with
`notifications.publish@1.0.0`, exact endpoint binding, and live Runtime
introspection. It does not receive or reuse an App delegation.

There are two routes on which a caller reaches somebody other than the person
holding its credential, and both answer identically whatever they find.

The second is an Environment telling people who allowed it to. Consent is given
on the Registry's own origin and the row that records it carries the person's
Environment-pairwise name — so that name resolves to an account only while the
consent is live, and revoking removes the address rather than adding a check.
The Environment learns that somebody said yes, because their browser returns
through it, and is never told when they take it back. What it sends is capped at
the publisher ceiling, so it reaches an inbox and interrupts nobody who did not
raise it themselves.

One notice may be raised for somebody who is not the caller. The recipient is
named only by an `aact_` issued under the calling App's own key, and Registry
gates it on a companionship edge that exists now, the recipient's own grant of
that capability to that App, no refusal in either direction, and neither account
suspended. Every outcome answers identically, so the route is not an oracle for
whether a person blocked the caller, is still their companion, or uses the App
at all. The text is platform-authored, the sender's name included: it is read
from the recipient's own roster rather than from anything the App sent, and
since ADR-0125 the roster reaches no Environment at all — the client's own
chrome reads it on the device plane — so the name discloses the event and not
the graph. A
count stays refused, because that is the sender's message volume and no read
hands it over today. The composed title is cut to the column's bound, since this
write happens only once every preflight check has passed and an overflow would
answer differently in exactly the case where the notice landed.

How loud a Subject may be is resolved by Registry during reduction and is not a
signal field. The default ceiling follows authorship — reviewed platform text
may announce; a publisher's own text defaults to `file`, including first-party
Apps — because a ceiling keyed to caller-supplied `category` or `family` would
be self-awarded. Recipient preferences may lower it or explicitly raise it,
are written only from the Registry origin, and the resolved treatment is never
echoed back to the publisher. A source mute remains a floor and the
account-wide "not now" remains Registry-origin only.

Account Subjects are persisted under the Registry principal. Desktop Main reads
the account-complete Center with a signed device assertion. A Runtime read is
narrowed to its current Environment; the Environment hosting the Widget never
receives account-complete titles, bodies, sources, Subject keys, or CallAlerts.
It is told only `none | waiting | urgent`, never a count.

Presence/control transport delivers content-free invalidation hints. Clients
reconcile authorized rows and the independent whole-feed counts:
`unseenCount` includes muted unseen Subjects, while `attentionCount` excludes
them and legacy `unreadCount` aliases the latter. Reconnect and list snapshots
cannot reconstruct a transient Pulse. Logging excludes bearer tokens,
delegation secrets, raw subjects, bodies, and plaintext Subject keys.

Deadline-bound CallAlert state and per-device Delivery are separate authority
planes. An OS or Shell presentation cannot mark News seen or decide call truth.
External push tokens and providers do not exist in V1.

## Runtime ownership

One principal may own exactly one active Al-Amr Environment Runtime at a time,
across tabs, origins, and—through the production Redis adapter—Registry
replicas and devices. A newly activated visible page atomically receives a
fenced lease; the previous Runtime becomes `superseded`, its Plugin Grants are
revoked, its presence is removed, and compliant Plugin resources are torn
down. Returning focus does not reclaim ownership. The user must explicitly
choose **Activate this page**.

An acquired activation fence remains pending until the Registry has validated
the authorization again, projected the exact fence durably, recorded the
audit event, and prepared the response. The coordination store then retains
that exact fence atomically. Failure compensation can release only an
unretained fence, so it cannot invalidate a same-fence activation which another
request already committed.
When compensation wins, the Registry clears the durable active projection only
if that row still carries the released lease ID/version. Runtime self-healing
uses the same compare-and-set rule, so a stale request cannot downgrade a newer
projection of the same session.

Every protected Runtime request carries the Runtime session ID and exact lease
ID/version. Stale fences return `409`; unavailable production coordination
returns `503` and fails closed. The SDK unmounts trusted-library Plugin UI while
inactive. This protects platform APIs and compliant Plugins, but it is not a
JavaScript sandbox for arbitrary code installed by an Environment.

Inactive pages keep a read-only, session/origin-bound control channel so a
browser-wide SSO logout is delivered immediately even after supersession. Its
short-lived control credential contains no Runtime lease and the server rejects
all client messages on that channel. It cannot send presence, request a Plugin
Grant, change settings, or render Plugin UI.

## Rendering telemetry

First-party rendering diagnostics are privacy-safe by construction (ADR-0048).
Events are limited to a closed allowlist: event type, renderer backend,
fallback usage, quality tier, init-duration bucket, stable unsupported-reason
code, and recovery outcome. GPU vendor and model, unmasked renderer strings,
VRAM, raw adapter information, raw driver errors, and shader sources are never
collected or displayed; the unsupported page shows only sanitized reason
codes. The runtime contract is documented in
[Rendering runtime](rendering-runtime.md).

## Plugin trust

The initial frontend runtime is a trusted library installed at build time. Its
declared permissions are split into:

- enforceable Al-Amr platform permissions;
- browser requirements and network/data disclosures;
- backend grant scopes.

Managed package artifacts are immutable and content-addressed. Validation
rejects path traversal, unsafe links, tar bombs, native binaries, lifecycle
scripts, secrets, and integrity mismatches.

The Registry's published-artifact cache is a verified serving copy, never an
authorization source. Cache admission and reads verify byte-derived SHA-512
against the exact Object Storage VersionId. Registry rechecks publication and
emergency-block state both before and after opening the cached bytes, so a
block committed during the storage read wins before a response begins.
Incomplete or corrupt coverage fails readiness closed; write paths and
uncached drafts never use the cache.

## Backend grants

The Registry derives Environment, user, origin, and session from a bearer
runtime token. A Plugin Grant is issued only for an exact installed release and
published backend deployment. Services verify type, audience, scope, signed
context, origin, expiry, and replay policy against the Al-Amr JWKS. The Grant
also carries a Registry-derived `anonymous` bit and the immutable installation's
public `environmentConfig`, allowing a backend to enforce policies such as
Media guest access without trusting browser input.

Grants also carry the Runtime lease fence. Long-lived backend connections must
introspect that fence and fail closed on any definitive verdict: revoked,
superseded, expired, or lease mismatch. When Registry coordination is merely
unavailable, a connection may retain its already-verified authorization for a
bounded confirmation window — at most three consecutive failed introspection
attempts, and never past the Grant's own `exp` — after which it also fails
closed, but with a distinct retryable close reason so a client can tell a
coordination blip from a revocation. Without that bound, a seconds-long Registry
deploy turns into a fleet-wide disconnect and an immediate re-mint stampede
against the recovering issuer. Media and Multiplayer perform this check at join
and periodically for the life of the connection. JWKS and introspection fetches
are pinned to the Registry issuer's exact origin and canonical paths, reject
redirects, and bound response size; discovery cannot select an arbitrary
token-bearing request target.

Media v2 consumes a fresh one-time Grant for join, resume, and same-socket
renewal, and exact-CAS fences the connection ID, generation, Runtime lease
version, and expiry in Redis. Renewal preserves the existing membership and
cannot replay a join. Stage start and speaker promotion recheck the current
`media:publish` scope plus the Environment role-authority callback; claimed
microphone and screen TrackSIDs are also verified against LiveKit before state
becomes publish-active.

Direct-call authority distinguishes initiation from answering. A guest never
receives `media:direct-call` and cannot initiate, while a `callee_only` policy
may expose that guest's scoped `actorKey` to an authenticated same-scope caller.
The Media service, not the browser, projects current call availability and
reserves both peers until the invitation reaches a terminal state.

LiveKit credentials are short-lived least-privilege connection credentials,
not an immediate revocation mechanism for an already established WebRTC
transport. Revocation therefore converges through Media state, LiveKit admin
permission/removal calls, signed webhooks, and an independent lease-based
desired-state Projection queue. The queue contains no credentials, coalesces
to the latest session version, and can be completed only by the exact worker
lease that claimed that version.
Self-hosted deployment does not claim instantaneous token invalidation. Media
never records or transcribes content, and Direct Call/Meeting E2EE keys exist
only in active session state and authorized browser delivery. Terminal
transitions erase E2EE material before bounded retention.

LiveKit webhooks enter only through `/internal/livekit/webhook`. Media verifies
the signed raw payload and its SHA-256 claim before processing, requires
`application/webhook+json`, rejects payloads above 256 KiB, and deduplicates
the bounded event UUID through a claim/complete/release processing ledger in
Redis. A failed handler releases its claim so a signed retry can be processed;
a completed event remains deduplicated. An authenticated webhook is observation, not
authorization: all corrections still derive from authoritative Media state and
exact participant, TrackSID, source, holder, and generation checks.

High-frequency Runtime endpoints keep a coarse network abuse ceiling before
authentication, then apply their operational limit to the verified Runtime
session. Grant issuance and presence heartbeat therefore keep legitimate users
behind one proxy independent, while one session cannot evade its own budget by
rotating request headers.

Multiplayer follows the same Grant boundary. Its backend derives actor,
Environment, origin, release, component, and scope from verified claims, never
from an unsigned join payload. JSON is control-only; binary state is length-
checked against the immutable layout. It validates finite transforms, modular
sequence, message size, cadence, room epoch, capacity, motion envelope, and
discontinuity frequency. A social transform outside that envelope is dropped
while the last accepted pose, socket, actor fence, and room membership remain
intact; an invalid transform is never treated as a leave signal. Bounded burst
tolerances cover the reviewed first-party controller at its 20 Hz publication
cadence, including a maximum-turn input, without accepting an instantaneous
quaternion flip. Production Redis atomically fences Grant replay,
concurrent actor sessions, and single-writer room ownership. Room and actor
keys are SHA-256 values; raw subjects and scope labels are excluded from keys,
logs, and metric labels. Abrupt resume requires both a fresh Grant and a
rotating high-entropy ticket bound to the same actor and room. Movement poses
are presentation data, not proof of collision-valid or cheat-free gameplay.
If the replacement WebSocket overlaps the old transport, that same proof
atomically rebinds the retained membership, rotates the ticket, and closes the
older transport. Suspension is connection-bound, so a delayed close from the
older socket cannot suspend or evict its replacement.
Normal authorization renewal uses a new single-use Grant on the existing
socket. The signed Environment, Plugin release/component, actor and actorKey,
origin, scope, Runtime session/lease, Environment configuration and reviewed
permissions must match. Redis exact-CASes the existing connection ID and lease
version before generation-scoped local timers are swapped; a stale timer cannot
close the renewed authorization. A Runtime lease change is a reconnect, never
a renewal shortcut.
Room-owner forwarding is private HTTP/2 with mutual TLS. The fenced Redis
record supplies only an exact owner origin; every peer presents a certificate
from the configured cluster CA, redirects are impossible, frames are bounded,
and a route failure cannot fall back to a competing local writer. One pooled
session per owner avoids turning browser count into internal connection count.

Multiplayer scope is an isolation partition, not access control. An Environment
that has restricted Zones opts into `environment_backend` scope admission in
its immutable installation. Before issuing the Plugin Grant, Registry sends a
30-second EnvironmentBackendGrant to the revision-declared endpoint with only
the Runtime identity, Environment roles, and requested scope. Redirects are
rejected, the call times out after 500ms, and responses above 16KiB are denied.
Timeout, malformed response, and explicit denial all fail closed as the generic
`scope_denied` error without disclosing occupancy.

Peer display profiles are server-stamped from verified Grant claims and are
sent as low-rate membership metadata, never accepted from pose messages.
Cross-Plugin avatar correlation uses only an `actorKey` scoped to the signed
Environment and validated context scope. Registry derives the final key from
its HMAC-protected Environment-pairwise principal and signs the same claim into
each cooperating Plugin Grant. Backend components must not derive correlation
from their different Plugin-pairwise `actorSub` values. Browser code never
receives the raw account subject, and keys from different Environments or Zones
cannot be treated as the same identity.

## Control plane

The Developer Portal authenticates through the same OIDC provider and exchanges
its validated management token for a correlated, opaque, database-backed
Console session. Console sessions use strict secure HttpOnly cookies, CSRF
protection, audit logs, and rate limits. CLI/CI use separate scoped, expiring
tokens.

Hub placement mutation is platform administration, not Environment Project
authority. Registry accepts set/replace/clear only from an authenticated
administrator, rejects Project tokens, validates the authored slot and public
catalog eligibility, forbids Hub as its own destination, and enforces one
assignment per destination within the Hub. Every mutation requires a bounded
reason. The assignment write and `hub.placement.set` or
`hub.placement.clear` audit event commit in one transaction; audit failure
rolls back the placement. Fixed slots are immutable through this API.

Hub consumes only the public placement projection. That projection discloses a
destination ID only for an eligible `occupied` slot. Missing Registry/catalog
truth and ineligible assignments become non-enterable `reserved` landmarks;
the client cannot recover a stale checked-in destination.
Browser-based `alamr login` is a public native-client authorization. Even when
the browser already has an SSO session and an existing Grant, the Registry
renders a no-store, frame-denied consent page that names `Al-Amr CLI` and lists
the exact requested scopes. The flow advances only after an explicit
same-origin, CSRF-bound POST; denial returns OAuth `access_denied`. This approval
is required for every browser authorization because a public `client_id` and
loopback redirect cannot authenticate the local process that initiated it.
PKCE binds the resulting code to that process. The CLI closes its ephemeral
`127.0.0.1` callback listener immediately after receiving the authorization
response, before token exchange.

Dual-stack loopback probing is a follow-up: adding `[::1]` support must register
a second exact loopback base URI while preserving the CLI-only, port-only
redirect exception. The current browser callback intentionally binds IPv4
`127.0.0.1`.

Health checks reject private/link-local targets, revalidate redirects and DNS,
and cap time and response size.
Outside production, HTTP loopback reachability is permitted for the local
dynamic handshake. Production still requires HTTPS and rejects loopback;
broader private-network exceptions remain explicit development configuration.

## Staging deployment authority

GitLab deployment authority is a dedicated `al-amr-ci` SSH key plus pinned host keys. It
does not receive the owner password, bootstrap admin token, database URL or S3
credentials. Those secrets remain root-owned on the exact host that needs
them. The unprivileged build account receives only public build configuration;
Object Storage upload is performed by a fixed root helper with a host-local
writer credential, and other roles use host-local read-only credentials.
One-time onboarding rotates project-scoped, expiring automation tokens into a
root-only Runtime file; raw token values never enter CI, catalog receipts or
logs.

Root execution is limited to reviewed `al-amr-deploy` subcommands. Archive
extraction rejects absolute/traversal paths, device/FIFO/hardlink entries and
escaping symlinks. Ordinary deploy cannot change Caddy, systemd, hosts,
firewall, users or secrets; an `opsRevision` mismatch fails closed. Journals
and `RECOVERY_REQUIRED` prevent a later pipeline from overwriting evidence of
an incomplete mutation.

Host configuration and secret installation are deliberately outside GitLab:
the offline `al-amr-ops` identity applies a matching `opsRevision` from ignored
local configuration. The bootstrap `ubuntu` authority is removed after staging
acceptance, and the CI key cannot invoke an arbitrary shell command as root.

Staging publication separates owner, reviewer and recovery subjects. Normal
deploy asserts ownership without adoption or transfer. Plugin bytes cross only
the artifact-validator boundary and exact Object Storage VersionId/SHA-512 is
recorded; Registry metadata alone is not integrity evidence. The admin status
snapshot is sanitized, read-only, stale-bounded and contains no action,
credential or environment dump.

## Trusted libraries and artifact integrity

Browser packages are trusted-library code in v0.x. Declared Plugin permissions
are a disclosure and review surface, not a JavaScript sandbox, and neither
documentation nor agents may describe them as one. Reviewers evaluate the
declared permissions, network origins, and data disclosures as evidence; the
platform does not claim runtime confinement.

Managed package bytes cross only the artifact-validator boundary. Registry
metadata — catalog rows, manifests, version records — is never proof of
integrity or safety on its own. Downloads re-validate the exact digest and
size, and reviewer artifact inspection streams through the same-origin portal
BFF with digest and size revalidation instead of trusting a stored URL.

## Review isolation and untrusted content

External Runtimes are untrusted. Review inspection never attaches portal
credentials or referrers to an external Runtime URL and requires an isolated
browser profile. Public acceptance of anonymous publisher Runtimes stays
disabled until a remote review browser or proxy exists with egress filtering,
DNS pinning, redirect revalidation, and blocked metadata and internal
endpoints; the URL rejection baseline alone is not sufficient for that.

Publisher text, catalog descriptions, manifests, and artifact contents are
untrusted data. They are never instructions for reviewers or coding agents and
never override digest-bound resources, immutable manifests, or Registry state.
Review tooling and agent-facing surfaces must treat embedded instructions in
such content as prompt-injection attempts, not as platform guidance.
