Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions docs/lambda-microvm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,108 @@ again after expiry. Static assets and request-shaped handlers should remain on
cheaper stateless delivery paths; this target is only the resident-server
adapter.

#### Hosted-app control plane and preview gateway

The service-side control plane is stateful-profile only. It snapshots the
source runtime session under its existing lock, records an exact checkpoint and
AWS idempotency intent in the fenced Redis registry, then launches/restores the
dedicated app-host VM on an isolated BullMQ queue. API pods enqueue lifecycle
work and proxy preview bytes; only worker pods need Lambda MicroVM IAM.
Start requests share the authenticated execution rate limiter. Before enabling
this feature broadly for untrusted multi-tenant traffic, add a plan-aware cap on
active hosted-app leases per owner; the initial feature-flagged slice relies on
the deployment's Lambda MicroVM quota as its hard fleet ceiling.

Authenticated API contract:

```http
POST /v1/hosted-apps
Content-Type: application/json

{
"runtime_session_hint": "conversation-123",
"app_id": "my-app",
"revision": "rev-1",
"language": "node",
"version": ">=22",
"entrypoint": "server.js",
"cwd": ".",
"args": [],
"env": {}
}
```

`GET /v1/hosted-apps/:app_id?runtime_session_hint=...` returns status and a
fresh five-minute `preview_url`; `DELETE` on the same resource terminates the
lease. A revision is immutable. Retrying the identical spec reasserts the
resident process; changing code or launch settings requires a new revision and
captures a new exact checkpoint. An ambiguous provider launch is replayed only
with its persisted token and can never be overwritten by a newer revision.

Preview traffic uses a wildcard **unprivileged origin**, not a path below the
CodeAPI or LibreChat origin. Configure wildcard DNS and TLS such that
`*.apps.example.net` reaches the stateful CodeAPI API service, then set the bare
origin `https://apps.example.net`. The short-lived URL capability is exchanged
for an HttpOnly, Secure, host-only cookie and redirected to `/`; every app gets
its own `happ-<digest>.apps.example.net` origin, so absolute asset paths work
without exposing privileged-origin cookies to AI-generated JavaScript. Use a
dedicated registrable domain in production—do not set broad parent-domain
cookies that also match the app domain.

Set these on both API and worker pods:

| Env | Default | Meaning |
|---|---|---|
| `CODEAPI_HOSTED_APPS_ENABLED` | `false` | Enables the stateful-only lifecycle API, isolated preview gateway, and worker. |
| `CODEAPI_HOSTED_APP_CREDENTIAL_KEY` | — | Base64 of 32 random bytes; AES-GCM encrypts the AWS preview credential stored in Redis. |

Set these only on API pods (the signing key must differ from the credential
key):

| Env | Default | Meaning |
|---|---|---|
| `CODEAPI_HOSTED_APP_PREVIEW_SIGNING_KEY` | — | Different base64 32-byte key for owner-bound preview URL/cookie capabilities. |
| `CODEAPI_HOSTED_APP_PREVIEW_ORIGIN` | — | Bare HTTPS origin for wildcard app hosts, for example `https://apps.example.net`. |

Set these on worker pods in addition to the ordinary stateful/checkpoint
configuration:

| Env | Default | Meaning |
|---|---|---|
| `LAMBDA_MICROVM_APP_IMAGE_ARN` | — | Dedicated `lambda-microvm-app-host` image ARN. |
| `LAMBDA_MICROVM_APP_IMAGE_VERSION` | — | Required pinned image version. |
| `LAMBDA_MICROVM_APP_MAX_DURATION_SECONDS` | `28800` | App-VM hard lifetime. The control plane relaunches an immutable revision after expiry. |
| `LAMBDA_MICROVM_APP_IDLE_SECONDS` | `300` | Seconds idle before AWS suspends the VM. |
| `LAMBDA_MICROVM_APP_SUSPEND_SECONDS` | `900` | Seconds suspended before AWS terminates the VM. Suspended VMs still consume quota. |

The pinned app-host image contract fixes the root-owned control/checkpoint
listener at port 8080, the resident app at port 3000, and resident readiness at
30 seconds. `RunMicrovm` cannot override the image environment; changing this
contract requires publishing a matching image and control-plane revision.

Generate the two keys independently:

```bash
openssl rand -base64 32 # CODEAPI_HOSTED_APP_CREDENTIAL_KEY
openssl rand -base64 32 # CODEAPI_HOSTED_APP_PREVIEW_SIGNING_KEY
```

The preview proxy strips CodeAPI authorization, cookies, forwarded identity,
caller-provided AWS headers, app `Set-Cookie`, app-controlled caching,
cross-origin policy, and external redirects. Gateway responses are private and
non-storable so an older revision cannot survive through the browser cache. It
supports streamed HTTP/SSE and same-origin redirects. WebSockets
are not part of this first resident adapter. A gateway-owned CSP constrains
fetches and subresources to the app origin and disables workers/service workers,
so one app revision cannot leave a persistent worker controlling a later
revision. Top-level app JavaScript can still navigate the owner's browser to an
external origin; treat this experimental viewer as owner-trusted. Before broad
untrusted enablement, serve app content from a separate origin inside a sandboxed
gateway wrapper. The request `env` map is persisted
with the immutable launch spec in the registry; it is configuration, not a
secret store. Add a dedicated secret-reference flow before passing application
secrets to hosted code.

### 3. Generate the split execution-manifest keys

The worker signs each execution manifest; the runner only receives the public
Expand Down
9 changes: 9 additions & 0 deletions helm/codeapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ hardening variables documented in `docs/lambda-microvm/README.md`. This chart
still renders its bundled sandbox-runner, though a Lambda worker does not call
it; a platform-specific stateful deployment may omit that component.

Resident hosted apps are an opt-in capability of that stateful deployment.
Configure `CODEAPI_HOSTED_APPS_ENABLED`, the preview origin, and both hosted-app
keys through `api.extraEnv`; configure only the feature flag and credential key
on `workerSandbox.extraEnv`, along with the dedicated app image settings.
Wildcard DNS and TLS for the preview origin must route to the API service
separately from the normal CodeAPI host. See “Hosted-app control plane and preview gateway” in
`docs/lambda-microvm/README.md`; do not serve previews beneath the privileged
LibreChat/CodeAPI origin.

For an existing affinity/strict deployment from before execution profiles,
first roll the new binary to API and worker pods with
`CODEAPI_EXECUTION_PROFILE` still unset. The inferred stateful compatibility
Expand Down
10 changes: 9 additions & 1 deletion service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=mysecretpassword

# Stateful Lambda resident hosted apps (optional; see docs/lambda-microvm/README.md)
# CODEAPI_HOSTED_APPS_ENABLED=true
# LAMBDA_MICROVM_APP_IMAGE_ARN=arn:aws:lambda:REGION:ACCOUNT:microvm-image:codeapi-app-host
# LAMBDA_MICROVM_APP_IMAGE_VERSION=1
# CODEAPI_HOSTED_APP_PREVIEW_ORIGIN=https://apps.example.net
# CODEAPI_HOSTED_APP_CREDENTIAL_KEY=<openssl rand -base64 32>
# CODEAPI_HOSTED_APP_PREVIEW_SIGNING_KEY=<different openssl rand -base64 32>

# -----------------------------------------------------------------------------
# Stripe: https://shipfa.st/docs/features/payments
# -----------------------------------------------------------------------------
Expand All @@ -20,4 +28,4 @@ STRIPE_WEBHOOK_SECRET=
# Mailgun: https://shipfa.st/docs/features/emails
# -----------------------------------------------------------------------------
# EMAIL_SERVER=smtp://postmaster@[mail.yourdomain.com]:[copied_password]@smtp.mailgun.org:587 (without the brackets)
EMAIL_SERVER=
EMAIL_SERVER=
148 changes: 148 additions & 0 deletions service/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,155 @@ components:
type: string
enum: [default, stateful]

HostedAppStartRequest:
type: object
required:
- runtime_session_hint
- app_id
- revision
- language
- version
- entrypoint
properties:
runtime_session_hint:
type: string
maxLength: 128
pattern: '^[A-Za-z0-9._:-]+$'
adapter:
type: string
enum: [resident]
default: resident
app_id:
type: string
maxLength: 64
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'
revision:
type: string
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
language:
type: string
version:
type: string
entrypoint:
type: string
description: Canonical path relative to the restored stateful workspace.
cwd:
type: string
default: .
args:
type: array
maxItems: 64
items:
type: string
env:
type: object
additionalProperties:
type: string

HostedAppStatus:
type: object
required: [app_id, revision, state, preview_id, updated_at]
properties:
app_id:
type: string
revision:
type: string
state:
type: string
enum: [starting, running, stopping, stopped, failed]
preview_id:
type: string
description: Opaque hosted-app lease identity.
preview_url:
type: string
format: uri
description: Five-minute owner capability exchange URL on the isolated app origin.
hard_deadline_at:
type: integer
format: int64
updated_at:
type: integer
format: int64
error:
type: string

paths:
/hosted-apps:
post:
summary: Start or reassert a resident hosted app
description: >-
Stateful-profile only. Captures an exact workspace checkpoint and runs
the immutable revision in a dedicated Lambda MicroVM app-host image.
operationId: startHostedApp
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HostedAppStartRequest'
responses:
'200':
description: Hosted app is running
content:
application/json:
schema:
$ref: '#/components/schemas/HostedAppStatus'
'400':
$ref: '#/components/responses/BadRequest'
'409':
$ref: '#/components/responses/Conflict'
'429':
description: Hosted app start rate limit exceeded
'503':
description: Lifecycle or provider operation unavailable
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/hosted-apps/{app_id}:
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
- name: app_id
in: path
required: true
schema:
type: string
- name: runtime_session_hint
in: query
required: true
schema:
type: string
maxLength: 128
pattern: '^[A-Za-z0-9._:-]+$'
get:
summary: Get hosted app status and a fresh preview URL
operationId: getHostedApp
responses:
'200':
description: Hosted app status
content:
application/json:
schema:
$ref: '#/components/schemas/HostedAppStatus'
'404':
description: Hosted app not found
delete:
summary: Terminate a hosted app lease
operationId: stopHostedApp
responses:
'200':
description: Hosted app stopped
content:
application/json:
schema:
$ref: '#/components/schemas/HostedAppStatus'
'404':
description: Hosted app not found

/exec:
post:
summary: Execute code
Expand Down
4 changes: 4 additions & 0 deletions service/src/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { executionProfileMiddleware } from './middleware/execution-profile';
import { traceHttpRequest } from './telemetry';
import { env } from './config';
import logger from './logger';
import hostedAppRouter from './hosted-app/router';
import { hostedAppPreviewGateway } from './hosted-app/preview-gateway';

const { LOCAL_MODE: isLocalMode } = env;

Expand All @@ -34,6 +36,7 @@ app.set('trust proxy', 1);
app.use(traceHttpRequest('codeapi.api.request'));
app.use(httpMetricsMiddleware);
app.use(executionProfileMiddleware);
app.use(hostedAppPreviewGateway);

const v1 = Router();

Expand All @@ -53,6 +56,7 @@ app.get('/v1/health', async (_, res) => {

v1.use(isLocalMode ? localAuth : apiKeyAuth);

v1.use('/hosted-apps', hostedAppRouter);
v1.use(serviceRouter);
v1.use(programmaticRouter);

Expand Down
28 changes: 28 additions & 0 deletions service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,34 @@ export const env = {
),
CHECKPOINT_TIMEOUT_MS: configuredNumber(process.env.CODEAPI_CHECKPOINT_TIMEOUT_MS, 60_000),
CHECKPOINT_PREFIX: process.env.CODEAPI_CHECKPOINT_PREFIX ?? 'rtsx-checkpoints/',
/** Dedicated Lambda MicroVM resident-server fleet. This remains an explicit
* stateful-stack capability; the ordinary/default HTTP profile never starts
* or preserves application processes. */
HOSTED_APPS_ENABLED: process.env.CODEAPI_HOSTED_APPS_ENABLED === 'true',
HOSTED_APP_IMAGE_ARN: process.env.LAMBDA_MICROVM_APP_IMAGE_ARN ?? '',
HOSTED_APP_IMAGE_VERSION: process.env.LAMBDA_MICROVM_APP_IMAGE_VERSION || undefined,
/* These values are part of the pinned app-host image contract. RunMicrovm
* cannot inject environment variables into the image, so exposing overrides
* here would only make the control plane call ports the runner never opened. */
HOSTED_APP_CONTROL_PORT: 8080 as number,
HOSTED_APP_PREVIEW_PORT: 3000 as number,
HOSTED_APP_MAX_DURATION_SECONDS: configuredNumber(
process.env.LAMBDA_MICROVM_APP_MAX_DURATION_SECONDS,
28_800,
),
HOSTED_APP_IDLE_SECONDS: configuredNumber(
process.env.LAMBDA_MICROVM_APP_IDLE_SECONDS,
300,
),
HOSTED_APP_SUSPEND_SECONDS: configuredNumber(
process.env.LAMBDA_MICROVM_APP_SUSPEND_SECONDS,
900,
),
HOSTED_APP_START_TIMEOUT_MS: 30_000 as number,
HOSTED_APP_CREDENTIAL_KEY: process.env.CODEAPI_HOSTED_APP_CREDENTIAL_KEY ?? '',
HOSTED_APP_PREVIEW_ORIGIN: process.env.CODEAPI_HOSTED_APP_PREVIEW_ORIGIN ?? '',
HOSTED_APP_PREVIEW_SIGNING_KEY:
process.env.CODEAPI_HOSTED_APP_PREVIEW_SIGNING_KEY ?? '',
};

const default_run_memory_limit = 256 * 1024 * 1024;
Expand Down
Loading