Skip to content
Draft
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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ Code Interpreter (internally `codeapi`, the prefix used by its env vars, images,
4. Files are persisted/retrieved via the **File Server** (backed by S3)
5. Tool calls from within sandboxes are routed through the **Tool Call Server**

## Execution profiles

Code API can run two isolated deployments at the same time:

- `default`: the AWS-free HTTP/libkrun path, with stateless executions.
- `stateful`: the AWS Lambda MicroVM path, with runtime-session affinity.

Set `CODEAPI_EXECUTION_PROFILE` consistently on an API deployment and its
workers. The default profile keeps the existing `python-queue` and
`other-queue`; the stateful profile uses `stateful-python-queue` and
`stateful-other-queue`. This allows both deployments to share Redis without
cross-consuming jobs.

Trusted callers should send `X-CodeAPI-Expected-Profile: default|stateful` on
every Code API request. A request that reaches the wrong deployment fails
before enqueue with HTTP 409 and `code=execution_profile_mismatch`; every
response advertises the actual deployment in `X-CodeAPI-Execution-Profile`.
Omitting the expected-profile header remains supported for older clients, but
provides no wrong-endpoint protection. There is deliberately no silent
fallback between profiles and no automatic workspace or file migration.

## Sandbox Isolation

Two modes are supported:
Expand Down
2 changes: 2 additions & 0 deletions docs/lambda-microvm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ builds.

```bash
CODEAPI_SANDBOX_BACKEND=lambda-microvm
CODEAPI_EXECUTION_PROFILE=stateful
CODEAPI_RUNTIME_SESSION_MODE=affinity # warm sessions + checkpoints
LAMBDA_MICROVM_IMAGE_ARN=<from step 4>
LAMBDA_MICROVM_IMAGE_VERSION=<exact version from step 4> # required for affinity/strict
Expand Down Expand Up @@ -310,6 +311,7 @@ appear in `api/src/config.ts`.
| Env | Default | Meaning |
|---|---|---|
| `CODEAPI_SANDBOX_BACKEND` | `http` | `http` (byte-identical to today) or `lambda-microvm`. |
| `CODEAPI_EXECUTION_PROFILE` | inferred | `default` for the HTTP/stateless deployment or `stateful` for the Lambda affinity/strict deployment. Stateful API and worker processes consume isolated BullMQ queues. |
| `CODEAPI_RUNTIME_SESSION_MODE` | `stateless` | `stateless` \| `affinity` \| `strict`. `affinity` and `strict` require the `lambda-microvm` backend. See [Operating modes](#operating-modes). |
| `CODEAPI_RUNTIME_SESSION_LOCK_WAIT_MS` | `15000` | How long a stateful execution waits for the session lock before returning `RUNTIME_SESSION_BUSY` (HTTP 409). |

Expand Down
9 changes: 9 additions & 0 deletions helm/codeapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ platform rather than templated here: external ingress/service mesh, KEDA-style
queue-depth autoscaling, and cloud-IAM secret delivery (the env hooks below
cover all of them).

**Execution profile.** This chart deploys the AWS-free `default` profile and
sets `CODEAPI_EXECUTION_PROFILE=default` on both API and worker pods. That
profile requires the HTTP sandbox backend in stateless mode and retains the
existing `python-queue` / `other-queue` BullMQ names. A separate stateful
Lambda MicroVM deployment must use `CODEAPI_EXECUTION_PROFILE=stateful`; it
then consumes `stateful-python-queue` / `stateful-other-queue`, so both stacks
may safely share Redis without consuming each other's jobs. Do not mix API
and worker profile values within one deployment.

**Authentication.** Outside local mode the API verifies JWTs. Configure the
verifier through environment variables on the api component, e.g.:

Expand Down
2 changes: 2 additions & 0 deletions helm/codeapi/templates/api-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ spec:
{{ include "codeapi.otel.env" (dict "root" . "serviceName" "aiml-codeapi-api") | nindent 12 }}
- name: CODEAPI_HARDENED_SANDBOX_MODE
value: {{ .Values.hardenedSandboxMode | quote }}
- name: CODEAPI_EXECUTION_PROFILE
value: {{ .Values.executionProfile | quote }}
# Redis connection
- name: REDIS_HOST
value: {{ include "codeapi.redis.host" . }}
Expand Down
2 changes: 2 additions & 0 deletions helm/codeapi/templates/worker-sandbox-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ spec:
{{ include "codeapi.otel.env" (dict "root" . "serviceName" "aiml-codeapi-service-worker") | nindent 12 }}
- name: CODEAPI_HARDENED_SANDBOX_MODE
value: {{ .Values.hardenedSandboxMode | quote }}
- name: CODEAPI_EXECUTION_PROFILE
value: {{ .Values.executionProfile | quote }}
- name: SANDBOX_ENDPOINT
value: "http://{{ include "codeapi.fullname" . }}-sandbox-runner:{{ .Values.workerSandbox.sandbox.port }}/api/v2"
- name: EGRESS_GATEWAY_URL
Expand Down
5 changes: 5 additions & 0 deletions helm/codeapi/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ internalServiceAuth:

hardenedSandboxMode: true

# Stable identity advertised by this API/worker deployment. The bundled chart
# is the AWS-free HTTP/libkrun profile. A separate Lambda MicroVM deployment
# must set this to `stateful`; the service then uses isolated BullMQ queues.
executionProfile: default

otel:
enabled: false
# OTLP/HTTP collector endpoint, e.g. "http://opentelemetry-collector.observability:4318".
Expand Down
107 changes: 106 additions & 1 deletion service/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ openapi: '3.0.0'
info:
title: LibreChat Code Interpreter API
version: '1.0.0'
description: API for sandbox code execution and file management
description: >-
API for sandbox code execution and file management. Trusted callers should
assert the intended deployment with X-CodeAPI-Expected-Profile on every
request; responses advertise the actual profile.
servers:
- url: https://api.librechat.ai/v1
description: LibreChat API server
Expand All @@ -17,6 +20,45 @@ components:
scheme: bearer
bearerFormat: JWT

parameters:
ExpectedExecutionProfile:
name: X-CodeAPI-Expected-Profile
in: header
required: false
description: >-
Trusted routing assertion. A mismatched endpoint returns HTTP 409
before any work is enqueued. Optional only for backwards compatibility.
schema:
type: string
enum: [default, stateful]

headers:
ExecutionProfile:
description: Execution profile served by this deployment.
schema:
type: string
enum: [default, stateful]

responses:
InvalidExecutionProfile:
description: Invalid expected execution profile
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
content:
application/json:
schema:
$ref: '#/components/schemas/ExecutionProfileError'
ExecutionProfileMismatch:
description: The request reached a different execution profile
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
content:
application/json:
schema:
$ref: '#/components/schemas/ExecutionProfileError'

schemas:
FileRef:
type: object
Expand Down Expand Up @@ -108,6 +150,12 @@ components:
type: array
items:
$ref: '#/components/schemas/RequestFile'
runtime_session_hint:
type: string
description: >-
Stable opaque hint for stateful runtime reuse. The server binds it
to the authenticated tenant and user. Ignored by the default
stateless profile.

FileObject:
type: object
Expand Down Expand Up @@ -156,12 +204,29 @@ components:
details:
type: string

ExecutionProfileError:
type: object
required: [error, code, actual_profile]
properties:
error:
type: string
code:
type: string
enum: [invalid_execution_profile, execution_profile_mismatch]
expected_profile:
type: string
actual_profile:
type: string
enum: [default, stateful]

paths:
/exec:
post:
summary: Execute code
description: Execute code with specified language and parameters
operationId: executeCode
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
requestBody:
required: true
content:
Expand All @@ -171,6 +236,9 @@ paths:
responses:
'200':
description: Successful execution
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
content:
application/json:
schema:
Expand All @@ -181,6 +249,10 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
'400':
$ref: '#/components/responses/InvalidExecutionProfile'
'409':
$ref: '#/components/responses/ExecutionProfileMismatch'
'503':
description: Service unavailable
content:
Expand All @@ -192,6 +264,7 @@ paths:
get:
summary: Download a file
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
- name: session_id
in: path
required: true
Expand All @@ -205,6 +278,9 @@ paths:
responses:
'200':
description: File content
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
content:
application/octet-stream:
schema:
Expand All @@ -216,10 +292,16 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
'400':
$ref: '#/components/responses/InvalidExecutionProfile'
'409':
$ref: '#/components/responses/ExecutionProfileMismatch'

/upload:
post:
summary: Upload files
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
requestBody:
required: true
content:
Expand All @@ -237,6 +319,9 @@ paths:
responses:
'200':
description: Successful upload
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
content:
application/json:
schema:
Expand All @@ -247,11 +332,16 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
'400':
$ref: '#/components/responses/InvalidExecutionProfile'
'409':
$ref: '#/components/responses/ExecutionProfileMismatch'

/files/{session_id}:
get:
summary: Get files information
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
- name: session_id
in: path
required: true
Expand All @@ -265,17 +355,25 @@ paths:
responses:
'200':
description: Files information
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FileObject'
'400':
$ref: '#/components/responses/InvalidExecutionProfile'
'409':
$ref: '#/components/responses/ExecutionProfileMismatch'

/files/{session_id}/{fileId}:
delete:
summary: Delete a file
parameters:
- $ref: '#/components/parameters/ExpectedExecutionProfile'
- name: session_id
in: path
required: true
Expand All @@ -289,9 +387,16 @@ paths:
responses:
'200':
description: File deleted successfully
headers:
X-CodeAPI-Execution-Profile:
$ref: '#/components/headers/ExecutionProfile'
'500':
description: Error deleting file
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'400':
$ref: '#/components/responses/InvalidExecutionProfile'
'409':
$ref: '#/components/responses/ExecutionProfileMismatch'
2 changes: 2 additions & 0 deletions service/src/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import programmaticRouter from './service/programmatic-router';
import { connection } from './queue';
import { metricsHandler } from './metrics';
import { httpMetricsMiddleware } from './middleware/httpMetrics';
import { executionProfileMiddleware } from './middleware/execution-profile';
import { traceHttpRequest } from './telemetry';
import { env } from './config';
import logger from './logger';
Expand All @@ -32,6 +33,7 @@ app.disable('x-powered-by');
app.set('trust proxy', 1);
app.use(traceHttpRequest('codeapi.api.request'));
app.use(httpMetricsMiddleware);
app.use(executionProfileMiddleware);

const v1 = Router();

Expand Down
18 changes: 16 additions & 2 deletions service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dotenv.config();
import { nanoid } from 'nanoid';
import type * as t from './types';
import { Languages } from './enum';
import { resolveExecutionProfile } from './execution-profile';

export const languageConfig: Record<Languages | string, t.LanguageConfig | undefined> = {
[Languages.bash]: { language: 'bash', version: '5.2.0', fileName: 'script.sh' },
Expand Down Expand Up @@ -259,6 +260,9 @@ export function resolveRuntimeSessionMode(
);
}

const sandboxBackend = resolveSandboxBackend(process.env.CODEAPI_SANDBOX_BACKEND);
const runtimeSessionMode = resolveRuntimeSessionMode(process.env.CODEAPI_RUNTIME_SESSION_MODE);

export const env = {
PORT: process.env.SERVICE_PORT ?? 3112,
LOCAL_MODE: process.env.LOCAL_MODE === 'true',
Expand Down Expand Up @@ -344,7 +348,7 @@ export const env = {
* (current Kubernetes/libkrun sandbox-runner).
* - `lambda-microvm`: AWS Lambda MicroVM backend.
*/
SANDBOX_BACKEND: resolveSandboxBackend(process.env.CODEAPI_SANDBOX_BACKEND),
SANDBOX_BACKEND: sandboxBackend,
/**
* Runtime session affinity for stateful sandbox backends.
* - `stateless` (default): no runtime sessions; `runtime_session_hint` ignored.
Expand All @@ -353,7 +357,17 @@ export const env = {
* - `strict`: same serialized session semantics, and a session hint is
* required instead of degrading requests without one to stateless.
*/
RUNTIME_SESSION_MODE: resolveRuntimeSessionMode(process.env.CODEAPI_RUNTIME_SESSION_MODE),
RUNTIME_SESSION_MODE: runtimeSessionMode,
/**
* Deployment identity used by trusted callers to route each agent to the
* intended execution stack. `default` is HTTP/stateless; `stateful` is
* Lambda MicroVM with session affinity. The startup policy rejects mixed
* tuples so an endpoint cannot claim one profile while running the other.
*/
EXECUTION_PROFILE: resolveExecutionProfile(
process.env.CODEAPI_EXECUTION_PROFILE,
runtimeSessionMode,
),
RUNTIME_SESSION_LOCK_WAIT_MS: configuredNumber(
process.env.CODEAPI_RUNTIME_SESSION_LOCK_WAIT_MS,
15_000,
Expand Down
5 changes: 0 additions & 5 deletions service/src/enum/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ export enum Jobs {
execute = 'execute',
}

export enum Queues {
python = 'python-queue',
other = 'other-queue',
}

export enum Languages {
bash = 'bash',
js = 'js',
Expand Down
Loading