mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
fix(daytona): set per-sandbox user/org defaults (#3175)
* feat(daytona): re-add Daytona cloud provider * fix(daytona): tighten live provider behavior * fix(daytona): harden reconnect and dashboard flows * fix(daytona): use platform sandbox defaults * fix(daytona): add user and org defaults * fix(ux): stop echoing shell script on startup --------- Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com> Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
This commit is contained in:
parent
564b5001a4
commit
a60d238dfc
3 changed files with 136 additions and 33 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.30.13",
|
||||
"version": "0.30.14",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -91,25 +91,18 @@ const DAYTONA_ALLOWED_METADATA_KEYS = new Set([
|
|||
|
||||
export const SANDBOX_SIZES: SandboxSize[] = [
|
||||
{
|
||||
id: "small",
|
||||
cpu: 2,
|
||||
memory: 4,
|
||||
disk: 30,
|
||||
label: "2 vCPU · 4 GiB RAM · 30 GiB disk",
|
||||
id: "user-default",
|
||||
cpu: 1,
|
||||
memory: 1,
|
||||
disk: 3,
|
||||
label: "User default (1 vCPU · 1 GiB RAM · 3 GiB disk)",
|
||||
},
|
||||
{
|
||||
id: "medium",
|
||||
id: "org-default",
|
||||
cpu: 4,
|
||||
memory: 8,
|
||||
disk: 50,
|
||||
label: "4 vCPU · 8 GiB RAM · 50 GiB disk",
|
||||
},
|
||||
{
|
||||
id: "large",
|
||||
cpu: 8,
|
||||
memory: 16,
|
||||
disk: 100,
|
||||
label: "8 vCPU · 16 GiB RAM · 100 GiB disk",
|
||||
disk: 10,
|
||||
label: "Org default (4 vCPU · 8 GiB RAM · 10 GiB disk)",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -317,6 +310,27 @@ function resolveSandboxSizeFromEnv(): SandboxSize | null {
|
|||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Let Daytona apply its own documented defaults unless the user picked an explicit size.
|
||||
*/
|
||||
function getCreateResources(size: SandboxSize):
|
||||
| {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
}
|
||||
| undefined {
|
||||
if (size.id === DEFAULT_SANDBOX_SIZE.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
cpu: size.cpu,
|
||||
memory: size.memory,
|
||||
disk: size.disk,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for a sandbox size or resolve one from environment variables.
|
||||
*/
|
||||
|
|
@ -406,16 +420,17 @@ export async function createServer(name: string): Promise<VMConnection> {
|
|||
const client = await getRequiredClient();
|
||||
const size = _state.sandboxSize;
|
||||
const image = getRequestedImage();
|
||||
const resources = getCreateResources(size);
|
||||
|
||||
logStep(`Creating Daytona sandbox '${name}' (${size.label})...`);
|
||||
const sandbox = await client.create({
|
||||
name,
|
||||
image,
|
||||
resources: {
|
||||
cpu: size.cpu,
|
||||
memory: size.memory,
|
||||
disk: size.disk,
|
||||
},
|
||||
...(resources
|
||||
? {
|
||||
resources,
|
||||
}
|
||||
: {}),
|
||||
labels: buildCreateLabels(),
|
||||
autoStopInterval: 0,
|
||||
autoArchiveInterval: 0,
|
||||
|
|
@ -723,10 +738,65 @@ function getPtySize(): {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a small bootstrap script so the PTY only has to exec a file path.
|
||||
*
|
||||
* The script clears the shell's echoed command line before launching the agent.
|
||||
*/
|
||||
async function prepareInteractiveBootstrapScript(sandboxId: string, cmd: string): Promise<string> {
|
||||
const sandbox = await ensureSandboxStarted(sandboxId);
|
||||
const homeDir = await getSandboxHomeDir(sandboxId);
|
||||
const remotePath = `${homeDir}/.spawn-interactive-session.sh`;
|
||||
const script = `#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Clear the shell's echoed bootstrap command before the agent UI takes over.
|
||||
printf '\\033[1A\\r\\033[2K\\r'
|
||||
|
||||
${cmd}
|
||||
`;
|
||||
|
||||
await sandbox.fs.uploadFile(Buffer.from(script), remotePath);
|
||||
await sandbox.process.executeCommand(`chmod 700 ${shellQuote(remotePath)}`);
|
||||
return remotePath;
|
||||
}
|
||||
|
||||
function consumeTerminalLine(buffer: string): {
|
||||
line: string;
|
||||
rest: string;
|
||||
} | null {
|
||||
const newlineIndex = buffer.search(/[\r\n]/);
|
||||
if (newlineIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let lineEnd = newlineIndex + 1;
|
||||
if (buffer[newlineIndex] === "\r" && buffer[newlineIndex + 1] === "\n") {
|
||||
lineEnd += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
line: buffer.slice(0, lineEnd),
|
||||
rest: buffer.slice(lineEnd),
|
||||
};
|
||||
}
|
||||
|
||||
function shouldSuppressBootstrapEcho(line: string, bootstrapScript: string): boolean {
|
||||
const trimmed = line.trim();
|
||||
return trimmed === `exec ${shellQuote(bootstrapScript)}`;
|
||||
}
|
||||
|
||||
async function runInteractivePty(sandboxId: string, cmd: string): Promise<number> {
|
||||
if (!cmd || /\0/.test(cmd)) {
|
||||
throw new Error("Invalid command: must be non-empty and must not contain null bytes");
|
||||
}
|
||||
|
||||
const sandbox = await ensureSandboxStarted(sandboxId);
|
||||
const decoder = new TextDecoder();
|
||||
const { cols, rows } = getPtySize();
|
||||
const bootstrapScript = await prepareInteractiveBootstrapScript(sandboxId, cmd);
|
||||
let startupBuffer = "";
|
||||
let filteringStartupEcho = true;
|
||||
const pty = await sandbox.process.createPty({
|
||||
id: `spawn-${randomUUID()}`,
|
||||
cols,
|
||||
|
|
@ -737,11 +807,33 @@ async function runInteractivePty(sandboxId: string, cmd: string): Promise<number
|
|||
LANG: process.env.LANG || "en_US.UTF-8",
|
||||
},
|
||||
onData: (data) => {
|
||||
process.stdout.write(
|
||||
decoder.decode(data, {
|
||||
stream: true,
|
||||
}),
|
||||
);
|
||||
const text = decoder.decode(data, {
|
||||
stream: true,
|
||||
});
|
||||
|
||||
if (!filteringStartupEcho) {
|
||||
process.stdout.write(text);
|
||||
return;
|
||||
}
|
||||
|
||||
startupBuffer += text;
|
||||
|
||||
for (;;) {
|
||||
const consumed = consumeTerminalLine(startupBuffer);
|
||||
if (!consumed) {
|
||||
break;
|
||||
}
|
||||
|
||||
startupBuffer = consumed.rest;
|
||||
if (shouldSuppressBootstrapEcho(consumed.line, bootstrapScript)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filteringStartupEcho = false;
|
||||
process.stdout.write(consumed.line + startupBuffer);
|
||||
startupBuffer = "";
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -759,7 +851,8 @@ async function runInteractivePty(sandboxId: string, cmd: string): Promise<number
|
|||
process.stdin.resume();
|
||||
process.stdin.setRawMode?.(true);
|
||||
const result = await asyncTryCatch(async () => {
|
||||
await pty.sendInput(`exec bash -lc ${shellQuote(cmd)}\n`);
|
||||
await pty.waitForConnection();
|
||||
await pty.sendInput(`exec ${shellQuote(bootstrapScript)}\n`);
|
||||
return pty.wait();
|
||||
});
|
||||
|
||||
|
|
@ -768,7 +861,15 @@ async function runInteractivePty(sandboxId: string, cmd: string): Promise<number
|
|||
process.stdin.setRawMode?.(false);
|
||||
process.stdin.pause();
|
||||
await asyncTryCatch(() => pty.disconnect());
|
||||
process.stdout.write(decoder.decode());
|
||||
const tail = decoder.decode();
|
||||
if (filteringStartupEcho) {
|
||||
startupBuffer += tail;
|
||||
if (!shouldSuppressBootstrapEcho(startupBuffer, bootstrapScript)) {
|
||||
process.stdout.write(startupBuffer);
|
||||
}
|
||||
} else {
|
||||
process.stdout.write(tail);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
throw result.error;
|
||||
|
|
@ -789,7 +890,7 @@ export async function interactiveSession(cmd: string): Promise<number> {
|
|||
throw new Error("No Daytona sandbox is active");
|
||||
}
|
||||
|
||||
const exitCode = await runInteractivePty(_state.sandboxId, cmd);
|
||||
const exitCode = await runInteractiveDaytonaCommand(_state.sandboxId, cmd);
|
||||
process.stderr.write("\n");
|
||||
logWarn(`Session ended. Your sandbox '${_state.sandboxId}' may still be running.`);
|
||||
logWarn(`Manage or delete it in the Daytona dashboard: ${DAYTONA_DASHBOARD_URL}`);
|
||||
|
|
|
|||
|
|
@ -76,10 +76,12 @@ OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
|||
| `DAYTONA_API_KEY` | Daytona API key | _(prompted)_ |
|
||||
| `DAYTONA_SANDBOX_NAME` | Sandbox name | _(prompted)_ |
|
||||
| `DAYTONA_IMAGE` | Base sandbox image | `daytonaio/sandbox:latest` |
|
||||
| `DAYTONA_SANDBOX_SIZE` | Spawn preset (`small`, `medium`, `large`) | `small` |
|
||||
| `DAYTONA_CPU` | vCPU override | _(preset)_ |
|
||||
| `DAYTONA_MEMORY` | Memory override in GiB | _(preset)_ |
|
||||
| `DAYTONA_DISK` | Disk override in GiB | _(preset)_ |
|
||||
| `DAYTONA_SANDBOX_SIZE` | Spawn preset (`user-default`, `org-default`) | `user-default` |
|
||||
| `DAYTONA_CPU` | vCPU override | `1` when partially overridden |
|
||||
| `DAYTONA_MEMORY` | Memory override in GiB | `1` when partially overridden |
|
||||
| `DAYTONA_DISK` | Disk override in GiB | `3` when partially overridden |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter API key | _(OAuth or prompted)_ |
|
||||
|
||||
If you leave all sandbox sizing variables unset, Spawn defers to Daytona's platform defaults: 1 vCPU, 1 GiB RAM, and 3 GiB disk. Set `DAYTONA_SANDBOX_SIZE=org-default` to request Daytona's documented organization per-sandbox limit: 4 vCPU, 8 GiB RAM, and 10 GiB disk.
|
||||
|
||||
Signed preview URLs are generated on demand for web dashboards. SSH access tokens are minted only when you connect and are never stored in Spawn history.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue