feat: add opt-in docker runtime and runtime config commands#824
feat: add opt-in docker runtime and runtime config commands#824vishkrish200 wants to merge 28 commits intomainfrom
Conversation
b00233a to
ef63895
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed:
runStartupopts type omits most runtime override fields- Added all missing runtime override fields (runtimeCpus, runtimeMemory, runtimeGpus, runtimeReadOnly, runtimeNetwork, runtimeCapDrop, runtimeTmpfs) to the runStartup function's opts parameter type to match RuntimeOverrideFlagOptions interface.
- ✅ Fixed: Duplicated
mergeRuntimeConfigwith subtly different deep-copy behavior- Removed duplicate mergeRuntimeConfig and isPlainObject implementations from CLI runtime-overrides.ts, imported them from @composio/ao-core instead, and exported them from core's index.ts to ensure consistent deep-copy behavior across the codebase.
Or push these changes by commenting:
@cursor push 0ffeb14692
Preview (0ffeb14692)
diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts
--- a/packages/cli/src/commands/start.ts
+++ b/packages/cli/src/commands/start.ts
@@ -69,10 +69,7 @@
formatProjectTypeForDisplay,
} from "../lib/project-detection.js";
import { formatAttachCommand } from "../lib/attach.js";
-import {
- appendStringOption,
- resolveRuntimeOverride,
-} from "../lib/runtime-overrides.js";
+import { appendStringOption, resolveRuntimeOverride } from "../lib/runtime-overrides.js";
const DEFAULT_PORT = 3000;
const IS_TTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -184,7 +181,7 @@
*/
async function promptAgentSelection(): Promise<{
orchestratorAgent: string;
- workerAgent: string
+ workerAgent: string;
} | null> {
if (canPromptForInstall()) {
const available = await detectAvailableAgents();
@@ -365,18 +362,17 @@
if (available.length > 0 || !canPromptForInstall()) return available;
console.log(chalk.yellow("⚠ No supported agent runtime detected."));
- console.log(chalk.dim(" You can install one now (recommended) or continue and install later.\n"));
- const choice = await promptSelect(
- "Choose runtime to install:",
- [
- ...AGENT_INSTALL_OPTIONS.map((option) => ({
- value: option.id,
- label: option.label,
- hint: [option.cmd, ...option.args].join(" "),
- })),
- { value: "skip", label: "Skip for now" },
- ],
+ console.log(
+ chalk.dim(" You can install one now (recommended) or continue and install later.\n"),
);
+ const choice = await promptSelect("Choose runtime to install:", [
+ ...AGENT_INSTALL_OPTIONS.map((option) => ({
+ value: option.id,
+ label: option.label,
+ hint: [option.cmd, ...option.args].join(" "),
+ })),
+ { value: "skip", label: "Skip for now" },
+ ]);
if (choice === "skip") {
return available;
}
@@ -849,6 +845,13 @@
runtime?: string;
runtimeConfig?: string;
runtimeImage?: string;
+ runtimeCpus?: string;
+ runtimeMemory?: string;
+ runtimeGpus?: string;
+ runtimeReadOnly?: boolean;
+ runtimeNetwork?: string;
+ runtimeCapDrop?: string[];
+ runtimeTmpfs?: string[];
},
): Promise<number> {
const runtimeOverride = resolveRuntimeOverride(config, project, opts ?? {});
@@ -1193,8 +1196,16 @@
"AO is already running. What do you want to do?",
[
{ value: "open", label: "Open dashboard", hint: "Keep the current instance" },
- { value: "new", label: "Start new orchestrator", hint: "Add a new session for this project" },
- { value: "restart", label: "Restart everything", hint: "Stop the current instance first" },
+ {
+ value: "new",
+ label: "Start new orchestrator",
+ hint: "Add a new session for this project",
+ },
+ {
+ value: "restart",
+ label: "Restart everything",
+ hint: "Stop the current instance first",
+ },
{ value: "quit", label: "Quit" },
],
"open",
@@ -1281,7 +1292,7 @@
proj.worker = { ...(proj.worker ?? {}), agent: workerAgent };
writeFileSync(config.configPath, yamlStringify(rawConfig, { indent: 2 }));
console.log(chalk.dim(` ✓ Saved to ${config.configPath}\n`));
-
+
config = loadConfig(config.configPath);
project = config.projects[projectId];
}
@@ -1350,7 +1361,11 @@
}
const config = loadConfig();
- const { projectId: _projectId, project } = await resolveProject(config, projectArg, "stop");
+ const { projectId: _projectId, project } = await resolveProject(
+ config,
+ projectArg,
+ "stop",
+ );
const sessionId = `${project.sessionPrefix}-orchestrator`;
const port = config.port ?? 3000;
diff --git a/packages/cli/src/lib/runtime-overrides.ts b/packages/cli/src/lib/runtime-overrides.ts
--- a/packages/cli/src/lib/runtime-overrides.ts
+++ b/packages/cli/src/lib/runtime-overrides.ts
@@ -1,4 +1,5 @@
import type { OrchestratorConfig, ProjectConfig } from "@composio/ao-core";
+import { mergeRuntimeConfig, isPlainObject } from "@composio/ao-core";
export interface RuntimeOverrideFlagOptions {
runtime?: string;
@@ -20,31 +21,6 @@
effectiveRuntimeConfig?: Record<string, unknown>;
}
-function isPlainObject(value: unknown): value is Record<string, unknown> {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
-function mergeRuntimeConfig(
- base?: Record<string, unknown>,
- override?: Record<string, unknown>,
-): Record<string, unknown> | undefined {
- if (!base && !override) return undefined;
-
- const merged: Record<string, unknown> = {};
- for (const source of [base, override]) {
- if (!source) continue;
- for (const [key, value] of Object.entries(source)) {
- const existing = merged[key];
- merged[key] =
- isPlainObject(existing) && isPlainObject(value)
- ? mergeRuntimeConfig(existing, value)
- : value;
- }
- }
-
- return merged;
-}
-
function parseRuntimeConfigOverride(raw?: string): Record<string, unknown> | undefined {
if (!raw) return undefined;
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -78,7 +78,6 @@
export { generateOrchestratorPrompt } from "./orchestrator-prompt.js";
export type { OrchestratorPromptConfig } from "./orchestrator-prompt.js";
-
// Global pause constants and utilities
export {
GLOBAL_PAUSE_UNTIL_KEY,
@@ -97,6 +96,7 @@
readLastJsonlEntry,
resolveProjectIdForSessionId,
} from "./utils.js";
+export { isPlainObject, mergeRuntimeConfig } from "./runtime-selection.js";
export {
getWebhookHeader,
parseWebhookJsonObject,This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
7f98ce6 to
acd0f96
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Shell execution via
-lcbypasses execFile safety- Removed the unsafe shell execution fallback that used
$SHELL -lc <command>and now only uses structured program+args from runtime plugins or the safe fallback.
- Removed the unsafe shell execution fallback that used
- ✅ Fixed: Redundant
isPlainObjectduplicated across runtime modules- Removed the duplicate
isPlainObjectfunction from runtime.ts and imported it from @composio/ao-core where it's already defined and exported.
- Removed the duplicate
Or push these changes by commenting:
@cursor push a45af336a5
Preview (a45af336a5)
diff --git a/packages/cli/src/commands/runtime.ts b/packages/cli/src/commands/runtime.ts
--- a/packages/cli/src/commands/runtime.ts
+++ b/packages/cli/src/commands/runtime.ts
@@ -6,6 +6,7 @@
loadConfigWithPath,
type OrchestratorConfig,
type ProjectConfig,
+ isPlainObject,
} from "@composio/ao-core";
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
import {
@@ -29,10 +30,6 @@
tmpfs?: string[];
}
-function isPlainObject(value: unknown): value is Record<string, unknown> {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
function readRawConfig(path: string): RawConfig {
const parsed = yamlParse(readFileSync(path, "utf-8"));
return isPlainObject(parsed) ? parsed : {};
diff --git a/packages/cli/src/lib/attach.ts b/packages/cli/src/lib/attach.ts
--- a/packages/cli/src/lib/attach.ts
+++ b/packages/cli/src/lib/attach.ts
@@ -17,13 +17,8 @@
info: AttachInfo | null | undefined,
fallback: { program: string; args: string[] },
): Promise<void> {
- const shell = process.env["SHELL"] || "/bin/sh";
- const program = info?.program ?? (info?.command ? shell : fallback.program);
- const args = info?.program
- ? (info.args ?? [])
- : info?.command
- ? ["-lc", info.command]
- : fallback.args;
+ const program = info?.program ?? fallback.program;
+ const args = info?.program ? (info.args ?? []) : fallback.args;
await new Promise<void>((resolve, reject) => {
const child = spawn(program, args, { stdio: "inherit" });This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
- Fixed runStartup opts parameter to include all runtime override fields (runtimeCpus, runtimeMemory, runtimeGpus, runtimeReadOnly, runtimeNetwork, runtimeCapDrop, runtimeTmpfs) to match RuntimeOverrideFlagOptions interface - Removed duplicate mergeRuntimeConfig and isPlainObject implementations from CLI and imported them from core instead, ensuring consistent deep-copy behavior across the codebase - Exported mergeRuntimeConfig and isPlainObject from @composio/ao-core Applied via @cursor push command
1c5d5c2 to
f7196e6
Compare
|
Validation update after the latest rebase:
The branch is clean locally and now sits on top of current |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 868a760. Configure here.


Summary
ao runtime show|set|clearplus Docker runtime override flags and Docker-aware doctor/preflight checksdataDir/worktreeDirkeysWhy
AO was still heavily tmux-shaped outside the runtime plugin layer. Adding Docker as an isolated, reproducible runtime needed more than a new plugin: session metadata, attach flows, config management, and doctor/docs all had to understand per-session runtime selection. This keeps tmux as the local default while enabling Docker-backed sessions for servers and CI.
Validation
pnpm --filter @composio/ao-core typecheckpnpm --filter @composio/ao-core test -- metadata.test.ts recovery-validator.test.ts recovery-actions.test.ts lifecycle-manager.test.ts session-manager.test.tspnpm --filter @composio/ao-plugin-runtime-docker typecheckpnpm --filter @composio/ao-plugin-runtime-docker testpnpm --filter @composio/ao-cli typecheckpnpm --filter @composio/ao-cli exec vitest run __tests__/lib/preflight.test.ts __tests__/commands/spawn.test.ts __tests__/commands/start.test.ts __tests__/commands/open.test.ts __tests__/commands/runtime.test.ts __tests__/scripts/doctor-script.test.tspnpm --filter @composio/ao-web typecheckpnpm --filter @composio/ao-web test -- server-compatibility.test.ts services.test.tsao send, attach withao session attach, and kill the session successfullyNotes
origin/main, which matches AO's existing worktree expectationsao doctormay still fail locally if the launcher is not installed in PATH; that is an environment/setup issue, not a runtime regression in this branch