Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
pnpm-workspace.yaml
build
scratchpad
docker-compose*
Expand Down
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ ENVIRONMENT='dev'

APP_ENV='development'
LIFECYCLE_MODE='all'
# Public UI origin used by host preview auth bootstrap redirects.
LIFECYCLE_UI_URL=
# Public preview host suffix, for URLs like https://3000--<slug>.preview.lifecycle.dev/.
CHAT_PREVIEW_DOMAIN=
PORT='3000'

FASTLY_TOKEN='1234'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:

strategy:
matrix:
node-version: [20.x]
node-version: [22.x]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tools]
node = "20"
node = "22"
pnpm = "9.15.0"
kubectl = "latest"
helm = "latest"
Expand Down
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
strict-peer-dependencies=false
package-manager-strict=false
package-manager-strict=true
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

## Unreleased

### Added

- Provider-agnostic workspace runtime backends: admins can select and configure **Kubernetes**
(default), **OpenSandbox**, **E2B**, **Modal**, or **Daytona** as the agent-session workspace
backend, with a capability catalog (`GET /api/v2/ai/workspace-runtime/backends`) and per-backend
connection tests (`POST /api/v2/ai/workspace-runtime/backends/{id}/test-connection`).
- Backend credentials (`opensandbox.apiKey`, `e2b.apiKey`, `daytona.apiKey`, `modal.tokenId`,
`modal.tokenSecret`) are now encrypted at rest with `ENCRYPTION_KEY`; existing plaintext values
keep working and are migrated to ciphertext on the next config save. Read responses only ever
expose `*Configured` presence flags.
- `PUT /api/v2/ai/config/agent-session/runtime` now merges `workspaceBackend` per-backend blocks
instead of replacing the whole section: omitted blocks are preserved, present blocks are replaced
as a whole (with omit-to-preserve for secret fields), and an explicit `<backend>: null` removes a
stored block — refused while non-ended sandboxes still reference that provider.

### Security

- `ENABLE_AUTH=true` is mandatory for any shared or network-reachable deployment. With auth
disabled, the workspace-backend configuration write path and the test-connection probe are
exposed unauthenticated, allowing credential replacement and server-side request probing.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM node:20-slim AS base
FROM node:22-slim AS base

ARG PORT

Expand Down
21 changes: 11 additions & 10 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ helm_resource(

local_resource(
'lifecycle-keycloak-github-idp-sync',
cmd='sh sysops/tilt/scripts/sync_keycloak_github_idp.sh {namespace} {secret}'.format(
cmd='KEYCLOAK_GITHUB_DEFAULT_SCOPE="repo user:email" sh sysops/tilt/scripts/sync_keycloak_github_idp.sh {namespace} {secret}'.format(
namespace=app_namespace,
secret=github_idp_secret_name,
),
Expand Down Expand Up @@ -359,15 +359,15 @@ if lifecycle_prod:
docker_build(
lifecycle_app,
".",
dockerfile="sysops/dockerfiles/tilt.app.dockerfile",
dockerfile="sysops/dockerfiles/tilt.app.Dockerfile",
build_args=dict(lifecycle_app_build_args, LIFECYCLE_BUILD="prod"),
)
else:
docker_build_with_restart(
lifecycle_app,
".",
entrypoint=["/app_setup_entrypoint.sh"],
dockerfile="sysops/dockerfiles/tilt.app.dockerfile",
dockerfile="sysops/dockerfiles/tilt.app.Dockerfile",
build_args=lifecycle_app_build_args,
live_update=[
sync("./src", "/app/src"),
Expand Down Expand Up @@ -495,13 +495,14 @@ k8s_resource(
)

# Ngrok for Keycloak
k8s_yaml('sysops/tilt/ngrok-keycloak.yaml')
k8s_resource(
'ngrok-keycloak',
port_forwards=['4041:4040'], # Different local port for Keycloak ngrok admin
labels=["infra"],
resource_deps=['lifecycle-keycloak']
)
if ngrok_keycloak_domain:
k8s_yaml('sysops/tilt/ngrok-keycloak.yaml')
k8s_resource(
'ngrok-keycloak',
port_forwards=['4041:4040'], # Different local port for Keycloak ngrok admin
labels=["infra"],
resource_deps=['lifecycle-keycloak']
)

##################################
# Keycloak (deployed via helm-charts lifecycle-keycloak)
Expand Down
61 changes: 61 additions & 0 deletions __mocks__/modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright 2026 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Jest manual mock for the 'modal' gRPC SDK: providers/modal.ts loads it via a dynamic import
// that @swc/jest transpiles to require(), which jest resolves to this mock automatically.

export class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotFoundError';
}
}

export const Probe = {
withTcp: (port: number) => ({ kind: 'tcp', port }),
};

export const modalMocks = {
clientCtor: jest.fn(),
clientClose: jest.fn(),
appsFromName: jest.fn(),
secretsFromName: jest.fn(),
secretsFromObject: jest.fn(),
imagesFromRegistry: jest.fn(),
imagesFromId: jest.fn(),
imagesDelete: jest.fn(),
sandboxesCreate: jest.fn(),
sandboxesFromId: jest.fn(),
};

export class ModalClient {
apps = { fromName: modalMocks.appsFromName };
secrets = { fromName: modalMocks.secretsFromName, fromObject: modalMocks.secretsFromObject };
images = {
fromRegistry: modalMocks.imagesFromRegistry,
fromId: modalMocks.imagesFromId,
delete: modalMocks.imagesDelete,
};
sandboxes = { create: modalMocks.sandboxesCreate, fromId: modalMocks.sandboxesFromId };

constructor(params?: unknown) {
modalMocks.clientCtor(params);
}

close(): void {
modalMocks.clientClose();
}
}
2 changes: 1 addition & 1 deletion app.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

ARG BASE_IMAGE_TAG=v1
ARG BASE_IMAGE_TAG=v2
FROM lifecycleoss/app-base:${BASE_IMAGE_TAG} AS packages

ARG PORT
Expand Down
4 changes: 2 additions & 2 deletions base.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM node:20-slim
FROM node:22-slim

ARG TARGETARCH

Expand Down Expand Up @@ -60,4 +60,4 @@ RUN npm install dotenv-cli --global
ENV BUILD_MODE=yes
ENV DATABASE_URL=no-db

WORKDIR /app
WORKDIR /app
1 change: 1 addition & 0 deletions docs/schema/yaml/1.0.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ services:
branchName: ''
# @param services.configuration.data (required)
data:

# @param services.dev
dev:
# @param services.dev.image (required)
Expand Down
20 changes: 18 additions & 2 deletions helm/environments/local/lifecycle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,30 @@ global:
value: 'false'
- name: ENABLE_AUTH
value: 'true'
- name: APP_HOST
value: 'http://localhost:5001'
- name: LIFECYCLE_UI_URL
value: 'http://localhost:3000'
- name: CHAT_PREVIEW_DOMAIN
value: 'localhost:5001'
- name: AGENT_SESSION_WORKSPACE_IMAGE
value: 'lifecycle-workspace:latest'
- name: AGENT_SESSION_WORKSPACE_EDITOR_IMAGE
value: 'codercom/code-server:4.98.2'
- name: AGENT_SESSION_WORKSPACE_GATEWAY_IMAGE
value: 'lifecycle-workspace:latest'
- name: AGENT_SESSION_WORKSPACE_BACKEND
value: 'opensandbox'
- name: OPEN_SANDBOX_PROTOCOL
value: 'http'
- name: OPEN_SANDBOX_DOMAIN
value: 'opensandbox-server.opensandbox-system.svc.cluster.local'
- name: OPEN_SANDBOX_API_KEY
value: 'lifecycle-opensandbox-dev'
- name: OPEN_SANDBOX_POOL_REF
value: 'lifecycle-workspace-pool'
- name: OPEN_SANDBOX_USE_SERVER_PROXY
value: 'true'
envFrom:
- secretRef:
name: app-secrets
Expand Down Expand Up @@ -155,8 +173,6 @@ components:
value: '250'
- name: GITHUB_API_REQUEST_INTERVAL
value: '10000'
- name: LIFECYCLE_UI_URL
value: 'http://localhost:3000'
- name: DD_TRACE_ENABLED
value: 'false'
ports:
Expand Down
79 changes: 79 additions & 0 deletions helm/environments/local/opensandbox-pool.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
apiVersion: sandbox.opensandbox.io/v1alpha1
kind: Pool
metadata:
name: lifecycle-workspace-pool
namespace: opensandbox
labels:
app.kubernetes.io/name: lifecycle-workspace-pool
app.kubernetes.io/part-of: lifecycle
spec:
template:
metadata:
labels:
app.kubernetes.io/name: lifecycle-workspace-pool
app.kubernetes.io/part-of: lifecycle
spec:
restartPolicy: Never
tolerations:
- operator: Exists
volumes:
- name: sandbox-storage
emptyDir: {}
- name: opensandbox-bin
emptyDir: {}
initContainers:
- name: task-executor-installer
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/task-executor:v0.1.0
command:
- /bin/sh
- -c
args:
- |
cp /workspace/server /opt/opensandbox/bin/task-executor && chmod +x /opt/opensandbox/bin/task-executor
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox/bin
- name: execd-installer
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.18
command:
- /bin/sh
- -c
args:
- |
cp ./execd /opt/opensandbox/bin/execd && cp ./bootstrap.sh /opt/opensandbox/bin/bootstrap.sh && chmod +x /opt/opensandbox/bin/execd && chmod +x /opt/opensandbox/bin/bootstrap.sh
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox/bin
containers:
- name: sandbox
image: lifecycle-workspace:latest
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |
exec /opt/opensandbox/bin/task-executor -listen-addr=0.0.0.0:5758 >/tmp/task-executor.log 2>&1
env:
- name: SANDBOX_MAIN_CONTAINER
value: main
- name: EXECD_ENVS
value: /opt/opensandbox/.env
- name: EXECD
value: /opt/opensandbox/bin/execd
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: '2'
memory: 4Gi
volumeMounts:
- name: sandbox-storage
mountPath: /var/lib/sandbox
- name: opensandbox-bin
mountPath: /opt/opensandbox/bin
capacitySpec:
bufferMax: 1
bufferMin: 1
poolMax: 3
poolMin: 1
10 changes: 10 additions & 0 deletions jestSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,13 @@

process.env.PINO_LOGGER = 'false';
process.env.IS_TESTING = 'true';

const bufferModule = require('buffer') as typeof import('buffer') & {
SlowBuffer?: typeof Buffer;
};

// Older JWT transitive dependencies still read `require('buffer').SlowBuffer.prototype`.
// Some Jest/Node combinations omit that legacy export, so mirror Buffer for test imports.
if (!bufferModule.SlowBuffer) {
bufferModule.SlowBuffer = bufferModule.Buffer;
}
Loading
Loading