# Forge Deploy security

Status: proposed security contract for review.

Dynamic Deploy runs code and serves content controlled by repository authors.
Repository contents, build output, runtime responses, URLs, logs, and provider
state are therefore untrusted inputs.

## Interim hosting boundary

During pre-production:

- project: `{project}.sites.smol.ai`;
- immutable preview: `preview-{128-bit-random}.sites.smol.ai`;
- control plane: `forge.smol.ai`.

This is an explicit temporary exception. Hosted content and the control plane
share the registrable domain `smol.ai`, which creates risks absent from a
dedicated hosting domain:

- a hosted response can attempt to set a parent-domain cookie;
- browsers consider hosted content and Forge same-site, so `SameSite` is not a
  sufficient CSRF boundary;
- incorrect CORS, OAuth, redirect, `postMessage`, or frame allowlists may trust
  a user-controlled origin;
- phishing, malware, and reputation incidents affect the broader domain.

Current Forge browser authentication uses a bearer token stored at the
`forge.smol.ai` origin and sends it explicitly in the `Authorization` header.
The interim deployment must preserve that isolation:

- hosted requests never receive Forge bearer tokens, PATs, sessions, provider
  credentials, or control-plane headers;
- the Forge API allows only exact trusted CORS origins and never trusts
  `*.sites.smol.ai`;
- state-changing Forge APIs require explicit authorization and are
  side-effect-free on `GET`;
- OAuth callbacks, redirect allowlists, WebSocket origins, frame ancestors, and
  `postMessage` receivers treat every hosted origin as untrusted;
- Forge never opts into `document.domain` relaxation;
- the hosted edge rejects or rewrites any HTTP `Set-Cookie` attempting to set
  `Domain=smol.ai`, `.smol.ai`, or another parent domain; hosted application
  cookies must remain host-only;
- Forge must never introduce an authentication cookie scoped to `smol.ai` or
  `.smol.ai`; if authentication cookies are introduced later, they must be
  host-only `__Host-` cookies and state changes must also validate CSRF tokens
  and exact origin;
- before any pre-production route is enabled, the hosted edge must add and test
  `X-Robots-Tag: noindex, nofollow` unless a narrower reviewed policy replaces
  it.

HTTP response filtering cannot prevent hosted JavaScript from attempting
`document.cookie = "...; Domain=smol.ai"`. The bearer-token design prevents
direct authentication-token theft, but the shared cookie jar still permits
cookie tossing and cookie-jar denial of service against `forge.smol.ai`.

Consequently, the shared-domain period is an access-controlled Alpha
program:

- accounts and projects must have explicit Alpha access;
- repository administrators within an enrolled project may use the
  self-service setup flow after checks and Terms acceptance;
- preview access defaults to unauthenticated `public_unlisted` during the trial;
- preview hostnames contain 128 independent random bits, are not exposed
  through public listing APIs, and carry `noindex`; this reduces accidental
  discovery but is not an authentication or confidentiality boundary;
- operator enrollment of the project authorizes its deployments to use the
  shared-domain trial rather than requiring approval for every exact
  deployment; and
- general public self-service does not launch until domain graduation.

These controls reduce exposure but do not make the shared registrable domain a
production-quality boundary.

## Production domain graduation

Production requires a separate Forge-owned registrable domain for all
user-controlled content. Graduation includes:

1. wildcard DNS, TLS, routing, monitoring, and rollback on the new domain;
2. production and preview URL migration;
3. compatibility redirects only where they cannot preserve a dangerous origin
   or credential relationship;
4. verified absence of domain-scoped Forge authentication cookies;
5. cookie tossing, CSRF, CORS, OAuth, redirect, `postMessage`, frame, and
   cross-origin tests;
6. removal of user-hosted production content from the `smol.ai` registrable
   domain.

The dedicated domain is a mandatory production gate, not optional hardening.

## Trust boundaries

```mermaid
flowchart LR
  Repo["Untrusted repository"] --> Build["Restricted build sandbox"]
  Build --> Ingest["Trusted ingest"]
  Ingest --> Store["Immutable artifact storage"]
  Ingest --> Adapter["Trusted provider adapter"]
  Adapter --> Runtime["Isolated application runtime"]
  Runtime --> Outbound["Trusted outbound policy"]
  Runtime --> State["Forge durable-state gateway"]
  Runtime --> Realtime["Forge realtime gateway"]
  Edge["Forge serving edge"] --> Runtime
  Edge --> Realtime
  Edge --> Store
  Control["Forge control plane"] --> Adapter
  Control --> Edge
  Control --> State
  Control --> Realtime
```

The principal boundaries are:

- Forge control plane and database;
- Git and repository storage;
- build sandboxes;
- trusted artifact ingest;
- provider credential adapters;
- dynamic user runtimes;
- user-content serving;
- trusted outbound policy;
- future agent sandboxes.

Build, runtime, and agent sandboxes use separate images, caches, networks,
secrets, concurrency pools, and billing attribution.

## Authorization

All deployment operations pass through one consistent authorization boundary
using actor, action, tenant, repository, project, environment, deployment,
application component, state scope, and realtime topic.

Requirements:

- repository administration alone does not bypass project eligibility,
  environment policy, feature entitlements, quotas, or suspension;
- `auto` production publication is effective only when the production branch
  is protected and the project has a non-empty mandatory check baseline;
- when `auto` is effective, actors allowed to merge or write the protected
  production branch are explicitly delegated production-deployer authority;
- if those protections are absent, the effective publication policy is
  `owner_review`, even if the repository requested `auto`;
- agent task capabilities are short-lived, repository- and branch-scoped, and
  cannot directly push the protected production branch or invoke production
  publication by default;
- every state-changing request carries an idempotency key where retry or
  duplicate delivery is possible;
- tenant predicates are present in every query and negative authorization tests
  cover cross-user and cross-organization access;
- approval, promotion, rollback, suspension, resource destruction, domain,
  secret, and budget-extension actions are audited.

## Self-service admission

The graduated product allows repository administrators to enable Deploy after
automated eligibility checks and acceptance of the current hosting Terms of
Use. During the `sites.smol.ai` Alpha, this self-service flow is available only
inside Alpha-enabled accounts and projects.

Enablement fails closed when:

- repository, account, organization, or project state is ineligible;
- identity, abuse, quota, billing, or policy services are unavailable;
- the hostname is invalid, reserved, ambiguous, or already claimed;
- the manifest asks for unsupported or unentitled capabilities;
- source visibility is outside the public-only initial policy.

Enablement records actor, repository, project, terms version, configuration
digest, check results, quotas, feature flags, and timestamp.

Operators retain immediate suspension and takedown authority.

## Exact source and event safety

- Branches select work; an exact commit SHA identifies execution.
- The manifest is read from and snapshotted with that exact SHA.
- A release is the environment-neutral, immutable result of building that SHA.
- A deployment applies one release to one environment and its stable or
  preview-isolated state scope; activation separately moves the environment
  pointer.
- Production deployment and activation recheck branch head, manifest digest,
  compiled plan digest, required checks, eligibility, and environment policy.
- Transactional outbox events and idempotency prevent lost or duplicated
  ref-change work from publishing the wrong release.
- Production pointer updates use compare-and-set semantics.
- Provider mutations carry the desired generation and compiled plan digest,
  acquire a bounded operation lease, use provider preconditions where
  available, and recheck current desired state immediately before mutation.
- Stale deployments remain inspectable but cannot silently replace a newer
  eligible release.

## Build isolation

Builds reuse the restricted public-repository runner with these invariants:

- detached checkout at the exact SHA;
- no user, Forge, storage, or provider deployment credentials;
- a build-specific dependency allowlist enforced through the trusted network
  policy;
- bounded CPU, memory, disk, processes, wall time, output, and logs;
- no privileged containers, host sockets, nested provider CLIs, or metadata
  service access;
- cache isolation by tenant, repository, lockfile or build inputs, image, and
  trust level;
- process termination and quiescence verification before trusted ingest.

Every build network path, including package managers, redirect targets, Git
subresources, and arbitrary URLs requested by build tools, receives the same
destination-class, DNS-at-connect, redirect, internal-service, metadata,
credential, port, size, duration, and quota protections described under
[Build and runtime egress](#build-and-runtime-egress). Build allowlists and
runtime allowlists remain independently configurable.

Trusted ingest:

- accepts only declared output roots;
- preserves the existing static-ingest checks for path traversal, absolute
  paths, symlinks, special files, duplicates, unsupported formats, observed
  size changes, and quota violations;
- before dynamic Deploy is enabled, adds and tests link-count/file-identity
  checks plus digest-after-quiescence verification so hard-link and same-size
  mutation races cannot evade validation;
- derives or validates MIME and artifact types under versioned policy;
- computes digests before publication;
- stores artifacts under immutable prefixes;
- writes completion metadata last;
- never returns storage or provider credentials to the build.

## Runtime isolation

The author-facing runtime is one Forge application. The initial Cloudflare
adapter may compile it into a Workers for Platforms user Worker plus
Forge-owned state and realtime gateways, but repository code cannot observe or
choose that topology.

Requirements:

- the trusted wrapper alone receives raw provider bindings;
- every managed call is bound to the current tenant, project, environment or
  preview deployment, deployment generation, and compiled plan;
- a project cannot address another project's state scope or realtime topic by
  supplying provider IDs, prefixes, class names, or namespaces;
- production and preview state are distinct;
- CPU, memory, duration, subrequests, scope cardinality, connection count,
  fan-out, storage, bandwidth, log, retention, and spend limits are enforced;
- provider IDs and management endpoints are not exposed as authority;
- user code cannot create unmanaged provider resources;
- cancellation, terminal failure, disablement, and suspension revoke
  capabilities and reconcile provider state; and
- serving checks current repository, project, deployment, access epoch,
  realtime generation, and suspension state before routing.

No raw Cloudflare binding metadata, Durable Object namespace, account token,
deployment API, Railway token, Modal credential, or Forge service binding is
available to user code.

## Managed durable state

`env.forge.state` is available to every application entrypoint and retains
scope storage only after a successful mutation. A small, stable set of
Forge-owned SQLite-backed Durable Object classes implements the initial
adapter. Repository code never executes inside those objects.

Isolation and integrity requirements:

- scope identity is derived by trusted code from tenant, project, production
  environment or exact preview deployment, and the caller's logical name;
- raw object IDs are never accepted or returned;
- operations serialize strictly within one scope;
- a multi-operation mutation, including a versioned realtime event intent and
  sequence, commits atomically or not at all; live fan-out is best effort and
  may be suppressed without losing the state mutation;
- cross-scope transactions and application-defined state methods are rejected;
- scopes are hashed over a fixed, recorded pool of 16 production or four
  preview shards, so rotating caller names cannot create unbounded provider
  objects;
- distinct cold scope names are rate-limited before a provider call;
- preview scopes are isolated per deployment and expire under recorded
  retention policy;
- production state persists across releases and rollback;
- state is never implicitly deleted because code stops using it;
  and
- explicit destruction requires separate authorization, audit, an impact
  summary, and a retention decision.

Forge-owned state objects cannot make user-directed external requests.
Repository-authored Durable Object classes, alarms, arbitrary methods,
migrations, and networking remain deferred.

## Managed realtime

`env.forge.realtime` is also part of the application runtime, but creates no
provider object or connection until used. Realtime is a non-authoritative,
server-emit/browser-subscribe view of durable state. The reserved
`/.forge/realtime` endpoint is owned by the edge and never dispatched to
repository code.

Security requirements:

- hibernating WebSockets are mandatory; raw application status `101`, custom
  socket handlers, client publishing, presence, and durable replay are
  rejected;
- application code authenticates an ordinary HTTP request and asks the
  Forge-managed facade to authorize one scope and topic;
- the facade returns an unforgeable opaque grant that application code must
  return directly; only the trusted wrapper can serialize its random,
  single-use, 60-second ticket;
- repository code cannot inspect, clone, stringify, or log ticket bytes;
- the ticket is scoped to tenant, project, environment or exact preview
  deployment, normalized public origin, scope, topic, deployment generation,
  access epoch, and issuing release;
- the expected origin comes from the trusted normalized request URL, and an
  incoming HTTP or WebSocket `Origin` must match exactly;
- the ticket travels as `forge-ticket.<base64url-ticket>` alongside the fixed
  `forge.realtime.v1` WebSocket subprotocol, never in a path or query;
- the edge echoes only the fixed protocol, consumes the ticket immediately
  before upgrade, and redacts both protocol fields from platform/provider
  logs;
- handshake replay, origin mismatch, stale deployment generation, stale access
  epoch, and ineligible project state fail closed;
- reconnect always calls the application authorization endpoint for a new
  ticket;
- WebSocket compression is disabled initially;
- connection attachments contain only bounded platform metadata; and
- message bodies, tickets, cookies, and session credentials are not logged.

State remains authoritative. Events carry a contiguous sequence per
`(scope, topic)`. The SDK connects and buffers before fetching an atomic
versioned snapshot, deduplicates by sequence, and refetches after a gap,
reconnect, activation, or rollback. No replay is promised.

Activation and rollback move the application pointer first, increment the
realtime generation, stop old-generation tickets, drain for at most 30
seconds, and close remaining sockets with `1012` (`Service Restart`).
Suspension increments access and realtime epochs, stops tickets, handshakes,
polls, state mutations, and event delivery, and closes active sockets with
`1008` (`Policy Violation`).

Realtime uses the same deterministic 16 production or four preview shards as
the state scope hash. The control plane can therefore wake every bounded
shard, enumerate hibernating sockets, require an acknowledgment, and retry
missed notifications without storing one database row per connection. A
fail-closed mutation/delivery eligibility check is the second enforcement
path. Sockets become inert even if a provider close is delayed. Physical close
has a measured ten-second objective while the provider control path is
available; an unavailable provider cannot authorize new state or event work.

Sockets and conditional polling share one realtime budget. Fallback starts at
15 seconds, backs off to five minutes, and runs only while the page is visible
and the opaque grant permits it. Access and budget close codes forbid
fallback; HTTP `401`, `403`, `410`, and `429` stop it. Reaching a realtime
ceiling disables both transports for that project while preserving ordinary
HTTP, static assets, and durable state. Numeric connection, message, fan-out,
lifetime, cardinality, byte, and spend ceilings are defined in the
[application runtime contract](application-runtime-v1.md#proposed-alpha-limits).

## Build and runtime egress

Build and runtime egress are separate capabilities and separate policies.

The default is an allowlist of normalized public destinations. A project may
receive the `broad_public_egress` feature flag, but all user-authored external
requests from the application HTTP runtime still traverse a trusted outbound
policy layer.

The default build allowlist covers Forge source access, npm registry and
package downloads, GitHub source downloads, and Prisma's pinned engine host
`binaries.prisma.sh`. These are dependency-install destinations only; adding a
runtime destination or a deployment-provider API requires a separate policy.

That layer:

- blocks loopback, private, link-local, carrier-grade NAT, multicast, cloud
  metadata, internal service, Forge control-plane, storage, database,
  credential-proxy, and provider-management destinations;
- rejects unsafe raw-IP destinations and unapproved ports;
- resolves and validates DNS at connection time;
- revalidates every redirect and protects against DNS rebinding;
- applies request, response-size, duration, concurrency, and bandwidth limits;
- never attaches Forge or provider credentials;
- records bounded, privacy-conscious audit and usage evidence;
- supports immediate per-project revocation.

Runtime code cannot widen its own network policy. A manifest request is
intersected with the trusted project entitlement.

Cloudflare Outbound Workers do not intercept requests made from Durable
Objects. The managed state and realtime gateways therefore expose no
user-controlled outbound networking; custom repository-authored Durable Object
classes remain deferred until Forge has an enforceable network boundary for
them.

## Preview access

Supported policy modes are `public`, `repository`, and `share_token`.

- Access mode is snapshotted per deployment.
- Forge credentials are never sent to a preview origin.
- Repository access starts at the reserved
  `/.forge/access/bootstrap#code=...` route. The URL fragment is not sent in
  the HTTP request or referrer.
- Edge-owned bootstrap code posts the single-use code to
  `/.forge/access/exchange` before repository code runs, then navigates to a
  clean URL.
- The exchange issues a signed, short-lived, `Secure`, `HttpOnly`, host-only
  `__Host-forge-access` cookie with `Path=/`, scoped to one deployment and
  hostname; codes and credentials are redacted from logs.
- Every authenticated request checks the deployment access epoch. Token
  revocation, repository visibility changes, or removal of a user's qualifying
  repository read permission increments that epoch.
- The same access epoch gates realtime ticket issuance and active sessions;
  revocation invalidates unused tickets and closes affected connections.
- After access validation, the edge strips every edge-owned access cookie and
  authentication header before dispatch. Repository code receives only
  application cookies and ordinary request headers.
- Share tokens are high entropy, hashed at rest, revocable, scoped, and removed
  from logs and referrers.
- A share link carries its raw secret only in
  `/.forge/access/bootstrap#share=...`. The self-contained edge page loads no
  third-party resources, clears browser history, and exchanges the secret
  before repository code runs.
- Share exchange returns the same kind of short-lived, `Secure`, `HttpOnly`,
  host-only `__Host-forge-access` cookie with `Path=/`; every request checks a
  share-access epoch incremented on revocation or replacement.
- Authenticated preview responses use private or appropriately partitioned
  caching.
- visibility changes, token revocation, repository privacy changes, deletion,
  disablement, and suspension bypass immutable caching.

Preview state is isolated from production by default. A preview cannot request
broader visibility, raw provider access, egress, or quotas than its trusted
environment policy.

## Response and routing policy

The serving and dispatch layers:

- normalize and validate hostnames and paths;
- never list storage or synthesize arbitrary object keys;
- prevent traversal and ambiguous encoding;
- serve or route only deployments recorded as ready and eligible;
- attach baseline `nosniff`, referrer, and permissions headers;
- reserve platform headers and remove provider internals;
- reject parent-domain `Set-Cookie` response headers during the
  `sites.smol.ai` period while explicitly accepting that hosted JavaScript can
  still attempt parent-domain cookie writes;
- avoid platform-generated broad CORS;
- keep suspension and access checks outside immutable artifact caches.

Dynamic applications may need their own response headers. The exact
forwarding, override, cookie, caching, CORS, framing, and service-worker rules
are part of the proposed
[application response-header contract](application-runtime-v1.md#requests-and-response-headers).
Application headers cannot override platform security, routing, access,
cookie, or suspension controls.

## Secrets

Runtime and build secrets are outside the first dynamic release.

When introduced, they require a separate reviewed contract covering:

- encrypted storage and key rotation;
- build versus runtime scope;
- environment isolation;
- least-privilege injection;
- log and error redaction;
- versioning and revocation;
- provider adapter transport;
- preview access and fork behavior;
- audit without value disclosure.

Provider master credentials remain behind trusted adapters and are never a
user-configurable secret.

## Abuse, suspension, and denial of wallet

Deploy must include:

- public abuse reporting;
- phishing, malware, spam, open-proxy, cryptomining, and anomalous-egress
  detection and response;
- account, organization, repository, project, deployment, hostname, preview,
  application-component, and runtime suspension;
- append-only operator audit with actor and reason;
- artifact quarantine and retention controls;
- rate, spend, traffic, provider-operation, and cleanup alerts;
- hard project, tenant, provider, and global circuit breakers;
- realtime-specific breakers for handshake storms, connection cardinality,
  fan-out amplification, traffic, log volume, and provider notification
  failure;
- documented takedown, appeal, restoration, and evidence retention.

Hosted artifacts and provider endpoints cannot bypass the Forge serving
suspension check. Suspension propagation is a measured service objective.

## Required security tests

Before constrained dynamic Deploy:

- cross-user and cross-organization project, deployment, state, realtime, log,
  and preview authorization;
- stale SHA, stale plan, duplicate event, duplicate provider operation, and
  compare-and-set promotion;
- build escape, symlink, path, output mutation, cache poisoning, and credential
  access;
- runtime cross-binding, managed-state cross-project access, raw-provider
  escape, custom-operation bypass, and provider-management access;
- state serialization, atomic mutation-and-emit, cross-scope rejection,
  preview isolation, fixed-shard/cold-scope limits, retention, and explicit
  destruction;
- realtime cross-tenant scope and topic access, ticket replay, exact-origin
  mismatch, opaque-grant inspection and forgery, stale generation, stale
  access epoch, high-cardinality topic creation, fan-out amplification,
  hibernation wakeup, ticket and message-body redaction;
- connect-buffer-snapshot races, snapshot cursor/ETag behavior, duplicate and
  missing event sequences, bootstrap buffer overflow, and fallback close-code
  behavior;
- activation and rollback drain, suspension of active and hibernated sockets,
  missed-provider-notification containment, quota exhaustion, polling
  equivalence, and project and global circuit breakers;
- egress private, metadata, internal host, raw IP, unsafe port, redirect, DNS
  rebinding, size, duration, and quota bypass;
- repository and share-token bootstrap leak, browser-history, replay, scope,
  revocation-epoch, cache, and suspension;
- HTTP and JavaScript parent-domain cookie behavior, cookie-jar denial of
  service, CORS, OAuth redirect, frame, `postMessage`, and same-site requests;
- provider partial failure, cancellation, retry, cleanup, and reconciliation;
- state retention, explicit destruction, and recovery;
- abuse suspension while artifacts and runtimes are cached.

## Production graduation gates

Deploy remains pre-production on `sites.smol.ai`. Production requires:

1. the dedicated registrable hosting domain and migration described above;
2. exact-SHA, stale-plan rejection, configurable publication, preview access,
   rollback, disablement, and suspension verified end to end;
3. all required authorization, origin, build, runtime, egress, state,
   realtime, and partial-failure tests passing;
4. operational quotas, billing attribution, audit, cleanup, abuse response, and
   global circuit breakers;
5. provider reconciliation and credential rotation procedures;
6. published and verified suspension propagation and rollback objectives;
7. a fixture completing push -> checks -> preview -> promotion -> rollback with
   owning-surface evidence.
