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
12 changes: 12 additions & 0 deletions .roomote/environments/roomote.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,25 @@ description:
(Redis, API, BullMQ, controller) plus a local Mintlify preview of the public
docs site (apps/docs) on the `docs` port.
initialUrl: http://127.0.0.1:3000/auth/dev-login
# Forward this deployment's compute provider credentials so the nested Roomote
# controller can spawn its own task sandboxes (see "Nested compute" in the
# environment definition docs).
inherit_compute: true
# Also forward the configured source-control providers (GitHub App fields) so
# the nested instance can clone repositories and mint its own repo tokens.
inherit_source_control: true
ports:
- name: web
port: 3000
initial_path: /auth/dev-login
primary: true
- name: docs
port: 3333
# Unproxied so the nested controller's sandboxes can reach the nested API
# directly; the controller command below reads it from ROOMOTE_API_HOST.
- name: api
port: 13001
proxied: false
services:
- postgres16
repositories:
Expand Down
32 changes: 32 additions & 0 deletions apps/docs/environments/definition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ agentInstructions: |
| `docker_projects` | list | no | Existing Compose or Dockerfile projects to build and start. See [Docker projects](#docker-projects). |
| `ports` | list | no | Named preview ports. See [Ports](#ports). |
| `oidc` | map | no | Sandbox OIDC targets. See [OIDC](#oidc). |
| `inherit_compute` | boolean | no | Forward the deployment's compute provider configuration into tasks. See [Nested deployments](#nested-deployments). |
| `inherit_source_control` | boolean | no | Forward the deployment's source-control provider configuration into tasks. See [Nested deployments](#nested-deployments). |
| `mcpServers` | map | no | Custom MCP servers for this environment. See [MCP servers](#mcp-servers). |
| `skills` | map | no | Installable skills by `owner/repo`. See [Skills](#skills). |
| `manualSkills` | list | no | Inline skills defined in the environment. See [Skills](#skills). |
Expand Down Expand Up @@ -452,6 +454,36 @@ oidc:

Each `token_file` must be an absolute path and must be unique across targets.

## Nested deployments

Roomote strips its own provider configuration from task sandboxes: compute
credentials and source-control app secrets never reach an agent. That is the
right default, but it means a Roomote instance running _inside_ an environment
(for example Roomote's own development environment) cannot spawn task
sandboxes of its own or clone repositories. Two opt-in flags forward the
deployment's configuration into tasks in this environment:

```yaml
inherit_compute: true
inherit_source_control: true
```

- `inherit_compute` forwards `DEFAULT_COMPUTE_PROVIDER` plus that provider's
variables (for example `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET`, or the
Roomote Cloud token pair), exactly as the deployment resolves them. Local
Docker cannot be nested and is never forwarded.
- `inherit_source_control` forwards every fully configured source-control
provider (for example the GitHub App slug, App ID, private key, OAuth client,
and webhook secret). Partially configured providers are skipped. The nested
instance then acts as the same app on the same repositories, while webhooks
keep arriving at the outer deployment.

Every task in the environment can read the forwarded values, so enable these
only for environments you trust with the deployment's credentials. The nested
instance also needs to be reachable by the sandboxes it spawns: expose its API
on an unproxied port (`proxied: false`) so `ROOMOTE_<NAME>_HOST` is a direct
machine URL those sandboxes can call.

## Write a definition with a coding agent

Because this page fully describes the schema, you can have a local coding agent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,27 @@ function getAdvancedItems(config: EnvironmentConfig) {
);
}

if (config.inherit_compute || config.inherit_source_control) {
items.push(
<div key="inherit_deployment" className="space-y-2">
<div className="text-xs text-muted-foreground">Nested deployment</div>
{config.inherit_compute ? (
<div className="rounded border border-border/70 px-3 py-2 text-sm">
Tasks receive this deployment&apos;s compute provider configuration
so a nested Roomote instance can spawn its own sandboxes.
</div>
) : null}
{config.inherit_source_control ? (
<div className="rounded border border-border/70 px-3 py-2 text-sm">
Tasks receive this deployment&apos;s source-control provider
configuration so a nested Roomote instance can reach its
repositories.
</div>
) : null}
</div>,
);
}

if (config.skills && Object.keys(config.skills).length > 0) {
items.push(
<div key="skills" className="space-y-2">
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions apps/web/src/components/settings/environments/yaml-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ export function configToYaml(config: EnvironmentConfig): string {
cleanConfig.oidc = config.oidc;
}

if (config.inherit_compute !== undefined) {
cleanConfig.inherit_compute = config.inherit_compute;
}

if (config.inherit_source_control !== undefined) {
cleanConfig.inherit_source_control = config.inherit_source_control;
}

if (config.ports && config.ports.length > 0) {
cleanConfig.ports = config.ports;
}
Expand Down
81 changes: 81 additions & 0 deletions apps/worker/src/commands/__tests__/setup.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions apps/worker/src/commands/__tests__/snapshot.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions apps/worker/src/commands/__tests__/utils.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 31 additions & 2 deletions apps/worker/src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import {
DEFAULT_MODEL_PROVIDER_CREDENTIAL_ENV_VAR_NAMES,
DISABLED_MODEL_PROVIDER_ENV_VAR_NAMES,
NESTED_DEPLOYMENT_ENV_VAR_NAME,
OPENCODE_AUTH_CONTENT_ENV_VAR_NAME,
SANDBOX_OPENROUTER_API_KEY_ENV_VAR_NAME,
TASK_MODEL_CONTEXT_WINDOWS_ENV_VAR_NAME,
TASK_MODEL_COSTS_ENV_VAR_NAME,
TaskPayloadKind,
parseModelProviderEnvKeys,
parseNestedDeploymentEnv,
} from '@roomote/types';

import { ExecutionError } from '../command-executor';
Expand Down Expand Up @@ -127,10 +129,14 @@ const INHERITED_MODEL_PROVIDER_ENV_VAR_NAMES: ReadonlySet<string> = new Set([
function buildEnvironmentWorkspaceEnvVars(
envVars: Record<string, string | undefined>,
launcherSandboxOpenRouterApiKey?: string,
launcherNestedComputeEnv?: Record<string, string> | null,
): Record<string, string> {
const sandboxOpenRouterApiKey =
envVars[SANDBOX_OPENROUTER_API_KEY_ENV_VAR_NAME] ??
launcherSandboxOpenRouterApiKey;
const nestedDeploymentEnv =
parseNestedDeploymentEnv(envVars[NESTED_DEPLOYMENT_ENV_VAR_NAME]) ??
launcherNestedComputeEnv;
const configuredProviderEnvVarNames = new Set(
parseModelProviderEnvKeys(envVars.R_MODEL_ENV_KEYS),
);
Expand All @@ -140,6 +146,7 @@ function buildEnvironmentWorkspaceEnvVars(
if (
value !== undefined &&
name !== SANDBOX_OPENROUTER_API_KEY_ENV_VAR_NAME &&
name !== NESTED_DEPLOYMENT_ENV_VAR_NAME &&
!name.startsWith('R_INFERENCE_GATEWAY_') &&
!INHERITED_MODEL_RUNTIME_ENV_VAR_NAMES.has(name) &&
!INHERITED_MODEL_PROVIDER_ENV_VAR_NAMES.has(name) &&
Expand All @@ -153,6 +160,13 @@ function buildEnvironmentWorkspaceEnvVars(
nestedEnvironmentEnvVars.OPENROUTER_API_KEY = sandboxOpenRouterApiKey;
}

// `inherit_compute` environments: expand the launcher's compute forwarding
// blob into the real provider names so a nested Roomote controller can
// spawn sandboxes with the outer deployment's provider.
if (nestedDeploymentEnv) {
Object.assign(nestedEnvironmentEnvVars, nestedDeploymentEnv);
}

return nestedEnvironmentEnvVars;
}

Expand Down Expand Up @@ -186,8 +200,18 @@ export async function setup({
workerEnv.refreshSystemEnv(process.env);

const runtimeEnv = workerEnv.getRuntimeEnv();
if (SANDBOX_OPENROUTER_API_KEY_ENV_VAR_NAME in runtimeEnv) {
// Launcher-only source names never stay in the worker runtime env: the
// sandbox OpenRouter key maps to OPENROUTER_API_KEY and the nested compute
// blob expands into provider names, both for the nested app only.
const nestedDeploymentEnv = parseNestedDeploymentEnv(
runtimeEnv[NESTED_DEPLOYMENT_ENV_VAR_NAME],
);
if (
SANDBOX_OPENROUTER_API_KEY_ENV_VAR_NAME in runtimeEnv ||
NESTED_DEPLOYMENT_ENV_VAR_NAME in runtimeEnv
) {
delete runtimeEnv[SANDBOX_OPENROUTER_API_KEY_ENV_VAR_NAME];
delete runtimeEnv[NESTED_DEPLOYMENT_ENV_VAR_NAME];
workerEnv.setRuntimeEnv(runtimeEnv);
}

Expand All @@ -204,11 +228,16 @@ export async function setup({
? buildEnvironmentWorkspaceEnvVars(
inheritedWorkspaceEnvVars,
sandboxOpenRouterApiKey ?? workerEnv.sandboxOpenRouterApiKey,
nestedDeploymentEnv,
)
: inheritedWorkspaceEnvVars,
userEnvVars:
isEnvironmentWorkspace && workspaceOpts.userEnvVars
? buildEnvironmentWorkspaceEnvVars(workspaceOpts.userEnvVars)
? buildEnvironmentWorkspaceEnvVars(
workspaceOpts.userEnvVars,
undefined,
nestedDeploymentEnv,
)
: workspaceOpts.userEnvVars,
};
let result: PrepareWorkspaceResult | undefined;
Expand Down
9 changes: 8 additions & 1 deletion apps/worker/src/commands/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,14 @@ export async function snapshot({

// Write source-control tokens under ~/.roomote and set up shell env files
// so file-backed credential helpers can authenticate git operations.
await injectEnvVars(envVars, undefined, { sourceControlToken });
// Snapshot setup is always an environment workspace, so the shell file
// gets the same projection as task setup: outer model transport stays
// out, and the launcher-only nested compute value is expanded by setup
// rather than written raw.
await injectEnvVars(envVars, undefined, {
sourceControlToken,
omitInheritedModelRuntimeEnvFromShell: true,
});

const environmentConfig = await findRuntimeEnvironmentConfig(environmentId);
const taskRun = await sdk.taskRuns.findFirstById(runId);
Expand Down
Loading
Loading