Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b31f756
Add Azure Container Apps compute provider
tebieshi Jul 28, 2026
4d3632e
Add Azure service principal auth for containers
tebieshi Jul 28, 2026
7b01d11
Add Azure private registry auth and CPU-memory scaling
tebieshi Jul 29, 2026
97929c7
Add Azure HTTP debug logging
tebieshi Jul 29, 2026
43f70d3
Fix Azure service principal auth support
tebieshi Jul 29, 2026
d64e8e5
Fix Azure standby and resume support for task sleeps
tebieshi Jul 29, 2026
32ffc45
Derive Azure timeoutRemainingMs from auto-delete policy
tebieshi Jul 29, 2026
16bdb0a
Add Azure sandbox size and egress TLS inspection options
tebieshi Jul 29, 2026
b77d6ae
Add Azure XS/XL sandbox presets with disk sizing
tebieshi Jul 30, 2026
df1d133
Use shared Select component for provider fields
tebieshi Jul 30, 2026
08f67f9
Fix Azure auth precedence, purge logs before snapshots, and update
tebieshi Jul 30, 2026
14d87a3
Purge closed detached logs before Azure snapshots, rename sleep-check
tebieshi Jul 30, 2026
2f72bca
Create aca-sandboxes-compute-provider.md
tebieshi Jul 30, 2026
7aceb93
Document proper Azure sandbox sizes and remove unused type
tebieshi Jul 30, 2026
73d148a
Merge branch 'RooCodeInc:develop' into feat/aca-sandboxes-compute-pro…
tebieshi Jul 30, 2026
5552164
Rename migration files to use underscores
tebieshi Jul 30, 2026
11ecf0a
Fix Azure size presets, improve sandbox lifecycle and credential
tebieshi Jul 30, 2026
b79ca06
Make Azure auto-delete suspension-anchored backstop, enlarge
tebieshi Jul 30, 2026
62c4a6c
Handle null token in Azure credential chain
tebieshi Jul 30, 2026
7b73f96
Fix Azure CPU rounding for small tier millicores
tebieshi Jul 30, 2026
617f09f
Merge remote-tracking branch 'authoritative/develop' into feat/aca-sa…
tebieshi Jul 30, 2026
f25ffdc
Add sleep check indexes migration
tebieshi Jul 30, 2026
6104d0f
Replace sleep-check indexes entirely with azure-inclusive v2
tebieshi Jul 30, 2026
b7d819c
Anchor Azure auto-delete deadline to stoppedAt
tebieshi Jul 30, 2026
e1c1931
Make sleep check index migration idempotent
tebieshi Jul 30, 2026
6df7067
Guard against malformed Azure timestamps to prevent NaN leaks
tebieshi Jul 30, 2026
b044709
Remove fixed width from sandbox provider select
tebieshi Jul 30, 2026
6183ea3
Omit stopped deadline when Azure stateDetails missing
tebieshi Jul 30, 2026
84217b6
Potential fix for pull request finding
tebieshi Jul 30, 2026
c345bf3
Fix Azure auth docs and settings UI improvements
tebieshi Jul 30, 2026
12e64a8
Record resume handle in mutation context for standby resumes
tebieshi Jul 30, 2026
a17ab15
Move optional Azure auth fields to advanced settings
mrubens Jul 31, 2026
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
5 changes: 5 additions & 0 deletions .changeset/aca-sandboxes-compute-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roomote/web': minor
---

Add Azure Container Apps Sandboxes (preview) as a compute provider: sandboxes run as hardware-isolated microVMs with memory+disk snapshots, sub-second suspend/resume standby, deterministic preview URLs with on-demand wake, and configurable sandbox size (XS–XL) and egress TLS inspection. The worker disk image can be provisioned automatically during setup, including from private registries. Auth via service principal, managed identity, or ambient az login.
41 changes: 39 additions & 2 deletions SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -737,9 +737,46 @@ BL_API_KEY=...
BL_WORKSPACE=...
# Optional prebuilt Blaxel image override
BLAXEL_IMAGE=sandbox/roomote-worker:<version>
```

`E2B_TEMPLATE_ID`, `DAYTONA_SNAPSHOT_NAME`, and `BLAXEL_IMAGE` can also be provisioned
# Azure Container Apps (preview)
DEFAULT_COMPUTE_PROVIDER=azure
AZURE_SUBSCRIPTION_ID=...
AZURE_RESOURCE_GROUP=...
AZURE_SANDBOX_GROUP=...
AZURE_SANDBOX_REGION=...
AZURE_SANDBOX_DISK_IMAGE=...
# Optional: user-assigned managed identity client ID for Azure-hosted
# controllers; omit to use the ambient Azure credential chain
AZURE_CLIENT_ID=...
# Optional (recommended for Docker installs): service principal auth —
# all three together. az login inside containers is impractical, so
# Docker/compose deployments should use this or a managed identity.
AZURE_TENANT_ID=...
AZURE_CLIENT_ID=<service-principal-app-id>
AZURE_CLIENT_SECRET=...
Comment thread
tebieshi marked this conversation as resolved.
Outdated
# Optional: pull credentials for baking the worker disk image from a private
# registry (GHCR: token owner's GitHub username + PAT with read:packages)
AZURE_SANDBOX_REGISTRY_USERNAME=...
AZURE_SANDBOX_REGISTRY_TOKEN=...
```

Azure auth uses the ambient Azure credential chain instead of an API key:
`az login` for local runs, or a managed identity (optionally user-assigned via
`AZURE_CLIENT_ID`) when the controller itself runs in Azure. Containerized
installs (docker compose, including `deploy/install.sh`) cannot use `az
login` inside the container — configure the service principal triple
(`AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET`), created with
`az ad sp create-for-rbac --name <name> --skip-assignment` plus a
`Container Apps SandboxGroup Data Owner` role assignment on the sandbox
group, or run the controller on an Azure VM with a managed identity. One-time sandbox
group bootstrap: `aca sandboxgroup create --name <group> --location <region>
--set-config` — the calling principal is granted the Container Apps
SandboxGroup Data Owner role automatically; grant it explicitly to any
additional principal (for example a deployed controller's managed identity).
Azure Container Apps sandboxes are in public preview, so expect API drift.

`E2B_TEMPLATE_ID`, `DAYTONA_SNAPSHOT_NAME`, `BLAXEL_IMAGE`, and
`AZURE_SANDBOX_DISK_IMAGE` can also be provisioned
automatically during setup when a registry-qualified `DOCKER_WORKER_IMAGE`
is configured — the setup wizard and the Settings → Sandboxes page build the
worker base artifact in your provider account after credentials are saved.
Expand Down
5 changes: 5 additions & 0 deletions apps/bullmq/src/scheduled-jobs/sleep-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ async function createSleepCheckClient(provider: ComputeProvider) {
provider: 'blaxel',
envFallback: await resolveComputeProviderEnvValues('blaxel'),
});
case 'azure':
return createComputeProviderClient({
provider: 'azure',
envFallback: await resolveComputeProviderEnvValues('azure'),
});
case 'docker':
return createComputeProviderClient({ provider: 'docker' });
default:
Expand Down
22 changes: 19 additions & 3 deletions apps/bullmq/src/scheduled-jobs/standby-retention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { activeRunStatuses, type RunStatus } from '@roomote/types';

const LOG_PREFIX = '[standbyRetention]';
const MS_PER_HOUR = 60 * 60 * 1_000;
const STANDBY_PROVIDERS = ['docker', 'blaxel'] as const;
const STANDBY_PROVIDERS = ['docker', 'blaxel', 'azure'] as const;

type StandbyProvider = (typeof STANDBY_PROVIDERS)[number];

Expand Down Expand Up @@ -42,6 +42,8 @@ export function selectStandbyEvictions(
const DEFAULT_POLICY = {
docker: { maxCount: 10, maxAgeHours: 24 },
blaxel: { maxCount: 25, maxAgeHours: 168 },
// Suspended ACA sandboxes cost almost nothing, so azure retention is generous.
azure: { maxCount: 50, maxAgeHours: 720 },
} as const;

function parsePolicyInteger(
Expand All @@ -63,7 +65,12 @@ export function resolveStandbyRetentionPolicy(
maxCount: number;
maxAgeMs: number;
} {
const prefix = provider === 'docker' ? 'DOCKER' : 'BLAXEL';
const prefix =
provider === 'docker'
? 'DOCKER'
: provider === 'azure'
? 'AZURE'
: 'BLAXEL';
const defaults = DEFAULT_POLICY[provider];
const maxCount = parsePolicyInteger(
env[`${prefix}_STANDBY_MAX_COUNT`],
Expand All @@ -74,7 +81,9 @@ export function resolveStandbyRetentionPolicy(
env[`${prefix}_STANDBY_MAX_AGE_HOURS`],
defaults.maxAgeHours,
1,
168,
// Providers with a higher default keep their ceiling (azure: 720h);
// others stay capped at 168h as before.
Math.max(168, defaults.maxAgeHours),
);

return { maxCount, maxAgeMs: maxAgeHours * MS_PER_HOUR };
Expand All @@ -85,6 +94,13 @@ async function createClient(provider: StandbyProvider) {
return createComputeProviderClient({ provider: 'docker' });
}

if (provider === 'azure') {
return createComputeProviderClient({
provider: 'azure',
envFallback: await resolveComputeProviderEnvValues('azure'),
});
}

return createComputeProviderClient({
provider: 'blaxel',
envFallback: await resolveComputeProviderEnvValues('blaxel'),
Expand Down
55 changes: 55 additions & 0 deletions apps/controller/src/RoomoteController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
DOCKER_SPAWN_TIMEOUT_MS,
spawnE2bWorker,
spawnBlaxelWorker,
spawnAzureWorker,
spawnModalWorker,
} from './compute-providers';

Expand Down Expand Up @@ -306,6 +307,60 @@ export class RoomoteController extends BaseController {
});
return;
}
case 'azure': {
const azureSubscriptionId = resolvedEnv.AZURE_SUBSCRIPTION_ID;
const azureResourceGroup = resolvedEnv.AZURE_RESOURCE_GROUP;
const azureSandboxGroup = resolvedEnv.AZURE_SANDBOX_GROUP;
const azureRegion = resolvedEnv.AZURE_SANDBOX_REGION;
const azureDiskImage = resolvedEnv.AZURE_SANDBOX_DISK_IMAGE;

if (!azureSubscriptionId) {
throw new Error(
'AZURE_SUBSCRIPTION_ID is required to spawn Azure workers',
);
}

if (!azureResourceGroup) {
throw new Error(
'AZURE_RESOURCE_GROUP is required to spawn Azure workers',
);
}

if (!azureSandboxGroup) {
throw new Error(
'AZURE_SANDBOX_GROUP is required to spawn Azure workers',
);
}

if (!azureRegion) {
throw new Error(
'AZURE_SANDBOX_REGION is required to spawn Azure workers',
);
}

if (!azureDiskImage) {
throw new Error(
'AZURE_SANDBOX_DISK_IMAGE is required to spawn Azure workers',
);
}

await spawnAzureWorker(taskRun, authToken, {
deploymentSlug,
azureTags: this.buildSandboxTags(),
azureSubscriptionId,
azureResourceGroup,
azureSandboxGroup,
azureRegion,
azureDiskImage,
azureClientId: resolvedEnv.AZURE_CLIENT_ID,
azureTenantId: resolvedEnv.AZURE_TENANT_ID,
azureClientSecret: resolvedEnv.AZURE_CLIENT_SECRET,
azureSize: resolvedEnv.AZURE_SANDBOX_SIZE,
azureTimeoutMs: timeoutMs,
localTarballPath: this.localWorkerReleasePath,
});
return;
}
default: {
const _exhaustive: never = provider;
throw new Error(`Unsupported compute provider: ${_exhaustive}`);
Expand Down
1 change: 1 addition & 0 deletions apps/controller/src/compute-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { cleanupStaleDockerSandboxes } from './docker-sandbox-security';
export { spawnDaytonaWorker } from './spawn-daytona-worker';
export { spawnE2bWorker } from './spawn-e2b-worker';
export { spawnBlaxelWorker } from './spawn-blaxel-worker';
export { spawnAzureWorker } from './spawn-azure-worker';
Loading
Loading