From 8a9fac9e706ac8bb128bd5d2d5bdf246fdc769e0 Mon Sep 17 00:00:00 2001 From: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Date: Fri, 15 May 2026 09:30:58 -0700 Subject: [PATCH 1/6] feat(agent-runtime): add sandboxed harness runtime package --- CHANGELOG.internal.md | 3 + package.json | 11 +- packages/agent-runtime/ASSUMPTIONS.md | 44 + packages/agent-runtime/VALIDATION.md | 211 +++ packages/agent-runtime/package.json | 33 + .../agent-runtime/src/harnesses/claude.ts | 58 + packages/agent-runtime/src/harnesses/codex.ts | 53 + .../agent-runtime/src/harnesses/common.ts | 142 ++ .../agent-runtime/src/harnesses/cursor.ts | 49 + .../agent-runtime/src/harnesses/gemini.ts | 27 + packages/agent-runtime/src/harnesses/index.ts | 46 + .../agent-runtime/src/harnesses/opencode.ts | 25 + packages/agent-runtime/src/harnesses/pi.ts | 25 + packages/agent-runtime/src/index.ts | 6 + packages/agent-runtime/src/runtime.ts | 97 ++ .../agent-runtime/src/sandbox/compute-sdk.ts | 221 +++ packages/agent-runtime/src/sandbox/index.ts | 14 + packages/agent-runtime/src/sandbox/local.ts | 219 +++ packages/agent-runtime/src/schemas.ts | 117 ++ packages/agent-runtime/src/session.ts | 262 +++ packages/agent-runtime/src/types.ts | 251 +++ packages/agent-runtime/test/harnesses.test.ts | 206 +++ packages/agent-runtime/test/runtime.test.ts | 228 +++ packages/agent-runtime/test/sandbox.test.ts | 166 ++ packages/agent-runtime/tsconfig.json | 11 + packages/codex-runner/src/CodexRunner.ts | 2 + packages/cursor-runner/src/CursorRunner.ts | 2 + packages/edge-worker/package.json | 1 + packages/gemini-runner/src/adapters.ts | 1 + pnpm-lock.yaml | 1434 +++++++++++++++-- tsconfig.base.json | 1 + 31 files changed, 3821 insertions(+), 145 deletions(-) create mode 100644 packages/agent-runtime/ASSUMPTIONS.md create mode 100644 packages/agent-runtime/VALIDATION.md create mode 100644 packages/agent-runtime/package.json create mode 100644 packages/agent-runtime/src/harnesses/claude.ts create mode 100644 packages/agent-runtime/src/harnesses/codex.ts create mode 100644 packages/agent-runtime/src/harnesses/common.ts create mode 100644 packages/agent-runtime/src/harnesses/cursor.ts create mode 100644 packages/agent-runtime/src/harnesses/gemini.ts create mode 100644 packages/agent-runtime/src/harnesses/index.ts create mode 100644 packages/agent-runtime/src/harnesses/opencode.ts create mode 100644 packages/agent-runtime/src/harnesses/pi.ts create mode 100644 packages/agent-runtime/src/index.ts create mode 100644 packages/agent-runtime/src/runtime.ts create mode 100644 packages/agent-runtime/src/sandbox/compute-sdk.ts create mode 100644 packages/agent-runtime/src/sandbox/index.ts create mode 100644 packages/agent-runtime/src/sandbox/local.ts create mode 100644 packages/agent-runtime/src/schemas.ts create mode 100644 packages/agent-runtime/src/session.ts create mode 100644 packages/agent-runtime/src/types.ts create mode 100644 packages/agent-runtime/test/harnesses.test.ts create mode 100644 packages/agent-runtime/test/runtime.test.ts create mode 100644 packages/agent-runtime/test/sandbox.test.ts create mode 100644 packages/agent-runtime/tsconfig.json diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index 574f227f1..5d9562f37 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -4,6 +4,9 @@ This changelog documents internal development changes, refactors, tooling update ## [Unreleased] +### Added +- Added `cyrus-agent-runtime`, a standalone experimental TypeScript package for unified agent session orchestration across harnesses and sandbox providers. It includes normalized session config, transcript envelopes, local and ComputeSDK-backed sandbox abstractions, harness adapters for Claude/Codex/Cursor/Gemini plus provisional PI/OpenCode adapters, and focused tests for config, runtime lifecycle, sandbox execution, and transcript parsing. + ## [0.2.50] - 2026-04-30 ### Added diff --git a/package.json b/package.json index 46132fa14..f3203d850 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "qs": ">=6.14.2", "vite": ">=7.1.11", "zod": "4.3.6", - "hono": ">=4.12.7", + "hono": ">=4.12.18", "@hono/node-server": ">=1.19.10", "rollup": ">=4.59.0", "flatted": ">=3.4.0", @@ -67,7 +67,14 @@ "diff": ">=8.0.3", "@tootallnate/once": ">=3.0.1", "@isaacs/brace-expansion": ">=5.0.1", - "tar": ">=7.5.11" + "tar": ">=7.5.11", + "fast-uri": ">=3.1.2", + "ip-address": ">=10.1.1", + "@opentelemetry/sdk-node": ">=0.217.0", + "@opentelemetry/exporter-prometheus": ">=0.217.0", + "@opentelemetry/otlp-transformer>protobufjs": ">=8.0.2", + "@anthropic-ai/sdk": ">=0.91.1", + "@daytonaio/sdk": ">=0.175.0" } }, "lint-staged": { diff --git a/packages/agent-runtime/ASSUMPTIONS.md b/packages/agent-runtime/ASSUMPTIONS.md new file mode 100644 index 000000000..5a8c96372 --- /dev/null +++ b/packages/agent-runtime/ASSUMPTIONS.md @@ -0,0 +1,44 @@ +# Agent Runtime Assumptions + +This package is intentionally built as a new standalone runtime layer with minimal dependency on the existing Cyrus runner packages. + +## Product Contract + +- The package exposes a TypeScript library API first. It does not ship a daemon or CLI in this iteration. +- A session has one Cyrus-owned `sessionId`. Harness-native session identifiers are represented as transcript metadata when a harness emits them. +- Transcript events preserve raw harness JSON whenever possible and wrap it in a stable runtime envelope. +- `addMessage()` queues messages for harnesses that do not support interactive stdin yet. The queue is visible and testable, but delivery is capability-gated. +- `interrupt()` is a soft user-message interruption when supported. `stop()` is lifecycle cancellation and attempts to terminate the running process. + +## Harness Contract + +- Claude, Codex, Cursor, Gemini, PI, and OpenCode are represented as harness adapters. +- Claude, Codex, Cursor, and Gemini command-line conventions are modeled from locally available CLIs and existing public behavior. +- PI and OpenCode are provisional adapters. Their commands and JSON formats are assumptions until real CLI transcripts are supplied. +- Harness adapters own command construction and transcript parsing. They do not own sandbox provisioning. + +## Sandbox Contract + +- Local execution is modeled as a sandbox provider. This keeps local and remote execution behind the same conceptual interface. +- ComputeSDK is the vendor abstraction for remote sandbox providers. +- The common ComputeSDK `runCommand()` API is treated as sufficient for one-shot harness runs. +- Streaming process execution is modeled as a capability, but is not assumed for every ComputeSDK provider. Full interactive harness support requires a provider-specific streaming process implementation. +- Volumes, FUSE mounts, snapshots, ports, and network egress are represented in config types even when a provider cannot enforce them yet. +- Daytona's ComputeSDK provider was smoke-tested with a remote working directory of `/home/daytona`; `/workspace` should not be assumed portable across providers. +- Cursor Agent was smoke-tested inside Daytona by installing the CLI with `curl https://cursor.com/install -fsS | bash` and running `/home/daytona/.local/bin/cursor-agent` with `CURSOR_API_KEY` provided as a secret environment variable. +- Codex Agent was smoke-tested inside Daytona far enough to authenticate and start a turn by materializing `~/.codex/auth.json` as a sensitive runtime file. Passing only `OPENAI_API_KEY` from the local Codex auth file produced a remote 401. The authenticated Codex turn later hit the account usage limit. +- Claude Code was smoke-tested inside Daytona far enough to install, start, and emit `system`/`assistant`/`result` events, but the local `claude.ai` login did not appear to be portable as a simple file or environment secret. Remote Claude completion needs an explicit portable credential such as `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`. + +## Security Contract + +- `env` is safe-to-log configuration. `secrets` must be redacted from transcript and error metadata. +- Secrets are passed into process environments only at execution time. +- Tool permissions are represented as declarative runtime config and translated into harness-native flags where currently known. +- Network egress policy is a declarative provider option in this iteration. Enforcement depends on the selected sandbox provider. + +## Feedback Loops + +- Config schema tests prove the public contract accepts and rejects expected shapes. +- Local sandbox tests prove the local provider can write files and execute commands. +- Harness adapter tests prove command construction and transcript parsing. +- Session runtime tests prove event emission, queueing, stop behavior, and result propagation. diff --git a/packages/agent-runtime/VALIDATION.md b/packages/agent-runtime/VALIDATION.md new file mode 100644 index 000000000..10e1abafe --- /dev/null +++ b/packages/agent-runtime/VALIDATION.md @@ -0,0 +1,211 @@ +# Agent Runtime Validation + +## Automated Checks + +Run from the repository root: + +```bash +pnpm --filter cyrus-agent-runtime typecheck +pnpm --filter cyrus-agent-runtime test:run +pnpm --filter cyrus-agent-runtime build +``` + +Current coverage: + +- Harness command construction and transcript parsing for Claude, Codex, Cursor, Gemini, PI, and OpenCode. +- Local sandbox filesystem and command execution. +- ComputeSDK sandbox wrapper with fake provider. +- Session lifecycle, queued messages, setup commands, transcript events, and result extraction. + +## Real Local Harness Smoke + +This validates `AgentRuntime`, the local sandbox provider, real `codex exec --json`, transcript event parsing, and result extraction. + +```bash +node --input-type=module -e " + import { createAgentSession } from './packages/agent-runtime/dist/index.js'; + const session = await createAgentSession({ + sessionId: 'smoke-codex', + harness: { kind: 'codex', model: 'gpt-5.2' }, + userPrompt: 'Reply exactly: runtime smoke ok', + sandbox: { provider: 'local', workingDirectory: process.cwd() } + }); + const result = await session.start(); + console.log(JSON.stringify({ + success: result.success, + result: result.result, + eventCount: result.events.length + })); +" +``` + +Observed result: + +```json +{"success":true,"result":"runtime smoke ok","eventCount":4} +``` + +## Real Daytona Harness Smoke + +This validates the full remote path: `AgentRuntime`, real ComputeSDK Daytona provider, remote sandbox create/destroy, declarative setup commands inside the sandbox, remote Cursor Agent install, real `cursor-agent --print --output-format stream-json`, transcript events emitted by the agent session running inside Daytona, and result extraction. + +Prerequisites: + +- `DAYTONA_API_KEY` in the environment. +- `CURSOR_API_KEY` in the environment. +- The package has been built with `pnpm --filter cyrus-agent-runtime build`. + +Run from `packages/agent-runtime`: + +```bash +node --input-type=module - <<'JS' +import { daytona } from '@computesdk/daytona'; +import { createAgentSession } from './dist/index.js'; +import { createComputeSdkSandboxProvider } from './dist/sandbox/compute-sdk.js'; + +const provider = createComputeSdkSandboxProvider({ + compute: daytona({ apiKey: process.env.DAYTONA_API_KEY, timeout: 300000 }), +}); +const transcriptKinds = []; +const transcriptRawTypes = []; +let sandboxToDestroy; +const trackingProvider = { + provider: 'daytona', + async create(config) { + const sandbox = await provider.create(config); + sandboxToDestroy = sandbox; + return sandbox; + }, +}; + +try { + const session = await createAgentSession( + { + sessionId: 'daytona-cursor-smoke', + harness: { + kind: 'cursor', + command: '/home/daytona/.local/bin/cursor-agent', + }, + userPrompt: 'Reply exactly: daytona cursor event smoke ok', + env: { + PATH: '/home/daytona/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + }, + secrets: { + CURSOR_API_KEY: process.env.CURSOR_API_KEY, + }, + packages: { + commands: [ + 'curl https://cursor.com/install -fsS | bash', + '/home/daytona/.local/bin/cursor-agent --version', + ], + }, + sandbox: { + provider: 'daytona', + name: `agent-runtime-cursor-${Date.now()}`, + workingDirectory: '/home/daytona', + timeoutMs: 300000, + metadata: { purpose: 'agent-runtime-cursor-event-smoke' }, + }, + }, + { + sandboxProviders: { daytona: trackingProvider }, + callbacks: { + onTranscriptEvent(event) { + transcriptKinds.push(event.kind); + if (event.raw && typeof event.raw === 'object' && 'type' in event.raw) { + transcriptRawTypes.push(event.raw.type); + } + }, + }, + }, + ); + + const result = await session.start(); + console.log(JSON.stringify({ + success: result.success, + result: result.result, + eventCount: result.events.length, + transcriptKinds, + transcriptRawTypes, + sandboxId: sandboxToDestroy?.sandboxId, + })); +} finally { + if (sandboxToDestroy) { + await sandboxToDestroy.destroy(); + } +} +JS +``` + +Observed result: + +```json +{ + "success": true, + "result": "daytona cursor event smoke ok", + "eventCount": 8, + "transcriptKinds": [ + "setup.started", + "setup.completed", + "setup.started", + "setup.completed", + "system", + "user", + "assistant", + "result" + ], + "transcriptRawTypes": ["system", "user", "assistant", "result"] +} +``` + +## Real Daytona Codex Auth Probe + +Codex was validated inside Daytona through runtime-managed sensitive file materialization: + +- `~/.codex/auth.json` was written with `sensitive: true`, and transcript events redacted the content. +- `@openai/codex` installed successfully inside Daytona. +- `codex exec --json --skip-git-repo-check` emitted `thread.started` and `turn.started`. +- Passing only `OPENAI_API_KEY` from local Codex auth produced a remote 401. +- Using `~/.codex/auth.json` authenticated, but the turn hit the account usage limit before completion. + +Observed authenticated-but-limited result: + +```json +{ + "success": false, + "exitCode": 1, + "events": [ + { + "kind": "error", + "raw": { + "type": "error", + "message": "You've hit your usage limit..." + } + }, + { + "kind": "turn.failed" + } + ] +} +``` + +## Real Daytona Claude Auth Probe + +Claude Code was validated inside Daytona far enough to prove event capture from a remote Claude process: + +- `@anthropic-ai/claude-code` installed successfully with a user-local npm prefix. +- `claude --version` returned `2.1.142 (Claude Code)`. +- `claude -p ... --output-format stream-json --verbose` emitted `system`, `assistant`, and `result` events inside Daytona. +- The result was `Not logged in · Please run /login`. + +Observed auth failure: + +```json +{ + "success": false, + "result": "Not logged in · Please run /login", + "events": ["system", "assistant", "result"] +} +``` + +The local Claude auth method is `claude.ai` first-party subscription auth, and no portable `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` was present in the environment. diff --git a/packages/agent-runtime/package.json b/packages/agent-runtime/package.json new file mode 100644 index 000000000..3d16925dd --- /dev/null +++ b/packages/agent-runtime/package.json @@ -0,0 +1,33 @@ +{ + "name": "cyrus-agent-runtime", + "version": "0.2.51", + "description": "Unified agent harness runtime with pluggable sandbox providers", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "ASSUMPTIONS.md", + "VALIDATION.md" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "vitest", + "test:run": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@computesdk/daytona": "^1.7.26", + "computesdk": "^4.0.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.3", + "vitest": "^3.1.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/agent-runtime/src/harnesses/claude.ts b/packages/agent-runtime/src/harnesses/claude.ts new file mode 100644 index 000000000..ba14b6944 --- /dev/null +++ b/packages/agent-runtime/src/harnesses/claude.ts @@ -0,0 +1,58 @@ +import type { HarnessAdapter, NormalizedAgentSessionConfig } from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const claudeHarness: HarnessAdapter = { + kind: "claude", + buildCommand(config: NormalizedAgentSessionConfig) { + const args = [ + "-p", + config.userPrompt, + "--output-format", + "stream-json", + "--verbose", + ]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt) { + args.push("--append-system-prompt", config.systemPrompt); + } + + if (config.permissions?.mode) { + args.push("--permission-mode", config.permissions.mode); + } + + if (config.permissions?.allowedTools?.length) { + args.push("--allowedTools", config.permissions.allowedTools.join(",")); + } + + if (config.permissions?.disallowedTools?.length) { + args.push( + "--disallowedTools", + config.permissions.disallowedTools.join(","), + ); + } + + return createCommand(config, "claude", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("claude", line, context); + }, + extractResult(events) { + const result = [...events].reverse().find((event) => { + return event.kind === "result" && isRecord(event.raw); + }); + return result && + isRecord(result.raw) && + typeof result.raw.result === "string" + ? result.raw.result + : undefined; + }, +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/agent-runtime/src/harnesses/codex.ts b/packages/agent-runtime/src/harnesses/codex.ts new file mode 100644 index 000000000..6a4a03fec --- /dev/null +++ b/packages/agent-runtime/src/harnesses/codex.ts @@ -0,0 +1,53 @@ +import type { HarnessAdapter, NormalizedAgentSessionConfig } from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const codexHarness: HarnessAdapter = { + kind: "codex", + buildCommand(config: NormalizedAgentSessionConfig) { + const args = ["exec", "--json", "--skip-git-repo-check"]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt) { + args.push( + "-c", + `developer_instructions=${JSON.stringify(config.systemPrompt)}`, + ); + } + + if (config.permissions?.mode) { + args.push( + "-c", + `approval_policy=${JSON.stringify(config.permissions.mode)}`, + ); + } + + args.push(config.userPrompt); + + return createCommand(config, "codex", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("codex", line, context); + }, + extractResult(events) { + const message = [...events].reverse().find((event) => { + if (!isRecord(event.raw)) { + return false; + } + const item = event.raw.item; + return isRecord(item) && item.type === "agent_message"; + }); + if (!message || !isRecord(message.raw) || !isRecord(message.raw.item)) { + return undefined; + } + const text = message.raw.item.text; + return typeof text === "string" ? text : undefined; + }, +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/agent-runtime/src/harnesses/common.ts b/packages/agent-runtime/src/harnesses/common.ts new file mode 100644 index 000000000..462f0f313 --- /dev/null +++ b/packages/agent-runtime/src/harnesses/common.ts @@ -0,0 +1,142 @@ +import type { + HarnessCommand, + HarnessKind, + NormalizedAgentSessionConfig, + TranscriptEvent, + TranscriptParseContext, +} from "../types.js"; + +export function resolveHarnessConfig(config: NormalizedAgentSessionConfig) { + return config.harness; +} + +export function resolveModel( + config: NormalizedAgentSessionConfig, +): string | undefined { + return config.model ?? resolveHarnessConfig(config).model; +} + +export function resolveCommand( + config: NormalizedAgentSessionConfig, + defaultCommand: string, +): string { + return resolveHarnessConfig(config).command ?? defaultCommand; +} + +export function withHarnessArgs( + config: NormalizedAgentSessionConfig, + args: string[], +): string[] { + return [...(resolveHarnessConfig(config).args ?? []), ...args]; +} + +export function createCommand( + config: NormalizedAgentSessionConfig, + defaultCommand: string, + args: string[], + options?: { + env?: Record; + stdin?: string; + }, +): HarnessCommand { + return { + command: resolveCommand(config, defaultCommand), + args: withHarnessArgs(config, args), + env: filterEnv(options?.env), + stdin: options?.stdin, + }; +} + +export function parseJsonLine( + kind: HarnessKind, + line: string, + context: TranscriptParseContext, +): TranscriptEvent | undefined { + const trimmed = line.trim(); + if (!trimmed) { + return undefined; + } + + const raw = safeJsonParse(trimmed) ?? trimmed; + return { + sessionId: context.sessionId, + harness: kind, + timestamp: (context.now?.() ?? new Date()).toISOString(), + kind: inferEventKind(raw), + raw, + normalized: normalizeEvent(raw), + }; +} + +function safeJsonParse(value: string): unknown | null { + try { + return JSON.parse(value) as unknown; + } catch { + return null; + } +} + +function inferEventKind(raw: unknown): string { + if (typeof raw === "string") { + return "text"; + } + + if (!isRecord(raw)) { + return "unknown"; + } + + return stringField(raw, "type") ?? stringField(raw, "event") ?? "json"; +} + +function normalizeEvent(raw: unknown): unknown { + if (!isRecord(raw)) { + return undefined; + } + + const type = stringField(raw, "type") ?? stringField(raw, "event"); + const text = + stringField(raw, "text") ?? + stringField(raw, "message") ?? + stringField(raw, "content") ?? + stringField(raw, "result"); + const toolName = + stringField(raw, "tool_name") ?? + stringField(raw, "toolName") ?? + stringField(raw, "name"); + + if (!type && !text && !toolName) { + return undefined; + } + + return { + ...(type ? { type } : {}), + ...(text ? { text } : {}), + ...(toolName ? { toolName } : {}), + }; +} + +function filterEnv( + env: Record | undefined, +): Record | undefined { + if (!env) { + return undefined; + } + const filtered = Object.fromEntries( + Object.entries(env).filter((entry): entry is [string, string] => { + return entry[1] !== undefined; + }), + ); + return Object.keys(filtered).length > 0 ? filtered : undefined; +} + +function stringField( + record: Record, + key: string, +): string | undefined { + const value = record[key]; + return typeof value === "string" ? value : undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/agent-runtime/src/harnesses/cursor.ts b/packages/agent-runtime/src/harnesses/cursor.ts new file mode 100644 index 000000000..6c34cb77d --- /dev/null +++ b/packages/agent-runtime/src/harnesses/cursor.ts @@ -0,0 +1,49 @@ +import type { HarnessAdapter, NormalizedAgentSessionConfig } from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const cursorHarness: HarnessAdapter = { + kind: "cursor", + buildCommand(config: NormalizedAgentSessionConfig) { + const args = ["--print", "--output-format", "stream-json", "--trust"]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if ( + config.permissions?.mode === "plan" || + config.permissions?.mode === "ask" + ) { + args.push("--mode", config.permissions.mode); + } + + if ( + config.permissions?.mode === "bypass" || + config.permissions?.mode === "auto" + ) { + args.push("--force"); + } + + args.push(config.userPrompt); + + return createCommand(config, "cursor-agent", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("cursor", line, context); + }, + extractResult(events) { + const result = [...events].reverse().find((event) => { + return event.kind === "result" && isRecord(event.raw); + }); + return result && + isRecord(result.raw) && + typeof result.raw.result === "string" + ? result.raw.result + : undefined; + }, +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/agent-runtime/src/harnesses/gemini.ts b/packages/agent-runtime/src/harnesses/gemini.ts new file mode 100644 index 000000000..292859fcb --- /dev/null +++ b/packages/agent-runtime/src/harnesses/gemini.ts @@ -0,0 +1,27 @@ +import type { HarnessAdapter, NormalizedAgentSessionConfig } from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const geminiHarness: HarnessAdapter = { + kind: "gemini", + buildCommand(config: NormalizedAgentSessionConfig) { + const args = ["--output-format", "stream-json"]; + const model = resolveModel(config) ?? "gemini-2.5-pro"; + + args.push("--model", model, "--yolo"); + + if (config.permissions?.mode && config.permissions.mode !== "default") { + args.push("--approval-mode", config.permissions.mode); + } + + args.push("-p", config.userPrompt); + + return createCommand(config, "gemini", args, { + env: { + GEMINI_SYSTEM_MD: config.systemPrompt, + }, + }); + }, + parseStdoutLine(line, context) { + return parseJsonLine("gemini", line, context); + }, +}; diff --git a/packages/agent-runtime/src/harnesses/index.ts b/packages/agent-runtime/src/harnesses/index.ts new file mode 100644 index 000000000..442a5f7ad --- /dev/null +++ b/packages/agent-runtime/src/harnesses/index.ts @@ -0,0 +1,46 @@ +import type { + HarnessAdapter, + HarnessCommand, + HarnessKind, + NormalizedAgentSessionConfig, +} from "../types.js"; +import { claudeHarness } from "./claude.js"; +import { codexHarness } from "./codex.js"; +import { cursorHarness } from "./cursor.js"; +import { geminiHarness } from "./gemini.js"; +import { opencodeHarness } from "./opencode.js"; +import { piHarness } from "./pi.js"; + +export type { + HarnessAdapter, + HarnessCommand, + TranscriptParseContext, +} from "../types.js"; + +export { + claudeHarness, + codexHarness, + cursorHarness, + geminiHarness, + opencodeHarness, + piHarness, +}; + +export const harnessAdapters: Record = { + claude: claudeHarness, + codex: codexHarness, + cursor: cursorHarness, + gemini: geminiHarness, + pi: piHarness, + opencode: opencodeHarness, +}; + +export function getHarnessAdapter(kind: HarnessKind): HarnessAdapter { + return harnessAdapters[kind]; +} + +export function buildHarnessInvocation( + config: NormalizedAgentSessionConfig, +): HarnessCommand { + return getHarnessAdapter(config.harness.kind).buildCommand(config); +} diff --git a/packages/agent-runtime/src/harnesses/opencode.ts b/packages/agent-runtime/src/harnesses/opencode.ts new file mode 100644 index 000000000..3d924224e --- /dev/null +++ b/packages/agent-runtime/src/harnesses/opencode.ts @@ -0,0 +1,25 @@ +import type { HarnessAdapter, NormalizedAgentSessionConfig } from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const opencodeHarness: HarnessAdapter = { + kind: "opencode", + buildCommand(config: NormalizedAgentSessionConfig) { + const args = ["run", "--output-format", "json"]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt) { + args.push("--system", config.systemPrompt); + } + + args.push(config.userPrompt); + + return createCommand(config, "opencode", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("opencode", line, context); + }, +}; diff --git a/packages/agent-runtime/src/harnesses/pi.ts b/packages/agent-runtime/src/harnesses/pi.ts new file mode 100644 index 000000000..3b306229a --- /dev/null +++ b/packages/agent-runtime/src/harnesses/pi.ts @@ -0,0 +1,25 @@ +import type { HarnessAdapter, NormalizedAgentSessionConfig } from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const piHarness: HarnessAdapter = { + kind: "pi", + buildCommand(config: NormalizedAgentSessionConfig) { + const args = ["run", "--json"]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt) { + args.push("--system", config.systemPrompt); + } + + args.push("--prompt", config.userPrompt); + + return createCommand(config, "pi", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("pi", line, context); + }, +}; diff --git a/packages/agent-runtime/src/index.ts b/packages/agent-runtime/src/index.ts new file mode 100644 index 000000000..04a42291f --- /dev/null +++ b/packages/agent-runtime/src/index.ts @@ -0,0 +1,6 @@ +export * from "./harnesses/index.js"; +export * from "./runtime.js"; +export * from "./sandbox/index.js"; +export * from "./schemas.js"; +export * from "./session.js"; +export * from "./types.js"; diff --git a/packages/agent-runtime/src/runtime.ts b/packages/agent-runtime/src/runtime.ts new file mode 100644 index 000000000..2263338be --- /dev/null +++ b/packages/agent-runtime/src/runtime.ts @@ -0,0 +1,97 @@ +import { randomUUID } from "node:crypto"; +import { getHarnessAdapter } from "./harnesses/index.js"; +import { createSandboxProvider } from "./sandbox/index.js"; +import { CreateAgentSessionConfigSchema } from "./schemas.js"; +import { RuntimeAgentSession } from "./session.js"; +import type { + AgentSession, + CreateAgentSessionConfig, + NormalizedAgentSessionConfig, + RuntimeCallbacks, + RuntimeHarnessConfig, + RuntimeSecret, + SandboxProvider, +} from "./types.js"; + +export interface CreateAgentRuntimeOptions { + callbacks?: RuntimeCallbacks; + sandboxProviders?: Record; +} + +export class AgentRuntime { + constructor(private readonly options: CreateAgentRuntimeOptions = {}) {} + + async createSession(config: CreateAgentSessionConfig): Promise { + const normalized = normalizeConfig(config); + const adapter = getHarnessAdapter(normalized.harness.kind); + const provider = + this.options.sandboxProviders?.[normalized.sandbox.provider] ?? + createSandboxProvider(normalized.sandbox.provider); + const sandbox = await provider.create(normalized.sandbox); + return new RuntimeAgentSession( + normalized, + adapter, + sandbox, + this.options.callbacks, + ); + } +} + +export function createAgentRuntime( + options?: CreateAgentRuntimeOptions, +): AgentRuntime { + return new AgentRuntime(options); +} + +export async function createAgentSession( + config: CreateAgentSessionConfig, + options?: CreateAgentRuntimeOptions, +): Promise { + return createAgentRuntime(options).createSession(config); +} + +export function normalizeConfig( + config: CreateAgentSessionConfig, +): NormalizedAgentSessionConfig { + const parsed = CreateAgentSessionConfigSchema.parse( + config, + ) as CreateAgentSessionConfig; + const harness = normalizeHarness(parsed.harness, parsed.model); + const secrets = normalizeSecrets(parsed.secrets ?? {}); + return { + ...parsed, + sessionId: parsed.sessionId ?? randomUUID(), + harness, + model: harness.model ?? parsed.model, + env: parsed.env ?? {}, + secrets, + sandbox: parsed.sandbox ?? { + provider: "local", + workingDirectory: process.cwd(), + }, + }; +} + +function normalizeHarness( + harness: CreateAgentSessionConfig["harness"], + model?: string, +): RuntimeHarnessConfig { + if (typeof harness === "string") { + return { kind: harness, model }; + } + return { + ...harness, + model: harness.model ?? model, + }; +} + +function normalizeSecrets( + secrets: Record, +): Record { + return Object.fromEntries( + Object.entries(secrets).map(([key, secret]) => [ + key, + typeof secret === "string" ? { value: secret, redact: true } : secret, + ]), + ); +} diff --git a/packages/agent-runtime/src/sandbox/compute-sdk.ts b/packages/agent-runtime/src/sandbox/compute-sdk.ts new file mode 100644 index 000000000..e07ea19d3 --- /dev/null +++ b/packages/agent-runtime/src/sandbox/compute-sdk.ts @@ -0,0 +1,221 @@ +import type { + CommandExecutionResult, + RunnerSandbox, + RunnerSandboxCapabilities, + RuntimeSandboxConfig, + SandboxFileEntry, + SandboxFilesystem, + SandboxProvider, + SandboxRunCommandOptions, +} from "../types.js"; +import { DEFAULT_RUNNER_SANDBOX_CAPABILITIES } from "./local.js"; + +export interface ComputeSdkFilesystemLike { + readFile?(path: string): Promise; + writeFile?(path: string, content: string): Promise; + readdir?(path: string): Promise; + mkdir?(path: string): Promise; + exists?(path: string): Promise; + remove?(path: string): Promise; + read?(path: string, options?: { encoding?: BufferEncoding }): Promise; + write?(path: string, content: string): Promise; + rm?( + path: string, + options?: { recursive?: boolean; force?: boolean }, + ): Promise; +} + +export interface ComputeSdkSandboxLike { + sandboxId?: string; + id?: string; + provider?: string; + workingDirectory?: string; + filesystem?: ComputeSdkFilesystemLike; + fs?: ComputeSdkFilesystemLike; + runCommand?( + command: string, + options?: SandboxRunCommandOptions, + ): Promise | string>; + destroy?(): Promise; + dispose?(): Promise; +} + +export interface ComputeSdkLike { + sandbox?: { + create(options?: Record): Promise; + getById?(sandboxId: string): Promise; + }; +} + +export interface ComputeSdkSandboxProviderOptions { + compute: ComputeSdkLike; + capabilities?: RunnerSandboxCapabilities; +} + +export class ComputeSdkSandboxProvider implements SandboxProvider { + readonly provider = "computesdk"; + + constructor(private readonly options: ComputeSdkSandboxProviderOptions) {} + + async create(config: RuntimeSandboxConfig): Promise { + const sandbox = config.id + ? ((await this.options.compute.sandbox?.getById?.(config.id)) ?? + (await this.createSandbox(config))) + : await this.createSandbox(config); + return new ComputeSdkRunnerSandbox( + sandbox, + this.options.capabilities ?? DEFAULT_RUNNER_SANDBOX_CAPABILITIES, + config, + ); + } + + private async createSandbox( + config: RuntimeSandboxConfig, + ): Promise { + if (!this.options.compute.sandbox?.create) { + throw new Error("ComputeSDK provider requires compute.sandbox.create()."); + } + return this.options.compute.sandbox.create({ + timeout: config.timeoutMs, + templateId: config.templateId, + metadata: config.metadata, + namespace: config.namespace, + name: config.name, + directory: config.workingDirectory, + volumes: config.volumes, + networkEgress: config.networkEgress, + }); + } +} + +export class ComputeSdkRunnerSandbox implements RunnerSandbox { + readonly sandboxId: string; + readonly provider: string; + readonly workingDirectory?: string; + readonly filesystem: SandboxFilesystem; + + constructor( + private readonly sandbox: ComputeSdkSandboxLike, + readonly capabilities: RunnerSandboxCapabilities, + config: RuntimeSandboxConfig, + ) { + this.sandboxId = sandbox.sandboxId ?? sandbox.id ?? config.id ?? "compute"; + this.provider = sandbox.provider ?? config.provider; + this.workingDirectory = sandbox.workingDirectory ?? config.workingDirectory; + const filesystem = sandbox.filesystem ?? sandbox.fs; + if (!filesystem) { + throw new Error( + "ComputeSDK sandbox does not expose filesystem operations.", + ); + } + this.filesystem = new ComputeSdkFilesystem(filesystem); + } + + async runCommand( + command: string, + options?: SandboxRunCommandOptions, + ): Promise { + if (!this.sandbox.runCommand) { + throw new Error("ComputeSDK sandbox does not expose runCommand()."); + } + const startedAt = Date.now(); + const result = await this.sandbox.runCommand(command, options); + if (typeof result === "string") { + return { + stdout: result, + stderr: "", + exitCode: 0, + durationMs: Date.now() - startedAt, + }; + } + return { + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + exitCode: result.exitCode ?? 0, + durationMs: result.durationMs ?? Date.now() - startedAt, + }; + } + + async destroy(): Promise { + if (this.sandbox.destroy) { + await this.sandbox.destroy(); + return; + } + await this.sandbox.dispose?.(); + } +} + +class ComputeSdkFilesystem implements SandboxFilesystem { + constructor(private readonly filesystem: ComputeSdkFilesystemLike) {} + + async readFile(path: string): Promise { + if (this.filesystem.readFile) { + return this.filesystem.readFile(path); + } + if (this.filesystem.read) { + return this.filesystem.read(path, { encoding: "utf8" }); + } + throw new Error("ComputeSDK filesystem does not support readFile()."); + } + + async writeFile(path: string, content: string): Promise { + if (this.filesystem.writeFile) { + await this.filesystem.writeFile(path, content); + return; + } + if (this.filesystem.write) { + await this.filesystem.write(path, content); + return; + } + throw new Error("ComputeSDK filesystem does not support writeFile()."); + } + + async readdir(path: string): Promise { + if (!this.filesystem.readdir) { + throw new Error("ComputeSDK filesystem does not support readdir()."); + } + const entries = await this.filesystem.readdir(path); + return entries.map((entry) => { + return typeof entry === "string" + ? { name: entry, type: "file" as const } + : entry; + }); + } + + async mkdir(path: string): Promise { + if (!this.filesystem.mkdir) { + throw new Error("ComputeSDK filesystem does not support mkdir()."); + } + await this.filesystem.mkdir(path); + } + + async exists(path: string): Promise { + if (this.filesystem.exists) { + return this.filesystem.exists(path); + } + try { + await this.readFile(path); + return true; + } catch { + return false; + } + } + + async remove(path: string): Promise { + if (this.filesystem.remove) { + await this.filesystem.remove(path); + return; + } + if (this.filesystem.rm) { + await this.filesystem.rm(path, { recursive: true, force: true }); + return; + } + throw new Error("ComputeSDK filesystem does not support remove()."); + } +} + +export function createComputeSdkSandboxProvider( + options: ComputeSdkSandboxProviderOptions, +): ComputeSdkSandboxProvider { + return new ComputeSdkSandboxProvider(options); +} diff --git a/packages/agent-runtime/src/sandbox/index.ts b/packages/agent-runtime/src/sandbox/index.ts new file mode 100644 index 000000000..826f7ada1 --- /dev/null +++ b/packages/agent-runtime/src/sandbox/index.ts @@ -0,0 +1,14 @@ +export * from "./compute-sdk.js"; +export * from "./local.js"; + +import { compute } from "computesdk"; +import type { SandboxProvider } from "../types.js"; +import { createComputeSdkSandboxProvider } from "./compute-sdk.js"; +import { createLocalSandboxProvider } from "./local.js"; + +export function createSandboxProvider(provider: string): SandboxProvider { + if (provider === "local") { + return createLocalSandboxProvider(); + } + return createComputeSdkSandboxProvider({ compute }); +} diff --git a/packages/agent-runtime/src/sandbox/local.ts b/packages/agent-runtime/src/sandbox/local.ts new file mode 100644 index 000000000..88a2a55d6 --- /dev/null +++ b/packages/agent-runtime/src/sandbox/local.ts @@ -0,0 +1,219 @@ +import { spawn } from "node:child_process"; +import { + access, + mkdir, + readdir, + readFile, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import { isAbsolute, join, resolve } from "node:path"; +import type { + CommandExecutionResult, + RunnerSandbox, + RunnerSandboxCapabilities, + RuntimeSandboxConfig, + SandboxFileEntry, + SandboxFilesystem, + SandboxProvider, + SandboxRunCommandOptions, +} from "../types.js"; + +export const UNSUPPORTED_STREAMING_PROCESS_REASON = + "Streaming processes are unsupported until provider-specific APIs are available."; + +export const DEFAULT_RUNNER_SANDBOX_CAPABILITIES: RunnerSandboxCapabilities = { + filesystem: true, + runCommand: true, + streamingProcess: false, +}; + +export interface LocalSandboxProviderOptions { + workingDirectory?: string; + capabilities?: RunnerSandboxCapabilities; +} + +export class LocalSandboxProvider implements SandboxProvider { + readonly provider = "local"; + private readonly defaultWorkingDirectory: string; + private readonly capabilities: RunnerSandboxCapabilities; + + constructor(options: LocalSandboxProviderOptions = {}) { + this.defaultWorkingDirectory = resolve( + options.workingDirectory ?? process.cwd(), + ); + this.capabilities = + options.capabilities ?? DEFAULT_RUNNER_SANDBOX_CAPABILITIES; + } + + async create(config: RuntimeSandboxConfig = { provider: "local" }) { + const workingDirectory = resolve( + config.workingDirectory ?? this.defaultWorkingDirectory, + ); + await mkdir(workingDirectory, { recursive: true }); + return new LocalRunnerSandbox({ + sandboxId: config.id ?? "local", + workingDirectory, + capabilities: this.capabilities, + }); + } +} + +interface LocalRunnerSandboxOptions { + sandboxId: string; + workingDirectory: string; + capabilities: RunnerSandboxCapabilities; +} + +export class LocalRunnerSandbox implements RunnerSandbox { + readonly provider = "local"; + readonly sandboxId: string; + readonly workingDirectory: string; + readonly capabilities: RunnerSandboxCapabilities; + readonly filesystem: SandboxFilesystem; + + constructor(options: LocalRunnerSandboxOptions) { + this.sandboxId = options.sandboxId; + this.workingDirectory = options.workingDirectory; + this.capabilities = options.capabilities; + this.filesystem = new LocalSandboxFilesystem(this.workingDirectory); + } + + async runCommand( + command: string, + options: SandboxRunCommandOptions = {}, + ): Promise { + return runLocalCommand(command, this.workingDirectory, options); + } + + async destroy() { + return; + } +} + +export class LocalSandboxFilesystem implements SandboxFilesystem { + constructor(private readonly workingDirectory: string) {} + + async readFile(path: string) { + return readFile(this.resolvePath(path), "utf8"); + } + + async writeFile(path: string, content: string) { + await writeFile(this.resolvePath(path), content); + } + + async readdir(path: string): Promise { + const entries = await readdir(this.resolvePath(path), { + withFileTypes: true, + }); + return Promise.all( + entries.map(async (entry) => { + const childPath = this.resolvePath(join(path, entry.name)); + const entryStat = await stat(childPath); + return { + name: entry.name, + type: entry.isDirectory() ? "directory" : "file", + size: entryStat.size, + modified: entryStat.mtime, + }; + }), + ); + } + + async mkdir(path: string) { + await mkdir(this.resolvePath(path), { recursive: true }); + } + + async exists(path: string) { + try { + await access(this.resolvePath(path)); + return true; + } catch { + return false; + } + } + + async remove(path: string) { + await rm(this.resolvePath(path), { recursive: true, force: true }); + } + + private resolvePath(path: string) { + return isAbsolute(path) ? path : resolve(this.workingDirectory, path); + } +} + +function runLocalCommand( + command: string, + workingDirectory: string, + options: SandboxRunCommandOptions, +): Promise { + if (options.background) { + return Promise.reject( + new Error( + "Background commands are not supported by LocalSandboxProvider.", + ), + ); + } + + return new Promise((resolveCommand, reject) => { + const startedAt = Date.now(); + const child = spawn(command, { + cwd: options.cwd + ? resolveCommandCwd(workingDirectory, options.cwd) + : workingDirectory, + env: { ...process.env, ...options.env }, + shell: true, + stdio: ["ignore", "pipe", "pipe"], + }); + + let settled = false; + let stdout = ""; + let stderr = ""; + let timeout: NodeJS.Timeout | undefined; + + if (options.timeout !== undefined) { + timeout = setTimeout(() => { + child.kill("SIGTERM"); + }, options.timeout); + } + + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + child.on("error", (error) => { + if (timeout) clearTimeout(timeout); + if (!settled) { + settled = true; + reject(error); + } + }); + child.on("close", (exitCode) => { + if (timeout) clearTimeout(timeout); + if (!settled) { + settled = true; + resolveCommand({ + stdout, + stderr, + exitCode: exitCode ?? 0, + durationMs: Date.now() - startedAt, + }); + } + }); + }); +} + +function resolveCommandCwd(workingDirectory: string, cwd: string) { + return isAbsolute(cwd) ? cwd : resolve(workingDirectory, cwd); +} + +export function createLocalSandboxProvider( + options?: LocalSandboxProviderOptions, +) { + return new LocalSandboxProvider(options); +} diff --git a/packages/agent-runtime/src/schemas.ts b/packages/agent-runtime/src/schemas.ts new file mode 100644 index 000000000..b6745c4ed --- /dev/null +++ b/packages/agent-runtime/src/schemas.ts @@ -0,0 +1,117 @@ +import { z } from "zod"; + +export const HarnessKindSchema = z.enum([ + "claude", + "codex", + "cursor", + "gemini", + "pi", + "opencode", +]); + +export const PermissionModeSchema = z.enum([ + "default", + "plan", + "ask", + "auto", + "bypass", +]); + +export const NetworkEgressModeSchema = z.enum([ + "default", + "disabled", + "proxied", + "unrestricted", +]); + +export const RuntimeNetworkEgressConfigSchema = z.object({ + mode: NetworkEgressModeSchema, + proxyUrl: z.string().optional(), + allowedHosts: z.array(z.string()).optional(), + deniedHosts: z.array(z.string()).optional(), +}); + +export const RuntimeSandboxConfigSchema = z.object({ + provider: z.string().min(1), + id: z.string().optional(), + name: z.string().optional(), + namespace: z.string().optional(), + workingDirectory: z.string().optional(), + templateId: z.string().optional(), + timeoutMs: z.number().int().positive().optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + volumes: z + .array( + z.object({ + name: z.string(), + mountPath: z.string(), + source: z.string().optional(), + kind: z.enum(["bind", "fuse", "provider"]).optional(), + readOnly: z.boolean().optional(), + }), + ) + .optional(), + networkEgress: RuntimeNetworkEgressConfigSchema.optional(), +}); + +export const RuntimeHarnessConfigSchema = z.object({ + kind: HarnessKindSchema, + model: z.string().optional(), + command: z.string().optional(), + args: z.array(z.string()).optional(), +}); + +export const CreateAgentSessionConfigSchema = z.object({ + sessionId: z.string().optional(), + harness: z.union([HarnessKindSchema, RuntimeHarnessConfigSchema]), + model: z.string().optional(), + systemPrompt: z.string().optional(), + userPrompt: z.string().min(1), + env: z.record(z.string(), z.string()).optional(), + secrets: z + .record( + z.string(), + z.union([ + z.string(), + z.object({ + value: z.string(), + redact: z.boolean().optional(), + }), + ]), + ) + .optional(), + packages: z + .object({ + system: z.array(z.string()).optional(), + npm: z.array(z.string()).optional(), + commands: z.array(z.string()).optional(), + }) + .optional(), + files: z + .array( + z.object({ + path: z.string().min(1), + content: z.string(), + sensitive: z.boolean().optional(), + }), + ) + .optional(), + mcps: z.record(z.string(), z.unknown()).optional(), + permissions: z + .object({ + mode: PermissionModeSchema.optional(), + allowedTools: z.array(z.string()).optional(), + disallowedTools: z.array(z.string()).optional(), + }) + .optional(), + memory: z + .object({ + enabled: z.boolean().optional(), + directory: z.string().optional(), + namespace: z.string().optional(), + }) + .optional(), + sandbox: RuntimeSandboxConfigSchema.optional(), + networkEgress: RuntimeNetworkEgressConfigSchema.optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}); diff --git a/packages/agent-runtime/src/session.ts b/packages/agent-runtime/src/session.ts new file mode 100644 index 000000000..3d2957a71 --- /dev/null +++ b/packages/agent-runtime/src/session.ts @@ -0,0 +1,262 @@ +import { EventEmitter } from "node:events"; +import { dirname } from "node:path"; +import type { + AgentSession, + AgentSessionResult, + HarnessAdapter, + NormalizedAgentSessionConfig, + RunnerSandbox, + RuntimeCallbacks, + TranscriptEvent, +} from "./types.js"; + +class AsyncEventBuffer implements AsyncIterable { + private queue: T[] = []; + private waiters: Array<(value: IteratorResult) => void> = []; + private closed = false; + + push(value: T): void { + const waiter = this.waiters.shift(); + if (waiter) { + waiter({ value, done: false }); + return; + } + this.queue.push(value); + } + + close(): void { + this.closed = true; + while (this.waiters.length > 0) { + this.waiters.shift()?.({ value: undefined, done: true }); + } + } + + [Symbol.asyncIterator](): AsyncIterator { + return { + next: () => { + const value = this.queue.shift(); + if (value !== undefined) { + return Promise.resolve({ value, done: false }); + } + if (this.closed) { + return Promise.resolve({ value: undefined, done: true }); + } + return new Promise>((resolve) => { + this.waiters.push(resolve); + }); + }, + }; + } +} + +export class RuntimeAgentSession extends EventEmitter implements AgentSession { + readonly sessionId: string; + readonly harness: NormalizedAgentSessionConfig["harness"]["kind"]; + readonly events: AsyncIterable; + + private readonly eventBuffer = new AsyncEventBuffer(); + private readonly observedEvents: TranscriptEvent[] = []; + private readonly queuedMessages: string[] = []; + private stopped = false; + private started = false; + + constructor( + private readonly config: NormalizedAgentSessionConfig, + private readonly adapter: HarnessAdapter, + private readonly sandbox: RunnerSandbox, + private readonly callbacks: RuntimeCallbacks = {}, + ) { + super(); + this.sessionId = config.sessionId; + this.harness = adapter.kind; + this.events = this.eventBuffer; + } + + async start(): Promise { + if (this.started) { + throw new Error(`Session ${this.sessionId} has already been started`); + } + this.started = true; + + const command = this.adapter.buildCommand(this.config); + const startedAt = Date.now(); + try { + await this.materializeFiles(); + await this.runSetupCommands(); + const result = await this.sandbox.runCommand( + [command.command, ...command.args.map(shellQuote)].join(" "), + { + cwd: this.config.sandbox.workingDirectory, + env: { + ...this.config.env, + ...command.env, + ...this.materializeSecrets(), + }, + }, + ); + + await this.parseOutput(result.stdout, "stdout"); + await this.parseOutput(result.stderr, "stderr"); + + const runtimeResult: AgentSessionResult = { + sessionId: this.sessionId, + harness: this.harness, + success: result.exitCode === 0 && !this.stopped, + exitCode: result.exitCode, + result: this.adapter.extractResult?.(this.observedEvents), + events: [...this.observedEvents], + }; + this.eventBuffer.close(); + return runtimeResult; + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + const failedEvent = this.createEvent("error", { + message: err.message, + durationMs: Date.now() - startedAt, + }); + await this.emitEvent(failedEvent); + this.eventBuffer.close(); + return { + sessionId: this.sessionId, + harness: this.harness, + success: false, + error: err, + events: [...this.observedEvents], + }; + } + } + + async addMessage(message: string): Promise { + this.queuedMessages.push(message); + await this.emitEvent(this.createEvent("message.queued", { message })); + } + + async interrupt(reason?: string): Promise { + await this.emitEvent(this.createEvent("interrupt.requested", { reason })); + } + + async stop(reason?: string): Promise { + this.stopped = true; + await this.emitEvent(this.createEvent("stop.requested", { reason })); + await this.sandbox.destroy(); + this.eventBuffer.close(); + } + + getQueuedMessages(): readonly string[] { + return this.queuedMessages; + } + + private async parseOutput( + output: string, + stream: "stdout" | "stderr", + ): Promise { + for (const line of output.split(/\r?\n/)) { + if (!line.trim()) { + continue; + } + const event = + stream === "stdout" + ? this.adapter.parseStdoutLine(line, { + sessionId: this.sessionId, + harness: this.harness, + }) + : this.adapter.parseStderrLine?.(line, { + sessionId: this.sessionId, + harness: this.harness, + }); + if (event) { + await this.emitEvent(event); + } + } + } + + private createEvent(kind: string, raw: unknown): TranscriptEvent { + return { + sessionId: this.sessionId, + harness: this.harness, + timestamp: new Date().toISOString(), + kind, + raw, + }; + } + + private async emitEvent(event: TranscriptEvent): Promise { + this.observedEvents.push(event); + this.eventBuffer.push(event); + this.emit("transcript", event); + await this.callbacks.onTranscriptEvent?.(event); + } + + private async materializeFiles(): Promise { + for (const file of this.config.files ?? []) { + await this.emitEvent( + this.createEvent("file.write.started", { + path: file.path, + sensitive: file.sensitive ?? false, + }), + ); + await this.sandbox.filesystem.mkdir(dirname(file.path)); + await this.sandbox.filesystem.writeFile(file.path, file.content); + await this.emitEvent( + this.createEvent("file.write.completed", { + path: file.path, + bytes: file.content.length, + content: file.sensitive ? "[redacted]" : file.content, + }), + ); + } + } + + private async runSetupCommands(): Promise { + const commands = [ + ...(this.config.packages?.system?.map( + (pkg) => `apt-get update && apt-get install -y ${shellQuote(pkg)}`, + ) ?? []), + ...(this.config.packages?.npm?.map( + (pkg) => `npm install -g ${shellQuote(pkg)}`, + ) ?? []), + ...(this.config.packages?.commands ?? []), + ]; + + for (const setupCommand of commands) { + await this.emitEvent( + this.createEvent("setup.started", { command: setupCommand }), + ); + const result = await this.sandbox.runCommand(setupCommand, { + cwd: this.config.sandbox.workingDirectory, + env: { + ...this.config.env, + ...this.materializeSecrets(), + }, + }); + await this.emitEvent( + this.createEvent("setup.completed", { + command: setupCommand, + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr, + }), + ); + if (result.exitCode !== 0) { + throw new Error( + `Setup command failed with exit code ${result.exitCode}: ${setupCommand}`, + ); + } + } + } + + private materializeSecrets(): Record { + const entries = Object.entries(this.config.secrets).map(([key, secret]) => [ + key, + secret.value, + ]); + return Object.fromEntries(entries); + } +} + +function shellQuote(value: string): string { + if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) { + return value; + } + return `'${value.replaceAll("'", "'\\''")}'`; +} diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts new file mode 100644 index 000000000..7ff88a0bd --- /dev/null +++ b/packages/agent-runtime/src/types.ts @@ -0,0 +1,251 @@ +export type HarnessKind = + | "claude" + | "codex" + | "cursor" + | "gemini" + | "pi" + | "opencode"; + +export type PermissionMode = "default" | "plan" | "ask" | "auto" | "bypass"; + +export type NetworkEgressMode = + | "default" + | "disabled" + | "proxied" + | "unrestricted"; + +export interface RuntimeSecret { + value: string; + redact?: boolean; +} + +export interface McpServerRuntimeConfig { + command?: string; + args?: string[]; + env?: Record; + url?: string; + httpUrl?: string; + headers?: Record; +} + +export interface RuntimeMemoryConfig { + enabled?: boolean; + directory?: string; + namespace?: string; +} + +export interface RuntimePackageConfig { + system?: string[]; + npm?: string[]; + commands?: string[]; +} + +export interface RuntimeFileConfig { + path: string; + content: string; + sensitive?: boolean; +} + +export interface RuntimeVolumeConfig { + name: string; + mountPath: string; + source?: string; + kind?: "bind" | "fuse" | "provider"; + readOnly?: boolean; +} + +export interface RuntimeNetworkEgressConfig { + mode: NetworkEgressMode; + proxyUrl?: string; + allowedHosts?: string[]; + deniedHosts?: string[]; +} + +export interface RuntimeSandboxConfig { + provider: "local" | string; + id?: string; + name?: string; + namespace?: string; + workingDirectory?: string; + templateId?: string; + timeoutMs?: number; + metadata?: Record; + volumes?: RuntimeVolumeConfig[]; + networkEgress?: RuntimeNetworkEgressConfig; +} + +export interface RuntimeHarnessConfig { + kind: HarnessKind; + model?: string; + command?: string; + args?: string[]; +} + +export interface RuntimePermissionConfig { + mode?: PermissionMode; + allowedTools?: string[]; + disallowedTools?: string[]; +} + +export interface CreateAgentSessionConfig { + sessionId?: string; + harness: HarnessKind | RuntimeHarnessConfig; + model?: string; + systemPrompt?: string; + userPrompt: string; + env?: Record; + secrets?: Record; + packages?: RuntimePackageConfig; + files?: RuntimeFileConfig[]; + mcps?: Record; + permissions?: RuntimePermissionConfig; + memory?: RuntimeMemoryConfig; + sandbox?: RuntimeSandboxConfig; + networkEgress?: RuntimeNetworkEgressConfig; + metadata?: Record; +} + +export interface TranscriptEvent { + sessionId: string; + harness: HarnessKind; + timestamp: string; + kind: string; + raw: unknown; + normalized?: unknown; + metadata?: Record; +} + +export interface HarnessCommand { + command: string; + args: string[]; + env?: Record; + stdin?: string; +} + +export interface HarnessAdapter { + readonly kind: HarnessKind; + buildCommand(config: NormalizedAgentSessionConfig): HarnessCommand; + parseStdoutLine( + line: string, + context: TranscriptParseContext, + ): TranscriptEvent | undefined; + parseStderrLine?( + line: string, + context: TranscriptParseContext, + ): TranscriptEvent | undefined; + extractResult?(events: TranscriptEvent[]): string | undefined; +} + +export interface TranscriptParseContext { + sessionId: string; + harness: HarnessKind; + now?: () => Date; +} + +export interface CommandExecutionResult { + stdout: string; + stderr: string; + exitCode: number; + durationMs: number; +} + +export interface SandboxFileEntry { + name: string; + type: "file" | "directory"; + size?: number; + modified?: Date; +} + +export interface SandboxFilesystem { + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + readdir(path: string): Promise; + mkdir(path: string): Promise; + exists(path: string): Promise; + remove(path: string): Promise; +} + +export interface SandboxRunCommandOptions { + cwd?: string; + env?: Record; + timeout?: number; + background?: boolean; +} + +export interface RunnerSandboxCapabilities { + filesystem: boolean; + runCommand: boolean; + streamingProcess: boolean; + snapshots?: boolean; + ports?: boolean; + volumes?: boolean; + networkEgress?: boolean; +} + +export interface RunnerSandbox { + readonly sandboxId: string; + readonly provider: string; + readonly workingDirectory?: string; + readonly capabilities: RunnerSandboxCapabilities; + readonly filesystem: SandboxFilesystem; + runCommand( + command: string, + options?: SandboxRunCommandOptions, + ): Promise; + destroy(): Promise; +} + +export interface SandboxProvider { + readonly provider: string; + create(config: RuntimeSandboxConfig): Promise; +} + +export interface PermissionPromptRequest { + sessionId: string; + harness: HarnessKind; + toolName: string; + input: unknown; + reason?: string; +} + +export interface PermissionPromptResponse { + allowed: boolean; + reason?: string; +} + +export interface RuntimeCallbacks { + onPermissionPrompt?: ( + request: PermissionPromptRequest, + ) => Promise | PermissionPromptResponse; + onTranscriptEvent?: (event: TranscriptEvent) => Promise | void; +} + +export interface AgentSessionResult { + sessionId: string; + harness: HarnessKind; + success: boolean; + exitCode?: number; + result?: string; + error?: Error; + events: TranscriptEvent[]; +} + +export interface NormalizedAgentSessionConfig + extends Omit { + sessionId: string; + harness: RuntimeHarnessConfig; + model?: string; + env: Record; + secrets: Record; + sandbox: RuntimeSandboxConfig; +} + +export interface AgentSession { + readonly sessionId: string; + readonly harness: HarnessKind; + readonly events: AsyncIterable; + start(): Promise; + addMessage(message: string): Promise; + interrupt(reason?: string): Promise; + stop(reason?: string): Promise; +} diff --git a/packages/agent-runtime/test/harnesses.test.ts b/packages/agent-runtime/test/harnesses.test.ts new file mode 100644 index 000000000..da9bea71d --- /dev/null +++ b/packages/agent-runtime/test/harnesses.test.ts @@ -0,0 +1,206 @@ +import { describe, expect, it } from "vitest"; +import { + buildHarnessInvocation, + getHarnessAdapter, + harnessAdapters, +} from "../src/harnesses/index.js"; +import type { NormalizedAgentSessionConfig } from "../src/types.js"; + +const baseConfig: NormalizedAgentSessionConfig = { + sessionId: "session-1", + harness: { kind: "claude" }, + userPrompt: "Fix the failing test", + env: {}, + secrets: {}, + sandbox: { + provider: "local", + workingDirectory: "/tmp/worktree", + }, +}; + +describe("harness adapters", () => { + it("registers every supported harness kind", () => { + expect(Object.keys(harnessAdapters).sort()).toEqual([ + "claude", + "codex", + "cursor", + "gemini", + "opencode", + "pi", + ]); + }); + + it("builds a Claude stream-json command", () => { + const command = buildHarnessInvocation({ + ...baseConfig, + model: "claude-sonnet-4-5", + systemPrompt: "Be concise", + permissions: { + mode: "ask", + allowedTools: ["Read(**)", "Edit(**)"], + disallowedTools: ["Bash"], + }, + }); + + expect(command.command).toBe("claude"); + expect(command.args).toEqual([ + "-p", + "Fix the failing test", + "--output-format", + "stream-json", + "--verbose", + "--model", + "claude-sonnet-4-5", + "--append-system-prompt", + "Be concise", + "--permission-mode", + "ask", + "--allowedTools", + "Read(**),Edit(**)", + "--disallowedTools", + "Bash", + ]); + }); + + it("builds a Codex JSON command", () => { + const command = buildHarnessInvocation({ + ...baseConfig, + harness: { kind: "codex" }, + model: "gpt-5.3-codex", + systemPrompt: "Use the repo style", + userPrompt: "Implement the feature", + permissions: { mode: "auto" }, + }); + + expect(command.command).toBe("codex"); + expect(command.args).toEqual([ + "exec", + "--json", + "--skip-git-repo-check", + "--model", + "gpt-5.3-codex", + "-c", + 'developer_instructions="Use the repo style"', + "-c", + 'approval_policy="auto"', + "Implement the feature", + ]); + }); + + it("builds a Cursor command matching headless print mode", () => { + const command = buildHarnessInvocation({ + ...baseConfig, + harness: { kind: "cursor" }, + model: "composer-2", + permissions: { mode: "ask" }, + userPrompt: "Patch the bug", + }); + + expect(command.command).toBe("cursor-agent"); + expect(command.args).toEqual([ + "--print", + "--output-format", + "stream-json", + "--trust", + "--model", + "composer-2", + "--mode", + "ask", + "Patch the bug", + ]); + }); + + it("builds a Gemini command with env-backed system prompt", () => { + const command = buildHarnessInvocation({ + ...baseConfig, + harness: { kind: "gemini" }, + systemPrompt: "System text", + userPrompt: "Analyze this", + permissions: { mode: "bypass" }, + }); + + expect(command.command).toBe("gemini"); + expect(command.args).toEqual([ + "--output-format", + "stream-json", + "--model", + "gemini-2.5-pro", + "--yolo", + "--approval-mode", + "bypass", + "-p", + "Analyze this", + ]); + expect(command.env?.GEMINI_SYSTEM_MD).toBe("System text"); + }); + + it("supports harness command and arg overrides", () => { + const command = buildHarnessInvocation({ + ...baseConfig, + harness: { + kind: "codex", + command: "/opt/bin/codex-dev", + args: ["--config", "profile=dev"], + }, + userPrompt: "Run it", + }); + + expect(command.command).toBe("/opt/bin/codex-dev"); + expect(command.args.slice(0, 2)).toEqual(["--config", "profile=dev"]); + expect(command.args.slice(2)).toEqual([ + "exec", + "--json", + "--skip-git-repo-check", + "Run it", + ]); + }); + + it("parses JSON stdout transcript lines", () => { + const adapter = getHarnessAdapter("gemini"); + const event = adapter.parseStdoutLine( + JSON.stringify({ + type: "tool_use", + tool_name: "read_file", + parameters: { path: "src/index.ts" }, + }), + { + sessionId: "session-1", + harness: "gemini", + now: () => new Date("2026-05-14T12:00:00.000Z"), + }, + ); + + expect(event).toMatchObject({ + sessionId: "session-1", + harness: "gemini", + timestamp: "2026-05-14T12:00:00.000Z", + kind: "tool_use", + normalized: { + type: "tool_use", + toolName: "read_file", + }, + }); + }); + + it("parses non-JSON stdout as text events and ignores blank lines", () => { + const adapter = getHarnessAdapter("claude"); + + expect( + adapter.parseStdoutLine(" ", { + sessionId: "session-1", + harness: "claude", + }), + ).toBeUndefined(); + expect( + adapter.parseStdoutLine("plain output", { + sessionId: "session-1", + harness: "claude", + }), + ).toMatchObject({ + sessionId: "session-1", + harness: "claude", + kind: "text", + raw: "plain output", + }); + }); +}); diff --git a/packages/agent-runtime/test/runtime.test.ts b/packages/agent-runtime/test/runtime.test.ts new file mode 100644 index 000000000..304551c2c --- /dev/null +++ b/packages/agent-runtime/test/runtime.test.ts @@ -0,0 +1,228 @@ +import { describe, expect, it } from "vitest"; +import { createAgentSession, normalizeConfig } from "../src/runtime.js"; +import type { + CommandExecutionResult, + RunnerSandbox, + RunnerSandboxCapabilities, + SandboxFilesystem, + SandboxProvider, +} from "../src/types.js"; + +describe("AgentRuntime", () => { + it("normalizes minimal session config", () => { + const config = normalizeConfig({ + harness: "codex", + userPrompt: "hello", + secrets: { + CURSOR_API_KEY: "secret", + }, + }); + + expect(config.sessionId).toBeTruthy(); + expect(config.harness).toEqual({ kind: "codex", model: undefined }); + expect(config.sandbox.provider).toBe("local"); + expect(config.secrets.CURSOR_API_KEY).toEqual({ + value: "secret", + redact: true, + }); + }); + + it("runs a session through an injected sandbox provider", async () => { + const sandbox = new FakeSandbox( + [ + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "done" }, + }), + ].join("\n"), + ); + const events = []; + const session = await createAgentSession( + { + sessionId: "session-1", + harness: "codex", + userPrompt: "Do it", + env: { NODE_ENV: "test" }, + secrets: { API_KEY: "secret" }, + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + callbacks: { + onTranscriptEvent(event) { + events.push(event.kind); + }, + }, + }, + ); + + await session.addMessage("queued"); + const result = await session.start(); + + expect(result).toMatchObject({ + sessionId: "session-1", + harness: "codex", + success: true, + result: "done", + }); + expect(events).toEqual(["message.queued", "item.completed"]); + expect(sandbox.commands[0]).toMatchObject({ + command: "codex exec --json --skip-git-repo-check 'Do it'", + options: { + env: { + NODE_ENV: "test", + API_KEY: "secret", + }, + }, + }); + }); + + it("runs setup commands before the harness command and emits setup events", async () => { + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "ready" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-setup", + harness: "codex", + userPrompt: "Run after setup", + packages: { + npm: ["example-cli"], + commands: ["example-cli --version"], + }, + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + }, + ); + + const result = await session.start(); + + expect(result.success).toBe(true); + expect(result.events.map((event) => event.kind)).toEqual([ + "setup.started", + "setup.completed", + "setup.started", + "setup.completed", + "item.completed", + ]); + expect(sandbox.commands.map((entry) => entry.command)).toEqual([ + "npm install -g example-cli", + "example-cli --version", + "codex exec --json --skip-git-repo-check 'Run after setup'", + ]); + }); + + it("materializes sensitive files before setup without exposing contents", async () => { + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "ready" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-files", + harness: "codex", + userPrompt: "Run after files", + files: [ + { + path: "/home/daytona/.codex/auth.json", + content: "secret-auth-json", + sensitive: true, + }, + ], + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + }, + ); + + const result = await session.start(); + + expect(result.success).toBe(true); + expect(sandbox.files).toEqual([ + { path: "/home/daytona/.codex/auth.json", content: "secret-auth-json" }, + ]); + expect(result.events.slice(0, 2)).toMatchObject([ + { + kind: "file.write.started", + raw: { path: "/home/daytona/.codex/auth.json", sensitive: true }, + }, + { + kind: "file.write.completed", + raw: { + path: "/home/daytona/.codex/auth.json", + bytes: 16, + content: "[redacted]", + }, + }, + ]); + }); +}); + +class FakeSandboxProvider implements SandboxProvider { + readonly provider = "local"; + + constructor(private readonly sandbox: RunnerSandbox) {} + + async create(): Promise { + return this.sandbox; + } +} + +class FakeSandbox implements RunnerSandbox { + readonly sandboxId = "fake"; + readonly provider = "local"; + readonly capabilities: RunnerSandboxCapabilities = { + filesystem: true, + runCommand: true, + streamingProcess: false, + }; + readonly files: Array<{ path: string; content: string }> = []; + readonly filesystem: SandboxFilesystem = { + async readFile() { + return ""; + }, + writeFile: async (path, content) => { + this.files.push({ path, content }); + }, + async readdir() { + return []; + }, + async mkdir() { + return; + }, + async exists() { + return true; + }, + async remove() { + return; + }, + }; + readonly commands: Array<{ + command: string; + options: unknown; + }> = []; + + constructor(private readonly stdout: string) {} + + async runCommand( + command: string, + options?: unknown, + ): Promise { + this.commands.push({ command, options }); + return { + stdout: this.stdout, + stderr: "", + exitCode: 0, + durationMs: 1, + }; + } + + async destroy(): Promise { + return; + } +} diff --git a/packages/agent-runtime/test/sandbox.test.ts b/packages/agent-runtime/test/sandbox.test.ts new file mode 100644 index 000000000..d12726da3 --- /dev/null +++ b/packages/agent-runtime/test/sandbox.test.ts @@ -0,0 +1,166 @@ +import { mkdtemp, realpath, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + type ComputeSdkSandboxLike, + createComputeSdkSandboxProvider, + createLocalSandboxProvider, +} from "../src/sandbox/index.js"; + +describe("LocalSandboxProvider", () => { + it("creates a local sandbox with filesystem and command execution", async () => { + const root = await mkdtemp(join(tmpdir(), "agent-runtime-local-")); + try { + const provider = createLocalSandboxProvider({ workingDirectory: root }); + const sandbox = await provider.create({ provider: "local" }); + + expect(sandbox.provider).toBe("local"); + expect(sandbox.capabilities.filesystem).toBe(true); + expect(sandbox.capabilities.runCommand).toBe(true); + expect(sandbox.capabilities.streamingProcess).toBe(false); + + await sandbox.filesystem.mkdir("nested"); + await sandbox.filesystem.writeFile("nested/hello.txt", "hello"); + + await expect( + sandbox.filesystem.readFile("nested/hello.txt"), + ).resolves.toBe("hello"); + await expect(sandbox.filesystem.exists("nested/hello.txt")).resolves.toBe( + true, + ); + await expect(sandbox.filesystem.readdir("nested")).resolves.toMatchObject( + [{ name: "hello.txt", type: "file", size: 5 }], + ); + + const result = await sandbox.runCommand( + "node -e \"console.log(process.cwd()); console.error('err')\"", + ); + + expect(result.exitCode).toBe(0); + expect(await realpath(result.stdout.trim())).toBe(await realpath(root)); + expect(result.stderr.trim()).toBe("err"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); + +describe("ComputeSdkSandboxProvider", () => { + it("wraps an injected compute object and forwards filesystem and command calls", async () => { + const calls: unknown[] = []; + const fakeSandbox: ComputeSdkSandboxLike = { + sandboxId: "sbx_123", + provider: "daytona", + workingDirectory: "/remote/workspace", + filesystem: { + async readFile(path) { + calls.push(["readFile", path]); + return "remote contents"; + }, + async writeFile(path, contents) { + calls.push(["writeFile", path, contents]); + }, + async mkdir(path) { + calls.push(["mkdir", path]); + }, + async readdir(path) { + calls.push(["readdir", path]); + return [{ name: "remote.txt", type: "file" }]; + }, + async exists(path) { + calls.push(["exists", path]); + return true; + }, + async remove(path) { + calls.push(["remove", path]); + }, + }, + async runCommand(command, options) { + calls.push(["runCommand", command, options]); + return { exitCode: 7, stdout: "out", stderr: "err", durationMs: 5 }; + }, + async destroy() { + calls.push(["destroy"]); + }, + }; + const compute = { + sandbox: { + async create(options: Record) { + calls.push(["create", options]); + return fakeSandbox; + }, + }, + }; + + const provider = createComputeSdkSandboxProvider({ compute }); + const sandbox = await provider.create({ + provider: "daytona", + id: "requested-id", + name: "agent-runtime-test", + workingDirectory: "/requested/workspace", + templateId: "template-1", + timeoutMs: 10_000, + metadata: { issue: "CYR-1" }, + }); + + expect(sandbox.sandboxId).toBe("sbx_123"); + expect(sandbox.provider).toBe("daytona"); + expect(sandbox.workingDirectory).toBe("/remote/workspace"); + expect(sandbox.capabilities.streamingProcess).toBe(false); + + await sandbox.filesystem.mkdir("/tmp/project"); + await sandbox.filesystem.writeFile("/tmp/project/remote.txt", "contents"); + await expect( + sandbox.filesystem.readFile("/tmp/project/remote.txt"), + ).resolves.toBe("remote contents"); + await expect(sandbox.filesystem.readdir("/tmp/project")).resolves.toEqual([ + { name: "remote.txt", type: "file" }, + ]); + await expect( + sandbox.filesystem.exists("/tmp/project/remote.txt"), + ).resolves.toBe(true); + await sandbox.filesystem.remove("/tmp/project"); + + await expect( + sandbox.runCommand("node --version", { + cwd: "/tmp/project", + env: { A: "1" }, + }), + ).resolves.toMatchObject({ + exitCode: 7, + stdout: "out", + stderr: "err", + durationMs: 5, + }); + await sandbox.destroy(); + + expect(calls).toEqual([ + [ + "create", + { + timeout: 10_000, + templateId: "template-1", + metadata: { issue: "CYR-1" }, + namespace: undefined, + name: "agent-runtime-test", + directory: "/requested/workspace", + volumes: undefined, + networkEgress: undefined, + }, + ], + ["mkdir", "/tmp/project"], + ["writeFile", "/tmp/project/remote.txt", "contents"], + ["readFile", "/tmp/project/remote.txt"], + ["readdir", "/tmp/project"], + ["exists", "/tmp/project/remote.txt"], + ["remove", "/tmp/project"], + [ + "runCommand", + "node --version", + { cwd: "/tmp/project", env: { A: "1" } }, + ], + ["destroy"], + ]); + }); +}); diff --git a/packages/agent-runtime/tsconfig.json b/packages/agent-runtime/tsconfig.json new file mode 100644 index 000000000..73b4fb869 --- /dev/null +++ b/packages/agent-runtime/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "module": "NodeNext", + "moduleResolution": "NodeNext" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/codex-runner/src/CodexRunner.ts b/packages/codex-runner/src/CodexRunner.ts index 4f949574d..fc9f236bb 100644 --- a/packages/codex-runner/src/CodexRunner.ts +++ b/packages/codex-runner/src/CodexRunner.ts @@ -93,6 +93,7 @@ function createAssistantToolUseMessage( content: contentBlocks, model: DEFAULT_CODEX_MODEL, stop_reason: null, + stop_details: null, stop_sequence: null, usage: { input_tokens: 0, @@ -146,6 +147,7 @@ function createAssistantBetaMessage( content: contentBlocks, model: DEFAULT_CODEX_MODEL, stop_reason: null, + stop_details: null, stop_sequence: null, usage: { input_tokens: 0, diff --git a/packages/cursor-runner/src/CursorRunner.ts b/packages/cursor-runner/src/CursorRunner.ts index c6f5db5c3..a7f7d72bf 100644 --- a/packages/cursor-runner/src/CursorRunner.ts +++ b/packages/cursor-runner/src/CursorRunner.ts @@ -110,6 +110,7 @@ function createAssistantToolUseMessage( content: contentBlocks, model: "cursor-agent", stop_reason: null, + stop_details: null, stop_sequence: null, usage: { input_tokens: 0, @@ -138,6 +139,7 @@ function createAssistantTextMessage( content: contentBlocks, model: "cursor-agent", stop_reason: null, + stop_details: null, stop_sequence: null, usage: { input_tokens: 0, diff --git a/packages/edge-worker/package.json b/packages/edge-worker/package.json index d21669df9..6489b6c4b 100644 --- a/packages/edge-worker/package.json +++ b/packages/edge-worker/package.json @@ -27,6 +27,7 @@ "@linear/sdk": "^64.0.0", "@ngrok/ngrok": "^1.5.1", "chokidar": "^4.0.3", + "computesdk": "^4.0.0", "cyrus-claude-runner": "workspace:*", "cyrus-cloudflare-tunnel-client": "workspace:*", "cyrus-codex-runner": "workspace:*", diff --git a/packages/gemini-runner/src/adapters.ts b/packages/gemini-runner/src/adapters.ts index 7c46f29ac..48f84003b 100644 --- a/packages/gemini-runner/src/adapters.ts +++ b/packages/gemini-runner/src/adapters.ts @@ -37,6 +37,7 @@ function createBetaMessage( content: contentBlocks, model: "gemini-3" as const, stop_reason: null, + stop_details: null, stop_sequence: null, usage: { input_tokens: 0, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10979dcf2..9390e7095 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,7 +11,7 @@ overrides: qs: '>=6.14.2' vite: '>=7.1.11' zod: 4.3.6 - hono: '>=4.12.7' + hono: '>=4.12.18' '@hono/node-server': '>=1.19.10' rollup: '>=4.59.0' flatted: '>=3.4.0' @@ -26,6 +26,13 @@ overrides: '@tootallnate/once': '>=3.0.1' '@isaacs/brace-expansion': '>=5.0.1' tar: '>=7.5.11' + fast-uri: '>=3.1.2' + ip-address: '>=10.1.1' + '@opentelemetry/sdk-node': '>=0.217.0' + '@opentelemetry/exporter-prometheus': '>=0.217.0' + '@opentelemetry/otlp-transformer>protobufjs': '>=8.0.2' + '@anthropic-ai/sdk': '>=0.91.1' + '@daytonaio/sdk': '>=0.175.0' importers: @@ -146,13 +153,35 @@ importers: specifier: ^3.1.4 version: 3.2.4(@types/node@20.19.39)(@vitest/ui@3.2.4)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.3) + packages/agent-runtime: + dependencies: + '@computesdk/daytona': + specifier: ^1.7.26 + version: 1.7.26(ws@8.20.0) + computesdk: + specifier: ^4.0.0 + version: 4.0.0 + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.39 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vitest: + specifier: ^3.1.4 + version: 3.2.4(@types/node@20.19.39)(@vitest/ui@3.2.4)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.8.3) + packages/claude-runner: dependencies: '@anthropic-ai/claude-agent-sdk': specifier: 0.2.123 version: 0.2.123(zod@4.3.6) '@anthropic-ai/sdk': - specifier: ^0.91.0 + specifier: '>=0.91.1' version: 0.91.1(zod@4.3.6) '@linear/sdk': specifier: ^64.0.0 @@ -313,6 +342,9 @@ importers: chokidar: specifier: ^4.0.3 version: 4.0.3 + computesdk: + specifier: ^4.0.0 + version: 4.0.0 cyrus-claude-runner: specifier: workspace:* version: link:../claude-runner @@ -419,7 +451,7 @@ importers: devDependencies: '@google/gemini-cli-core': specifier: 0.17.0 - version: 0.17.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + version: 0.17.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -621,8 +653,8 @@ packages: peerDependencies: zod: 4.3.6 - '@anthropic-ai/sdk@0.81.0': - resolution: {integrity: sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw==} + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} hasBin: true peerDependencies: zod: 4.3.6 @@ -630,15 +662,167 @@ packages: zod: optional: true - '@anthropic-ai/sdk@0.91.1': - resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} - hasBin: true + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.1047.0': + resolution: {integrity: sha512-gk8g31eqvgf7eLCpkVjWs9KL7gYgkomt3FT2o9tbIe6goYrBheN2lHxhCsTn1zFYbt7EwrZXTGkQPIQNIN0c5w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.10': + resolution: {integrity: sha512-ZGFFlYynBR78Y/F8b/7y4i4sgW/iGwJSjoM7AZo5Et6vyr4/L0bunN+uzKMsvecCZyqcPp4RRK7Rs17l0kMujg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.8': + resolution: {integrity: sha512-fVfUCL/Xh2zINYMPZvj+iBn6XWouQf0DAnjaWCI9MkmqXzL2Iy5FoQB8O7syFe6gN6AH1ecDDU58T51Ou0kFkA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.36': + resolution: {integrity: sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.38': + resolution: {integrity: sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.40': + resolution: {integrity: sha512-0NFGS9I3PD2yMveQqqpwpRdyZVStzgk0Yr2rZHh80kV/QNqQCK5lSrksvU3nBcRNSUF5Uk8rL3Xk0EVR+UVAnA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.40': + resolution: {integrity: sha512-IEIl+UQnrEjZP53TSl91e8LBephi4i1Mt9WZrMgN8pOg6xPOLZdkN1GhsEzjkMD1TQy4Fp2dwWA/9ToTQFOlLA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.41': + resolution: {integrity: sha512-h6BlclpsPGkx7Pv7ukr8oKVqN3jvxnH5n9ZIUQa8focr1ZkKd2MYiPJ2Nv9GI97dohJVJBfZAsTp/qoZL5R1pw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.36': + resolution: {integrity: sha512-eDQ6X7clTAOxXegOx4rGT1hyfusGEYdJGCGo0Ym2+CKeMQBjk+SJSxSVev11NJew5xJHJ/c3hryl2awKaxuSEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.40': + resolution: {integrity: sha512-jaABbsoOkGlKg5kaHetYmUV6mWM57H89ia0Yksom1XxC847mfjmEVb4p7VijS1sjPbXjUii4cftJuwsl4MXkRg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.40': + resolution: {integrity: sha512-bfIrM8IIzbRtXRQWx/vNEUBLTImLZyX5uKk8uSdeSAZ4Mj3Yi4UnRJLK4FkQLWErbM3McpVLQ1DaM6XO66Ed5g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/lib-storage@3.1047.0': + resolution: {integrity: sha512-PMgo648wwGkpj6i7I8cHygNv8oCOrxwzjwfHNZNAl/XJppXlvaNUWF9zBi9jj8U63IyiC7yDzuA8nQ7GnPJnEQ==} + engines: {node: '>=20.0.0'} peerDependencies: - zod: 4.3.6 + '@aws-sdk/client-s3': ^3.1047.0 + + '@aws-sdk/middleware-bucket-endpoint@3.972.12': + resolution: {integrity: sha512-MAG0Adg7FFEwuoeLbb5SBnXDW7S2EpNTwHnQ4h3pJqSKVQOhOmugyA1MfMh6AD4SAfx0lko4htZdwkNoLqFj5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.11': + resolution: {integrity: sha512-xpobcctR1AHSrvkiArgTyLffn78Lt9unPMpa/yic9RKn+bOf/5M55UIM6RaPL5xKzI06/GSsTDywTWvzEAbyyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.18': + resolution: {integrity: sha512-2noO+4ARfC+8vOIyvJvQE6bioVaTRkUcPvUoM/jgwXcweZnZovSZ6OCs/cs+NU2p7yvuwuJT/7LkTzBSj5pU4A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.11': + resolution: {integrity: sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.10': + resolution: {integrity: sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.12': + resolution: {integrity: sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.39': + resolution: {integrity: sha512-cimoQxecHHNad+lv2g7QJ24Cxqh1P0EULJSxyX4YD95BUIGeGRPumbdEXpHPxNkJRU99DVmh7u16Y+uhFu31Yw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.10': + resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.40': + resolution: {integrity: sha512-QLpD+HNQtL1Mc49/GRa6RmZvi/TEYBWPevC9F3L+j96IoG3xOSRctdQfbkX0lETb3TX9QQXU1oGYDmAB+YJprA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.8': + resolution: {integrity: sha512-/Vw2M27w+0APfMDzDpvv8auA4WiJ4D22+lC61pMS2M8Wk+4IydeRqh5utbrh+A5gQRxgUYd/xz3tdv8nQlmiHg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.14': + resolution: {integrity: sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.26': + resolution: {integrity: sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1047.0': + resolution: {integrity: sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.9': + resolution: {integrity: sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.11': + resolution: {integrity: sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==} + + '@aws-sdk/util-user-agent-node@3.973.26': + resolution: {integrity: sha512-9bHR/EERjhrUGyo1qW620ogbGBtCglYB/pEtcm85sVd4/Ah+bwdLI3g1aJf75oNwNwh7+fw+8wOk/OCWHjzVmA==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' peerDependenciesMeta: - zod: + aws-crt: optional: true + '@aws-sdk/xml-builder@3.972.24': + resolution: {integrity: sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -731,6 +915,15 @@ packages: '@bufbuild/protobuf@1.10.0': resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} + '@computesdk/cmd@0.4.1': + resolution: {integrity: sha512-hhcYrwMnOpRSwWma3gkUeAVsDFG56nURwSaQx8vCepv0IuUv39bK4mMkgszolnUQrVjBDdW7b3lV+l5B2S8fRA==} + + '@computesdk/daytona@1.7.26': + resolution: {integrity: sha512-wVv+ugFQAOeS0MU9K3lS5wkuoax0qwnnfXM8ZU+hF6ouPwpDvsI9fkRDY3qu5mXguwqvOh5UprNC2THRg5gegw==} + + '@computesdk/provider@2.0.0': + resolution: {integrity: sha512-ch7JQVa5k3z1cVyuSkR1IfX8EhOgEg+xhGqPUvLUGo4OsWK3Lg1gL15yVO2iy+/KgMqBjOSMfTei1mX5r0iHCw==} + '@connectrpc/connect-node@1.7.0': resolution: {integrity: sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==} engines: {node: '>=16.0.0'} @@ -772,6 +965,15 @@ packages: resolution: {integrity: sha512-DkTwOAuao9wIeUioaM0aQi6hkWLC8oLAnqlR4HR9hn5xytd9A4cEB2fZpSHd8pJ2YRN0VJVkxnggxLRNT7ghuQ==} engines: {node: '>=18'} + '@daytona/api-client@0.175.0': + resolution: {integrity: sha512-XWx2kqcYZrs8hXqo7jJU1JSKT1NNyqLQL6Nh+hbSvEoizo070WnSTHMu2cSXIRrv1l82tgXVeylIGl6v1fW9hQ==} + + '@daytona/toolbox-api-client@0.175.0': + resolution: {integrity: sha512-wiSn1nTgROFkq4qtcYsDWuyZdtEBzb9mb3LkQmNm6ZB8crB9nIBwNxolziYPgS6DgGlXtsv4qEnWqUlDUva/Ew==} + + '@daytonaio/sdk@0.175.0': + resolution: {integrity: sha512-2MR0sUAbyH7Z7Kay12XgVby5z5Vm/AECn/ELXNlFy1AKESE3VvglGuMq+ez1Ld+7r3Eylo4aiSt5msxqqQbQaw==} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -1043,7 +1245,7 @@ packages: resolution: {integrity: sha512-jI9yMDyFpqBeSighf/zlXnQG/nl9AyBc6aAgy4XtxJMyt/CNyJpvPfzDD+bCc2zAOmhhqtF6TnmIaY+xV4mIrw==} engines: {node: '>=20'} peerDependencies: - hono: '>=4.12.7' + hono: '>=4.12.18' '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} @@ -1237,6 +1439,21 @@ packages: resolution: {integrity: sha512-P06o9TpxrJbiRbHQkiwy/rUrlXRupc+Z8KT4MiJfmcdWxvIdzjCaJOdnNkcOTs6DMyzIOefG5tvk/HLdtjqr0g==} engines: {node: '>= 10'} + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@npmcli/fs@1.1.1': resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} @@ -1294,6 +1511,10 @@ packages: resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.217.0': + resolution: {integrity: sha512-Cdq0jW2lknrNfrAm92MyEAvpe2cRsKjdnQLHUL6xRA4IVUnsWx6P65E7NcUO0Y+L4w1Aee5iV8FvjSwd+lrs9A==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.57.2': resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} engines: {node: '>=14'} @@ -1302,14 +1523,20 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/configuration@0.217.0': + resolution: {integrity: sha512-xCtrYOhBqdy6ZOMfe0Oa73ZKF+2LMhoOv4L5vmwAHVvOXUg+V3fvKuEIr9ZyD0Ow+vxllEjWO6PV1wd0DOtyvw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks@1.30.1': resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/context-async-hooks@2.0.1': - resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + '@opentelemetry/context-async-hooks@2.7.1': + resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1338,14 +1565,26 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0': + resolution: {integrity: sha512-vC5S0Dc+noxD86CVtNu1+awCHPA5Kewi1Sg23ps+9lh4YifwsKXh3pe4XTNEKtUJiAcjpJ5dqStGakLbrSE+YQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-http@0.203.0': resolution: {integrity: sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.203.0': - resolution: {integrity: sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==} + '@opentelemetry/exporter-logs-otlp-http@0.217.0': + resolution: {integrity: sha512-KfLAdt1uilVE+3FxbgVnp2ZrzqbIawzcesnRoi+Kh9ckB5Ld5D8btUgoBvwTbdmuNx1j6b132Wsh72azq+pPNQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.217.0': + resolution: {integrity: sha512-Se0GG/ZO24mQTlQj7zprR4pNI0nKe4lPDPBsuJmi6508b9TlZEuUd3EfyuHk6oJxzL7fGyDFYAbxNigQvRP2ZQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1356,20 +1595,32 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0': + resolution: {integrity: sha512-0GpJKnCoVaVA1rKBMVPHziznfOQlXgH72S9ktjBAF1AnAVPzX7vVEBGrhwiSxxHDAiefXk+J8znApsMb/K6Z3w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-http@0.203.0': resolution: {integrity: sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.203.0': - resolution: {integrity: sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==} + '@opentelemetry/exporter-metrics-otlp-http@0.217.0': + resolution: {integrity: sha512-1zkMzzhiNJdVmLxuwkltqWGw4fOOam47bqRxmuQNjyKJe/9NmY5cIrZ4kiQV7sVGxoOgT0ZvGUfLcjvtpC/b9Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0': + resolution: {integrity: sha512-nfxt/KxVGFkjkO/M+58y1ugHu/dwPtxG4eYq0KApcQ7xk5CHzhdn+IuLZfDSvNDrJ3Uy5q++Fj/wbK7i8yryfQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.203.0': - resolution: {integrity: sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==} + '@opentelemetry/exporter-prometheus@0.217.0': + resolution: {integrity: sha512-U9MCXxJu0sBCh5aEkylYRR4xVIL8D1CW6dGwvYXbfFr0qveSorfD0XJchCAWoW6QfAAIcY/yxjf4Dj8OgkHBPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1380,20 +1631,32 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0': + resolution: {integrity: sha512-fPZs2fw7veLH3pEKu8vSepUa2fQpAE2P7al6qU10aH9GrEJJ8YaPgsd5xON7by5rbcEVS71FOU2aWyK6nzB7VQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.203.0': resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.203.0': - resolution: {integrity: sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==} + '@opentelemetry/exporter-trace-otlp-http@0.217.0': + resolution: {integrity: sha512-38YQoqtYjglz2GV94LGUN/djLvxtvGIQO68o6qAFPVshjmwSdX1F2i0c7vn3lEl1L5B/YqjB/bgKXaVx7KO+RQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.217.0': + resolution: {integrity: sha512-nPV8gKHUiSuTZpQcnZU3/pBlK7crSyEGpZuh5MtWySB0vv6NNG0QvvfKitQt+Fc2Mc6qfyU54KlZcurwoTbrVg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.0.1': - resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + '@opentelemetry/exporter-zipkin@2.7.1': + resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -1452,6 +1715,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-http@0.217.0': + resolution: {integrity: sha512-B88Y7k5A9a60pHUboFoeJlgVwXq2T0rsZKj6dTwzSMKSOsNXR4Jz5ovwprVn3kHLAZrkyLEjQtBJ34DYHs1U4Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-http@0.57.2': resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} engines: {node: '>=14'} @@ -1542,6 +1811,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.217.0': + resolution: {integrity: sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.57.2': resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} engines: {node: '>=14'} @@ -1554,26 +1829,44 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.217.0': + resolution: {integrity: sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.203.0': resolution: {integrity: sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.217.0': + resolution: {integrity: sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.203.0': resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.0.1': - resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + '@opentelemetry/otlp-transformer@0.217.0': + resolution: {integrity: sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.7.1': + resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.0.1': - resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + '@opentelemetry/propagator-jaeger@2.7.1': + resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1612,14 +1905,26 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.217.0': + resolution: {integrity: sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.0.1': resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.203.0': - resolution: {integrity: sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==} + '@opentelemetry/sdk-metrics@2.7.1': + resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.217.0': + resolution: {integrity: sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -1636,8 +1941,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.0.1': - resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.7.1': + resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1852,6 +2163,42 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/core@3.24.2': + resolution: {integrity: sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.2': + resolution: {integrity: sha512-iYr9ekBjmZ+FwkiHEopqGscBbl78X62cq3p5Dd0eC+gNd7fybNZFQQdDuOQjTVmFymleuA8YRWZnuXWZ8B3kKA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.2': + resolution: {integrity: sha512-3wF40g8OOCA5BnwQUvwtzZqYBbWWftDjpAlWIUo6Yld3ZzJaMAKqg7MWQBPjE8oLaqvZQUE7tVGlZPsae6A4bQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.2': + resolution: {integrity: sha512-EdksTZ8UXYxGUgQ4mpIKrHoaj9WVGsp66TpZuixLAz1Jex8YDLnS4RH9ktGED5aOpN0OJlEtrsC9IGt76go1eA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.2': + resolution: {integrity: sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@statsig/client-core@3.31.0': resolution: {integrity: sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==} @@ -2118,6 +2465,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -2132,6 +2482,9 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer@5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -2139,6 +2492,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + byte-counter@0.1.0: resolution: {integrity: sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==} engines: {node: '>=20'} @@ -2204,6 +2561,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -2246,6 +2606,9 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + computesdk@4.0.0: + resolution: {integrity: sha512-R0n3/FDamC+ie2UyJauc4SPfMPLK/MIjgZaoihIKp53hZ8zLyAKsQkcpHw0JDE+RTWPPrrW/SX5tmqf1XyKEWw==} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -2473,6 +2836,10 @@ packages: resolution: {integrity: sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==} engines: {node: '>=10'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -2489,6 +2856,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2517,6 +2888,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stringify@6.3.0: resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} @@ -2526,8 +2901,15 @@ packages: fast-querystring@1.1.2: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true fastify-mcp@2.1.0: resolution: {integrity: sha512-nx1Es7kEqzYe3COWwqdzQbtjgGJVKXFow6pd6rKKsByyXANdJAkEXAXeIJ+ox/vhGD26X7yX6pDDGmaezkBy6g==} @@ -2767,8 +3149,12 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} - hono@4.12.16: - resolution: {integrity: sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==} + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hono@4.12.18: + resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} engines: {node: '>=16.9.0'} hosted-git-info@7.0.2: @@ -2848,6 +3234,10 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2873,10 +3263,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - ip-address@10.1.1: resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==} engines: {node: '>= 12'} @@ -2963,6 +3349,11 @@ packages: isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3174,6 +3565,14 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3419,6 +3818,10 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -3430,6 +3833,10 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3561,6 +3968,10 @@ packages: resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} engines: {node: '>=12.0.0'} + protobufjs@8.3.0: + resolution: {integrity: sha512-JpJpFaR7yKNb6WqKvJJ1MLbiuIQWQnbUUb06nDtf2/i8YWYYLEfP6xf9BwSJoJQg1wAy61EQB8dssQg64oX4aA==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3582,6 +3993,9 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -3637,6 +4051,10 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -3693,6 +4111,9 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3853,12 +4274,19 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + stream-events@1.0.5: resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3901,6 +4329,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -4245,6 +4676,10 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4321,7 +4756,7 @@ snapshots: '@anthropic-ai/claude-agent-sdk@0.2.123(zod@4.3.6)': dependencies: - '@anthropic-ai/sdk': 0.81.0(zod@4.3.6) + '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) zod: 4.3.6 optionalDependencies: @@ -4337,38 +4772,396 @@ snapshots: - '@cfworker/json-schema' - supports-color - '@anthropic-ai/sdk@0.81.0(zod@4.3.6)': + '@anthropic-ai/sdk@0.91.1(zod@4.3.6)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: zod: 4.3.6 - '@anthropic-ai/sdk@0.91.1(zod@4.3.6)': + '@aws-crypto/crc32@5.2.0': dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.3.6 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 - '@babel/code-frame@7.29.0': + '@aws-crypto/crc32c@5.2.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 - '@babel/helper-string-parser@7.27.1': {} + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 - '@babel/helper-validator-identifier@7.28.5': {} + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 - '@babel/parser@7.29.2': + '@aws-crypto/sha256-js@5.2.0': dependencies: - '@babel/types': 7.29.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 - '@babel/runtime@7.29.2': {} + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 - '@babel/types@7.29.0': + '@aws-crypto/util@5.2.0': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@aws-sdk/types': 3.973.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1047.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.10 + '@aws-sdk/credential-provider-node': 3.972.41 + '@aws-sdk/middleware-bucket-endpoint': 3.972.12 + '@aws-sdk/middleware-expect-continue': 3.972.11 + '@aws-sdk/middleware-flexible-checksums': 3.974.18 + '@aws-sdk/middleware-host-header': 3.972.11 + '@aws-sdk/middleware-location-constraint': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.12 + '@aws-sdk/middleware-sdk-s3': 3.972.39 + '@aws-sdk/middleware-ssec': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.40 + '@aws-sdk/region-config-resolver': 3.972.14 + '@aws-sdk/signature-v4-multi-region': 3.996.26 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.9 + '@aws-sdk/util-user-agent-browser': 3.972.11 + '@aws-sdk/util-user-agent-node': 3.973.26 + '@smithy/core': 3.24.2 + '@smithy/fetch-http-handler': 5.4.2 + '@smithy/node-http-handler': 4.7.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.974.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.24 + '@smithy/core': 3.24.2 + '@smithy/signature-v4': 5.4.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/fetch-http-handler': 5.4.2 + '@smithy/node-http-handler': 4.7.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/credential-provider-env': 3.972.36 + '@aws-sdk/credential-provider-http': 3.972.38 + '@aws-sdk/credential-provider-login': 3.972.40 + '@aws-sdk/credential-provider-process': 3.972.36 + '@aws-sdk/credential-provider-sso': 3.972.40 + '@aws-sdk/credential-provider-web-identity': 3.972.40 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/credential-provider-imds': 4.3.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.41': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.36 + '@aws-sdk/credential-provider-http': 3.972.38 + '@aws-sdk/credential-provider-ini': 3.972.40 + '@aws-sdk/credential-provider-process': 3.972.36 + '@aws-sdk/credential-provider-sso': 3.972.40 + '@aws-sdk/credential-provider-web-identity': 3.972.40 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/credential-provider-imds': 4.3.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/token-providers': 3.1047.0 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/lib-storage@3.1047.0(@aws-sdk/client-s3@3.1047.0)': + dependencies: + '@aws-sdk/client-s3': 3.1047.0 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-bucket-endpoint@3.972.12': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.18': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.10 + '@aws-sdk/crc64-nvme': 3.972.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.12': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.39': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/signature-v4-multi-region': 3.996.26 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/signature-v4': 5.4.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.9 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.8': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.10 + '@aws-sdk/middleware-host-header': 3.972.11 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.12 + '@aws-sdk/middleware-user-agent': 3.972.40 + '@aws-sdk/region-config-resolver': 3.972.14 + '@aws-sdk/signature-v4-multi-region': 3.996.26 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.9 + '@aws-sdk/util-user-agent-browser': 3.972.11 + '@aws-sdk/util-user-agent-node': 3.973.26 + '@smithy/core': 3.24.2 + '@smithy/fetch-http-handler': 5.4.2 + '@smithy/node-http-handler': 4.7.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.14': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.26': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/signature-v4': 5.4.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1047.0': + dependencies: + '@aws-sdk/core': 3.974.10 + '@aws-sdk/nested-clients': 3.997.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.9': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.26': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.40 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.24': + dependencies: + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@bcoe/v8-coverage@1.0.2': {} @@ -4411,6 +5204,24 @@ snapshots: '@bufbuild/protobuf@1.10.0': {} + '@computesdk/cmd@0.4.1': {} + + '@computesdk/daytona@1.7.26(ws@8.20.0)': + dependencies: + '@computesdk/provider': 2.0.0 + '@daytonaio/sdk': 0.175.0(ws@8.20.0) + computesdk: 4.0.0 + transitivePeerDependencies: + - aws-crt + - debug + - supports-color + - ws + + '@computesdk/provider@2.0.0': + dependencies: + '@computesdk/cmd': 0.4.1 + computesdk: 4.0.0 + '@connectrpc/connect-node@1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0))': dependencies: '@bufbuild/protobuf': 1.10.0 @@ -4454,6 +5265,49 @@ snapshots: - bluebird - supports-color + '@daytona/api-client@0.175.0': + dependencies: + axios: 1.15.2 + transitivePeerDependencies: + - debug + + '@daytona/toolbox-api-client@0.175.0': + dependencies: + axios: 1.15.2 + transitivePeerDependencies: + - debug + + '@daytonaio/sdk@0.175.0(ws@8.20.0)': + dependencies: + '@aws-sdk/client-s3': 3.1047.0 + '@aws-sdk/lib-storage': 3.1047.0(@aws-sdk/client-s3@3.1047.0) + '@daytona/api-client': 0.175.0 + '@daytona/toolbox-api-client': 0.175.0 + '@iarna/toml': 2.2.5 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + axios: 1.15.2 + busboy: 1.6.0 + dotenv: 17.4.2 + expand-tilde: 2.0.2 + fast-glob: 3.3.3 + form-data: 4.0.5 + isomorphic-ws: 5.0.0(ws@8.20.0) + pathe: 2.0.3 + shell-quote: 1.8.3 + tar: 7.5.13 + transitivePeerDependencies: + - aws-crt + - debug + - supports-color + - ws + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -4552,7 +5406,7 @@ snapshots: dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.0 + fast-uri: 3.1.2 '@fastify/error@4.2.0': {} @@ -4611,21 +5465,21 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/precise-date': 4.0.0 '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) google-auth-library: 9.15.1(encoding@0.1.13) googleapis: 137.1.0(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color - '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@grpc/grpc-js': 1.14.3 @@ -4633,7 +5487,7 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) google-auth-library: 9.15.1(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -4660,11 +5514,11 @@ snapshots: '@google-cloud/promisify@4.0.0': {} - '@google/gemini-cli-core@0.17.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google/gemini-cli-core@0.17.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@google-cloud/logging': 11.2.1(encoding@0.1.13) - '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) - '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google/genai': 1.16.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(encoding@0.1.13) '@iarna/toml': 2.2.5 '@joshua.litt/get-ripgrep': 0.0.3 @@ -4678,7 +5532,7 @@ snapshots: '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation-http': 0.203.0(@opentelemetry/api@1.9.1) '@opentelemetry/resource-detector-gcp': 0.40.3(@opentelemetry/api@1.9.1)(encoding@0.1.13) - '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.217.0(@opentelemetry/api@1.9.1) '@types/glob': 8.1.0 '@types/html-to-text': 9.0.4 '@xterm/headless': 5.5.0 @@ -4688,7 +5542,7 @@ snapshots: diff: 9.0.0 dotenv: 17.4.2 fast-levenshtein: 2.0.6 - fast-uri: 3.1.0 + fast-uri: 3.1.2 fdir: 6.5.0(picomatch@4.0.4) fzf: 0.5.2 glob: 12.0.0 @@ -4766,9 +5620,9 @@ snapshots: protobufjs: 7.5.6 yargs: 17.7.2 - '@hono/node-server@2.0.1(hono@4.12.16)': + '@hono/node-server@2.0.1(hono@4.12.18)': dependencies: - hono: 4.12.16 + hono: 4.12.18 '@iarna/toml@2.2.5': {} @@ -4875,7 +5729,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': dependencies: - '@hono/node-server': 2.0.1(hono@4.12.16) + '@hono/node-server': 2.0.1(hono@4.12.18) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -4885,7 +5739,7 @@ snapshots: eventsource-parser: 3.0.8 express: 5.2.1 express-rate-limit: 8.4.1(express@5.2.1) - hono: 4.12.16 + hono: 4.12.18 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -4957,6 +5811,20 @@ snapshots: '@ngrok/ngrok-win32-ia32-msvc': 1.7.0 '@ngrok/ngrok-win32-x64-msvc': 1.7.0 + '@nodable/entities@2.1.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 @@ -5004,17 +5872,27 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs@0.217.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs@0.57.2': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.1': {} + '@opentelemetry/configuration@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + yaml: 2.8.3 + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5043,6 +5921,16 @@ snapshots: '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http@0.203.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5052,16 +5940,25 @@ snapshots: '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto@0.203.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-http@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-logs-otlp-proto@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.1)': dependencies: @@ -5075,6 +5972,18 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http@0.203.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5084,22 +5993,32 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto@0.203.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-http@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus@0.203.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.1)': dependencies: @@ -5112,6 +6031,17 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5121,21 +6051,30 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-http@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.1)': @@ -5214,6 +6153,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation-http@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5348,6 +6297,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5366,6 +6324,12 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -5374,6 +6338,14 @@ snapshots: '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5383,17 +6355,28 @@ snapshots: '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) - protobufjs: 7.5.6 + protobufjs: 8.3.0 - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-transformer@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + protobufjs: 8.3.0 - '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/redis-common@0.36.2': {} @@ -5432,36 +6415,53 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-node@0.203.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.203.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-node@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/configuration': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-proto': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-proto': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-prometheus': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color @@ -5480,12 +6480,19 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions@1.28.0': {} @@ -5667,6 +6674,54 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@smithy/core@3.24.2': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.2': + dependencies: + '@smithy/core': 3.24.2 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/types@4.14.1': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@statsig/client-core@3.31.0': {} '@statsig/js-client@3.31.0': @@ -5890,7 +6945,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5981,6 +7036,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.14.1: {} + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -5993,6 +7050,11 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + buffer@5.6.0: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -6002,6 +7064,10 @@ snapshots: dependencies: run-applescript: 7.1.0 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + byte-counter@0.1.0: {} bytes@3.1.2: {} @@ -6091,6 +7157,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} + clean-stack@2.2.0: optional: true @@ -6128,6 +7196,10 @@ snapshots: commander@14.0.3: {} + computesdk@4.0.0: + dependencies: + '@computesdk/cmd': 0.4.1 + console-control-strings@1.1.0: optional: true @@ -6336,6 +7408,8 @@ snapshots: dependencies: uuid: 8.3.2 + events@3.3.0: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -6359,12 +7433,16 @@ snapshots: expand-template@2.0.3: {} + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + expect-type@1.3.0: {} express-rate-limit@8.4.1(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.1.0 + ip-address: 10.1.1 express@5.2.1: dependencies: @@ -6415,12 +7493,20 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stringify@6.3.0: dependencies: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -6430,7 +7516,19 @@ snapshots: dependencies: fast-decode-uri-component: 1.0.1 - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} + + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 fastify-mcp@2.1.0(zod@4.3.6): dependencies: @@ -6767,7 +7865,11 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.12.16: {} + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hono@4.12.18: {} hosted-git-info@7.0.2: dependencies: @@ -6869,6 +7971,13 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 + import-in-the-middle@3.0.1: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: optional: true @@ -6890,10 +7999,7 @@ snapshots: ini@1.3.8: {} - ip-address@10.1.0: {} - - ip-address@10.1.1: - optional: true + ip-address@10.1.1: {} ipaddr.js@1.9.1: {} @@ -6955,6 +8061,10 @@ snapshots: transitivePeerDependencies: - encoding + isomorphic-ws@5.0.0(ws@8.20.0): + dependencies: + ws: 8.20.0 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -7177,6 +8287,13 @@ snapshots: merge-descriptors@2.0.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-db@1.54.0: {} @@ -7405,6 +8522,8 @@ snapshots: parse-ms@4.0.0: {} + parse-passwd@1.0.0: {} + parseley@0.12.1: dependencies: leac: 0.6.0 @@ -7414,6 +8533,8 @@ snapshots: path-exists@5.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: optional: true @@ -7550,6 +8671,10 @@ snapshots: '@types/node': 20.19.39 long: 5.3.2 + protobufjs@8.3.0: + dependencies: + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7574,6 +8699,8 @@ snapshots: dependencies: side-channel: 1.1.0 + queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} quick-lru@5.1.1: {} @@ -7634,6 +8761,13 @@ snapshots: transitivePeerDependencies: - supports-color + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + resolve-alpn@1.2.1: {} resolve-pkg-maps@1.0.0: {} @@ -7710,6 +8844,10 @@ snapshots: run-applescript@7.1.0: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} safe-regex2@5.1.1: @@ -7906,12 +9044,19 @@ snapshots: std-env@3.10.0: {} + stream-browserify@3.0.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-events@1.0.5: dependencies: stubs: 3.0.0 stream-shift@1.0.3: {} + streamsearch@1.1.0: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -7957,6 +9102,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + strnum@2.3.0: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -8065,8 +9212,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsx@4.21.0: dependencies: @@ -8268,6 +9414,8 @@ snapshots: xdg-basedir@5.1.0: {} + xml-naming@0.1.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/tsconfig.base.json b/tsconfig.base.json index 1a1401432..a5d60fa2a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "cyrus-linear-client": ["packages/linear-client"], "cyrus-history-stream": ["packages/history-stream"], "cyrus-edge-worker": ["packages/edge-worker"], + "cyrus-agent-runtime": ["packages/agent-runtime"], "cyrus-claude-runner": ["packages/claude-runner"], "cyrus-codex-runner": ["packages/codex-runner"], "cyrus-cursor-runner": ["packages/cursor-runner"], From f9b6150bbc2db112990030765669780d6040d2f8 Mon Sep 17 00:00:00 2001 From: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Date: Fri, 15 May 2026 11:21:05 -0700 Subject: [PATCH 2/6] docs(agent-runtime): record claude daytona validation --- packages/agent-runtime/ASSUMPTIONS.md | 2 +- packages/agent-runtime/VALIDATION.md | 30 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/agent-runtime/ASSUMPTIONS.md b/packages/agent-runtime/ASSUMPTIONS.md index 5a8c96372..dd18b06e3 100644 --- a/packages/agent-runtime/ASSUMPTIONS.md +++ b/packages/agent-runtime/ASSUMPTIONS.md @@ -27,7 +27,7 @@ This package is intentionally built as a new standalone runtime layer with minim - Daytona's ComputeSDK provider was smoke-tested with a remote working directory of `/home/daytona`; `/workspace` should not be assumed portable across providers. - Cursor Agent was smoke-tested inside Daytona by installing the CLI with `curl https://cursor.com/install -fsS | bash` and running `/home/daytona/.local/bin/cursor-agent` with `CURSOR_API_KEY` provided as a secret environment variable. - Codex Agent was smoke-tested inside Daytona far enough to authenticate and start a turn by materializing `~/.codex/auth.json` as a sensitive runtime file. Passing only `OPENAI_API_KEY` from the local Codex auth file produced a remote 401. The authenticated Codex turn later hit the account usage limit. -- Claude Code was smoke-tested inside Daytona far enough to install, start, and emit `system`/`assistant`/`result` events, but the local `claude.ai` login did not appear to be portable as a simple file or environment secret. Remote Claude completion needs an explicit portable credential such as `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`. +- Claude Code was smoke-tested inside Daytona by installing the CLI with a user-local npm prefix and running `/home/daytona/.npm-global/bin/claude` with `CLAUDE_CODE_OAUTH_TOKEN` provided as a secret environment variable. The remote session emitted `system`/`assistant`/`result` events and completed successfully. ## Security Contract diff --git a/packages/agent-runtime/VALIDATION.md b/packages/agent-runtime/VALIDATION.md index 10e1abafe..5cd42dfc9 100644 --- a/packages/agent-runtime/VALIDATION.md +++ b/packages/agent-runtime/VALIDATION.md @@ -189,23 +189,35 @@ Observed authenticated-but-limited result: } ``` -## Real Daytona Claude Auth Probe +## Real Daytona Claude Smoke -Claude Code was validated inside Daytona far enough to prove event capture from a remote Claude process: +Claude Code was validated inside Daytona with an explicit portable Claude Code OAuth token provided as a secret environment variable: - `@anthropic-ai/claude-code` installed successfully with a user-local npm prefix. - `claude --version` returned `2.1.142 (Claude Code)`. - `claude -p ... --output-format stream-json --verbose` emitted `system`, `assistant`, and `result` events inside Daytona. -- The result was `Not logged in · Please run /login`. +- The remote Claude session completed successfully with the exact requested result. -Observed auth failure: +Observed runtime result: ```json { - "success": false, - "result": "Not logged in · Please run /login", - "events": ["system", "assistant", "result"] + "success": true, + "exitCode": 0, + "result": "daytona claude event smoke ok", + "eventCount": 9, + "eventKinds": [ + "setup.started", + "setup.completed", + "setup.started", + "setup.completed", + "setup.started", + "setup.completed", + "system", + "assistant", + "result", + "stop.requested" + ], + "transcriptKinds": ["system", "assistant", "result"] } ``` - -The local Claude auth method is `claude.ai` first-party subscription auth, and no portable `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` was present in the environment. From cabbde4c137562e88471ea5a85ea31eb3cf369d9 Mon Sep 17 00:00:00 2001 From: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Date: Fri, 15 May 2026 15:21:10 -0700 Subject: [PATCH 3/6] feat(agent-runtime): live process streaming for local and Daytona sandboxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional `streamCommand(command, options)` capability to `RunnerSandbox`, with `onStdout` / `onStderr` chunk callbacks, an `AbortSignal` for cancellation, and an `AsyncIterable input` option for live stdin. Local provider implements it via `child_process.spawn`; Daytona is reached through a pluggable `NativeStreamAdapter` registry that unwraps ComputeSDK's `ProviderSandbox.getInstance()` to the native `@daytonaio/sdk` Sandbox and uses async sessions + `getSessionCommandLogs(onStdout, onStderr)`. `RuntimeAgentSession.start()` now prefers `streamCommand` when `capabilities.streamingProcess` is true, line-buffers chunks across packet boundaries, and emits `TranscriptEvent`s as the harness CLI produces them. New `interactiveInput` opt-in routes `addMessage()` into the running process's stdin (default off — most one-shot CLIs block on a piped-but-never-closed stdin). Verified end-to-end: - local `spawn`: chunks land at the exact 400ms cadence the child emits - real `codex exec` via `createAgentSession`: events emitted ~8.6s before turn end - real Daytona Claude `stream-json`: system event landed 1.7s before result event over a remote sandbox --- CHANGELOG.internal.md | 1 + .../agent-runtime/src/sandbox/compute-sdk.ts | 75 +++- packages/agent-runtime/src/sandbox/local.ts | 106 ++++- .../sandbox/native-stream-adapters/daytona.ts | 203 ++++++++++ .../sandbox/native-stream-adapters/index.ts | 35 ++ .../sandbox/native-stream-adapters/types.ts | 61 +++ packages/agent-runtime/src/schemas.ts | 1 + packages/agent-runtime/src/session.ts | 140 ++++++- packages/agent-runtime/src/types.ts | 41 ++ .../test-scripts/streaming-spike.mjs | 368 ++++++++++++++++++ packages/agent-runtime/test/runtime.test.ts | 246 ++++++++++++ packages/agent-runtime/test/sandbox.test.ts | 228 ++++++++++- 12 files changed, 1482 insertions(+), 23 deletions(-) create mode 100644 packages/agent-runtime/src/sandbox/native-stream-adapters/daytona.ts create mode 100644 packages/agent-runtime/src/sandbox/native-stream-adapters/index.ts create mode 100644 packages/agent-runtime/src/sandbox/native-stream-adapters/types.ts create mode 100644 packages/agent-runtime/test-scripts/streaming-spike.mjs diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index 5d9562f37..585bf3688 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -6,6 +6,7 @@ This changelog documents internal development changes, refactors, tooling update ### Added - Added `cyrus-agent-runtime`, a standalone experimental TypeScript package for unified agent session orchestration across harnesses and sandbox providers. It includes normalized session config, transcript envelopes, local and ComputeSDK-backed sandbox abstractions, harness adapters for Claude/Codex/Cursor/Gemini plus provisional PI/OpenCode adapters, and focused tests for config, runtime lifecycle, sandbox execution, and transcript parsing. +- Added live process streaming to `cyrus-agent-runtime`. New optional `RunnerSandbox.streamCommand(command, options)` capability surfaces stdout/stderr chunks to callbacks as they arrive, with `signal: AbortSignal` for cancellation and `input: AsyncIterable` for live stdin. Implemented natively in `LocalSandboxProvider` (via `child_process.spawn`) and for Daytona inside `ComputeSdkSandboxProvider` via a pluggable `NativeStreamAdapter` registry that reaches the underlying `@daytonaio/sdk` Sandbox through ComputeSDK's `ProviderSandbox.getInstance()` escape hatch, using async sessions + `getSessionCommandLogs(onStdout, onStderr)`. User-supplied adapters can be registered via `ComputeSdkSandboxProviderOptions.nativeStreamAdapters` for ComputeSDK providers we don't bundle (E2B, Vercel, Blaxel, Modal, Railway, Runloop, Cloudflare, Codesandbox). `RuntimeAgentSession.start()` now prefers `streamCommand` when `capabilities.streamingProcess` is true, line-buffers chunks across packet boundaries, and emits `TranscriptEvent`s live as the harness CLI produces them. New `CreateAgentSessionConfig.interactiveInput` opt-in flag routes `addMessage()` chunks into the running process's stdin (most one-shot CLIs hang on piped-but-never-closed stdin, so this defaults off). Verified end-to-end against real `codex exec` (events emitted ~8.6s before turn end), the local `child_process.spawn` path (chunks landed at the exact 400ms cadence the child produced them), and real Daytona Claude `stream-json` (system event landed 1.7s before result event over a remote sandbox). ## [0.2.50] - 2026-04-30 diff --git a/packages/agent-runtime/src/sandbox/compute-sdk.ts b/packages/agent-runtime/src/sandbox/compute-sdk.ts index e07ea19d3..61b9d92cb 100644 --- a/packages/agent-runtime/src/sandbox/compute-sdk.ts +++ b/packages/agent-runtime/src/sandbox/compute-sdk.ts @@ -7,8 +7,22 @@ import type { SandboxFilesystem, SandboxProvider, SandboxRunCommandOptions, + SandboxStreamCommandOptions, } from "../types.js"; import { DEFAULT_RUNNER_SANDBOX_CAPABILITIES } from "./local.js"; +import { + BUILT_IN_NATIVE_STREAM_ADAPTERS, + type NativeStreamAdapter, + resolveNativeStreamAdapter, +} from "./native-stream-adapters/index.js"; + +// Re-export Daytona shape types so existing callers importing them from this +// module continue to work after the adapter refactor. +export { + type DaytonaNativeSandboxShape, + type DaytonaProcessShape, + hasDaytonaProcessShape, +} from "./native-stream-adapters/daytona.js"; export interface ComputeSdkFilesystemLike { readFile?(path: string): Promise; @@ -36,6 +50,14 @@ export interface ComputeSdkSandboxLike { command: string, options?: SandboxRunCommandOptions, ): Promise | string>; + /** + * ComputeSDK's `ProviderSandbox.getInstance()` escape hatch — returns the + * native provider sandbox (e.g. @daytonaio/sdk Sandbox). Used by + * {@link ComputeSdkRunnerSandbox.streamCommand} to access provider-specific + * streaming primitives that ComputeSDK's universal `runCommand` cannot + * expose. + */ + getInstance?(): unknown; destroy?(): Promise; dispose?(): Promise; } @@ -50,6 +72,16 @@ export interface ComputeSdkLike { export interface ComputeSdkSandboxProviderOptions { compute: ComputeSdkLike; capabilities?: RunnerSandboxCapabilities; + /** + * User-supplied native-streaming adapters. Tried after the built-in + * adapters (currently Daytona only). Use this to add streaming support + * for ComputeSDK providers we don't ship a built-in for, e.g. E2B, + * Vercel, Blaxel, Modal, Railway, Runloop, Cloudflare, Codesandbox. + * + * Each adapter probes `ProviderSandbox.getInstance()` for a recognized + * native shape (see {@link NativeStreamAdapter}). + */ + nativeStreamAdapters?: readonly NativeStreamAdapter[]; } export class ComputeSdkSandboxProvider implements SandboxProvider { @@ -66,6 +98,7 @@ export class ComputeSdkSandboxProvider implements SandboxProvider { sandbox, this.options.capabilities ?? DEFAULT_RUNNER_SANDBOX_CAPABILITIES, config, + this.options.nativeStreamAdapters, ); } @@ -92,12 +125,16 @@ export class ComputeSdkRunnerSandbox implements RunnerSandbox { readonly sandboxId: string; readonly provider: string; readonly workingDirectory?: string; + readonly capabilities: RunnerSandboxCapabilities; readonly filesystem: SandboxFilesystem; + private readonly streamAdapter?: NativeStreamAdapter; + private readonly nativeInstance: unknown; constructor( private readonly sandbox: ComputeSdkSandboxLike, - readonly capabilities: RunnerSandboxCapabilities, + capabilities: RunnerSandboxCapabilities, config: RuntimeSandboxConfig, + extraStreamAdapters?: readonly NativeStreamAdapter[], ) { this.sandboxId = sandbox.sandboxId ?? sandbox.id ?? config.id ?? "compute"; this.provider = sandbox.provider ?? config.provider; @@ -109,6 +146,20 @@ export class ComputeSdkRunnerSandbox implements RunnerSandbox { ); } this.filesystem = new ComputeSdkFilesystem(filesystem); + + // Probe the ComputeSDK escape hatch for a native sandbox a registered + // adapter can drive. Today's built-ins recognize Daytona; users can + // extend by passing extraStreamAdapters. + this.nativeInstance = sandbox.getInstance?.(); + this.streamAdapter = resolveNativeStreamAdapter( + this.nativeInstance, + extraStreamAdapters, + ); + this.capabilities = { + ...capabilities, + streamingProcess: + capabilities.streamingProcess || Boolean(this.streamAdapter), + }; } async runCommand( @@ -136,6 +187,28 @@ export class ComputeSdkRunnerSandbox implements RunnerSandbox { }; } + async streamCommand( + command: string, + options: SandboxStreamCommandOptions = {}, + ): Promise { + if (!this.streamAdapter) { + const builtIns = BUILT_IN_NATIVE_STREAM_ADAPTERS.map((a) => a.name).join( + ", ", + ); + throw new Error( + `ComputeSDK sandbox does not support streaming for provider "${this.provider}". ` + + `No registered NativeStreamAdapter detected a streaming-capable native instance. ` + + `Built-in adapters: ${builtIns}. Add support via ` + + `ComputeSdkSandboxProviderOptions.nativeStreamAdapters.`, + ); + } + return this.streamAdapter.streamCommand( + this.nativeInstance, + command, + options, + ); + } + async destroy(): Promise { if (this.sandbox.destroy) { await this.sandbox.destroy(); diff --git a/packages/agent-runtime/src/sandbox/local.ts b/packages/agent-runtime/src/sandbox/local.ts index 88a2a55d6..5297f02d5 100644 --- a/packages/agent-runtime/src/sandbox/local.ts +++ b/packages/agent-runtime/src/sandbox/local.ts @@ -18,6 +18,7 @@ import type { SandboxFilesystem, SandboxProvider, SandboxRunCommandOptions, + SandboxStreamCommandOptions, } from "../types.js"; export const UNSUPPORTED_STREAMING_PROCESS_REASON = @@ -29,6 +30,15 @@ export const DEFAULT_RUNNER_SANDBOX_CAPABILITIES: RunnerSandboxCapabilities = { streamingProcess: false, }; +/** + * Local execution is always stream-capable via Node's child_process.spawn. + */ +export const LOCAL_RUNNER_SANDBOX_CAPABILITIES: RunnerSandboxCapabilities = { + filesystem: true, + runCommand: true, + streamingProcess: true, +}; + export interface LocalSandboxProviderOptions { workingDirectory?: string; capabilities?: RunnerSandboxCapabilities; @@ -44,7 +54,7 @@ export class LocalSandboxProvider implements SandboxProvider { options.workingDirectory ?? process.cwd(), ); this.capabilities = - options.capabilities ?? DEFAULT_RUNNER_SANDBOX_CAPABILITIES; + options.capabilities ?? LOCAL_RUNNER_SANDBOX_CAPABILITIES; } async create(config: RuntimeSandboxConfig = { provider: "local" }) { @@ -87,6 +97,18 @@ export class LocalRunnerSandbox implements RunnerSandbox { return runLocalCommand(command, this.workingDirectory, options); } + async streamCommand( + command: string, + options: SandboxStreamCommandOptions = {}, + ): Promise { + return runLocalCommand(command, this.workingDirectory, options, { + onStdout: options.onStdout, + onStderr: options.onStderr, + signal: options.signal, + input: options.input, + }); + } + async destroy() { return; } @@ -143,10 +165,18 @@ export class LocalSandboxFilesystem implements SandboxFilesystem { } } +interface LocalStreamHooks { + onStdout?: (chunk: string) => void; + onStderr?: (chunk: string) => void; + signal?: AbortSignal; + input?: AsyncIterable; +} + function runLocalCommand( command: string, workingDirectory: string, options: SandboxRunCommandOptions, + stream: LocalStreamHooks = {}, ): Promise { if (options.background) { return Promise.reject( @@ -158,19 +188,26 @@ function runLocalCommand( return new Promise((resolveCommand, reject) => { const startedAt = Date.now(); + // We always pipe stdout/stderr so we can capture + stream; stdin is + // only piped when the caller supplies an input iterable. Either way, + // child.stdout/stderr are guaranteed non-null below (asserted). + const stdinMode: "ignore" | "pipe" = stream.input ? "pipe" : "ignore"; const child = spawn(command, { cwd: options.cwd ? resolveCommandCwd(workingDirectory, options.cwd) : workingDirectory, env: { ...process.env, ...options.env }, shell: true, - stdio: ["ignore", "pipe", "pipe"], + stdio: [stdinMode, "pipe", "pipe"], }); + const childStdout = child.stdout!; + const childStderr = child.stderr!; let settled = false; let stdout = ""; let stderr = ""; let timeout: NodeJS.Timeout | undefined; + let inputDrainer: Promise | undefined; if (options.timeout !== undefined) { timeout = setTimeout(() => { @@ -178,16 +215,66 @@ function runLocalCommand( }, options.timeout); } - child.stdout.setEncoding("utf8"); - child.stderr.setEncoding("utf8"); - child.stdout.on("data", (chunk: string) => { + const onAbort = () => { + child.kill("SIGTERM"); + }; + if (stream.signal) { + if (stream.signal.aborted) { + child.kill("SIGTERM"); + } else { + stream.signal.addEventListener("abort", onAbort, { once: true }); + } + } + + if (stream.input && child.stdin) { + const stdin = child.stdin; + const inputIterable = stream.input; + inputDrainer = (async () => { + try { + for await (const chunk of inputIterable) { + if (stream.signal?.aborted) return; + if (!stdin.writable) return; + stdin.write(chunk); + } + } catch { + // Iterable errors close stdin below. + } finally { + try { + stdin.end(); + } catch { + // stdin may already be closed by the process exiting. + } + } + })(); + // stdin errors should not crash the run. + stdin.on("error", () => {}); + } + + childStdout.setEncoding("utf8"); + childStderr.setEncoding("utf8"); + childStdout.on("data", (chunk: string) => { stdout += chunk; + if (stream.onStdout) { + try { + stream.onStdout(chunk); + } catch { + // Caller-supplied callbacks must not break the run; swallow. + } + } }); - child.stderr.on("data", (chunk: string) => { + childStderr.on("data", (chunk: string) => { stderr += chunk; + if (stream.onStderr) { + try { + stream.onStderr(chunk); + } catch { + // Caller-supplied callbacks must not break the run; swallow. + } + } }); child.on("error", (error) => { if (timeout) clearTimeout(timeout); + stream.signal?.removeEventListener("abort", onAbort); if (!settled) { settled = true; reject(error); @@ -195,8 +282,15 @@ function runLocalCommand( }); child.on("close", (exitCode) => { if (timeout) clearTimeout(timeout); + stream.signal?.removeEventListener("abort", onAbort); if (!settled) { settled = true; + // Resolve as soon as the child exits. The input drainer (if any) + // is intentionally orphaned — the caller owns the input + // iterable's lifetime and closes it after the command returns. + // Awaiting it here would deadlock when the iterable outlives + // the process. + void inputDrainer; resolveCommand({ stdout, stderr, diff --git a/packages/agent-runtime/src/sandbox/native-stream-adapters/daytona.ts b/packages/agent-runtime/src/sandbox/native-stream-adapters/daytona.ts new file mode 100644 index 000000000..8c417e903 --- /dev/null +++ b/packages/agent-runtime/src/sandbox/native-stream-adapters/daytona.ts @@ -0,0 +1,203 @@ +import type { + CommandExecutionResult, + SandboxStreamCommandOptions, +} from "../../types.js"; +import type { NativeStreamAdapter } from "./types.js"; + +/** + * Structural shape of the Daytona @daytonaio/sdk Sandbox.process surface we + * need for live log streaming. Typed loosely so this package does not take a + * hard dependency on @daytonaio/sdk. + */ +export interface DaytonaProcessShape { + createSession(sessionId: string): Promise; + executeSessionCommand( + sessionId: string, + req: { + command: string; + runAsync?: boolean; + suppressInputEcho?: boolean; + }, + timeout?: number, + ): Promise<{ cmdId?: string }>; + getSessionCommandLogs( + sessionId: string, + commandId: string, + onStdout: (chunk: string) => void, + onStderr: (chunk: string) => void, + ): Promise; + getSessionCommand( + sessionId: string, + commandId: string, + ): Promise<{ exitCode?: number }>; + deleteSession(sessionId: string): Promise; + sendSessionCommandInput?( + sessionId: string, + commandId: string, + data: string, + ): Promise; +} + +export interface DaytonaNativeSandboxShape { + process: DaytonaProcessShape; +} + +export function hasDaytonaProcessShape( + instance: unknown, +): instance is DaytonaNativeSandboxShape { + if (!instance || typeof instance !== "object") return false; + const proc = (instance as { process?: unknown }).process; + if (!proc || typeof proc !== "object") return false; + const p = proc as Record; + return ( + typeof p.createSession === "function" && + typeof p.executeSessionCommand === "function" && + typeof p.getSessionCommandLogs === "function" && + typeof p.getSessionCommand === "function" && + typeof p.deleteSession === "function" + ); +} + +/** Built-in adapter for Daytona via @daytonaio/sdk. */ +export const daytonaStreamAdapter: NativeStreamAdapter = { + name: "daytona", + detect: hasDaytonaProcessShape, + async streamCommand(instance, command, options) { + if (!hasDaytonaProcessShape(instance)) { + throw new Error( + "daytonaStreamAdapter.streamCommand received a non-Daytona instance.", + ); + } + return runDaytonaStreamCommand(instance.process, command, options); + }, +}; + +async function runDaytonaStreamCommand( + proc: DaytonaProcessShape, + command: string, + options: SandboxStreamCommandOptions, +): Promise { + const startedAt = Date.now(); + const sessionId = `agent-runtime-stream-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 10)}`; + + // Compose env/cwd into the command string the same way ComputeSDK's + // Daytona provider does — Daytona's session API doesn't accept these as + // structured fields. + let fullCommand = command; + if (options.env && Object.keys(options.env).length > 0) { + const envPrefix = Object.entries(options.env) + .map(([k, v]) => `${k}=${shellQuote(v)}`) + .join(" "); + fullCommand = `${envPrefix} ${fullCommand}`; + } + if (options.cwd) { + fullCommand = `cd ${shellQuote(options.cwd)} && ${fullCommand}`; + } + + const abort = options.signal; + const onAbort = () => { + void proc.deleteSession(sessionId).catch(() => {}); + }; + + await proc.createSession(sessionId); + let stdoutBuffer = ""; + let stderrBuffer = ""; + let cmdId: string | undefined; + let inputDrainer: Promise | undefined; + + try { + if (abort?.aborted) { + throw new Error("Stream command aborted before start."); + } + abort?.addEventListener("abort", onAbort, { once: true }); + + const started = await proc.executeSessionCommand(sessionId, { + command: fullCommand, + runAsync: true, + }); + cmdId = started.cmdId; + if (!cmdId) { + throw new Error( + "Daytona executeSessionCommand did not return a cmdId for streaming.", + ); + } + + // Drain caller-supplied stdin chunks concurrently with the log stream. + // We intentionally do not await this drainer at the end — the caller + // (typically RuntimeAgentSession) owns the input iterable's lifetime + // and closes it after the process exits. Awaiting here would deadlock + // because the iterable is still open when the process exits. + if (options.input) { + if (!proc.sendSessionCommandInput) { + throw new Error( + "Daytona SDK does not expose sendSessionCommandInput — cannot route stdin chunks.", + ); + } + const cmdIdFinal = cmdId; + const sendInput = proc.sendSessionCommandInput.bind(proc); + inputDrainer = (async () => { + for await (const chunk of options.input!) { + if (abort?.aborted) return; + try { + await sendInput(sessionId, cmdIdFinal, chunk); + } catch { + // Process may have exited; subsequent writes will fail. + return; + } + } + })(); + inputDrainer.catch(() => {}); + } + + // This promise resolves when the remote command finishes and all logs + // have been drained. Callbacks fire live as bytes arrive. + await proc.getSessionCommandLogs( + sessionId, + cmdId, + (chunk) => { + stdoutBuffer += chunk; + if (options.onStdout) { + try { + options.onStdout(chunk); + } catch { + // Caller-supplied callbacks must not break the run. + } + } + }, + (chunk) => { + stderrBuffer += chunk; + if (options.onStderr) { + try { + options.onStderr(chunk); + } catch { + // Caller-supplied callbacks must not break the run. + } + } + }, + ); + + // Note: we intentionally do not await inputDrainer here — the caller + // owns the input iterable's lifetime. See comment above. + void inputDrainer; + const final = await proc.getSessionCommand(sessionId, cmdId); + return { + stdout: stdoutBuffer, + stderr: stderrBuffer, + exitCode: final.exitCode ?? 0, + durationMs: Date.now() - startedAt, + }; + } finally { + abort?.removeEventListener("abort", onAbort); + try { + await proc.deleteSession(sessionId); + } catch { + // Session may already be gone; ignore. + } + } +} + +function shellQuote(value: string): string { + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} diff --git a/packages/agent-runtime/src/sandbox/native-stream-adapters/index.ts b/packages/agent-runtime/src/sandbox/native-stream-adapters/index.ts new file mode 100644 index 000000000..ed9d6860a --- /dev/null +++ b/packages/agent-runtime/src/sandbox/native-stream-adapters/index.ts @@ -0,0 +1,35 @@ +export * from "./daytona.js"; +export * from "./types.js"; + +import { daytonaStreamAdapter } from "./daytona.js"; +import type { NativeStreamAdapter } from "./types.js"; + +/** + * Built-in adapters tried in order when a `ComputeSdkRunnerSandbox` is + * constructed. Currently only Daytona is wired; new adapters get appended + * here once we validate them against the real provider SDK. + */ +export const BUILT_IN_NATIVE_STREAM_ADAPTERS: readonly NativeStreamAdapter[] = [ + daytonaStreamAdapter, +]; + +/** + * Pick the first adapter whose `detect()` returns true for `instance`. + */ +export function resolveNativeStreamAdapter( + instance: unknown, + extraAdapters: readonly NativeStreamAdapter[] = [], +): NativeStreamAdapter | undefined { + if (instance === undefined || instance === null) return undefined; + for (const adapter of [ + ...BUILT_IN_NATIVE_STREAM_ADAPTERS, + ...extraAdapters, + ]) { + try { + if (adapter.detect(instance)) return adapter; + } catch { + // Bad adapters must not break detection for good ones. + } + } + return undefined; +} diff --git a/packages/agent-runtime/src/sandbox/native-stream-adapters/types.ts b/packages/agent-runtime/src/sandbox/native-stream-adapters/types.ts new file mode 100644 index 000000000..59da6cabd --- /dev/null +++ b/packages/agent-runtime/src/sandbox/native-stream-adapters/types.ts @@ -0,0 +1,61 @@ +import type { + CommandExecutionResult, + SandboxStreamCommandOptions, +} from "../../types.js"; + +/** + * Adapter that knows how to drive a native provider sandbox (the thing + * returned from ComputeSDK's `ProviderSandbox.getInstance()`) using + * provider-specific streaming primitives that ComputeSDK's universal + * `runCommand` does not expose. + * + * Each adapter probes the native instance via {@link detect}; the first + * adapter whose `detect()` returns `true` claims the sandbox and is used to + * back {@link RunnerSandbox.streamCommand}. + * + * To add support for a new ComputeSDK provider, implement this interface and + * pass it as `nativeStreamAdapters` on {@link ComputeSdkSandboxProviderOptions}. + * Existing built-ins are exported from + * `./native-stream-adapters/index.js`. + * + * TODO: built-in adapters to add as we validate them against real SDKs: + * - @e2b/sdk Sandbox — has `commands.run({ onStdout, onStderr })` and + * `sandbox.process.start()` returning a handle + * with `sendInput`. Streaming-native. + * - @vercel/sandbox — `sandbox.runCommand` streams via callbacks on + * recent versions. + * - @blaxel/sandbox — exposes a `Process` with stdout / stderr + * observables. + * - @modal-labs/modal — `Sandbox.exec().stdout.read_async()` chunked. + * - @railway/sandbox — TBD. + * - @runloop/sandbox — uses Devbox API with `process.exec` + log polling. + * - @cloudflare/sandbox — Worker-based; HTTP streaming response. + * - @codesandbox/sdk — shell session with output streams. + * + * See https://github.com/computesdk/compute for the canonical list of + * providers. + */ +export interface NativeStreamAdapter { + /** Stable name used in error messages and capability metadata. */ + readonly name: string; + /** + * Structural type guard. Should return `true` only if `instance` is a + * native sandbox this adapter can drive. Implementations must be cheap + * and non-throwing. + */ + detect(instance: unknown): boolean; + /** + * Stream a command. Implementations must: + * - Invoke `options.onStdout(chunk)` and `options.onStderr(chunk)` as + * bytes arrive (not buffered until exit). + * - Honor `options.signal` (best-effort cancel). + * - Drain `options.input` (if provided) into the process's stdin live. + * - Resolve with the full buffered `CommandExecutionResult` once the + * process exits. + */ + streamCommand( + instance: unknown, + command: string, + options: SandboxStreamCommandOptions, + ): Promise; +} diff --git a/packages/agent-runtime/src/schemas.ts b/packages/agent-runtime/src/schemas.ts index b6745c4ed..f1554a0a7 100644 --- a/packages/agent-runtime/src/schemas.ts +++ b/packages/agent-runtime/src/schemas.ts @@ -114,4 +114,5 @@ export const CreateAgentSessionConfigSchema = z.object({ sandbox: RuntimeSandboxConfigSchema.optional(), networkEgress: RuntimeNetworkEgressConfigSchema.optional(), metadata: z.record(z.string(), z.unknown()).optional(), + interactiveInput: z.boolean().optional(), }); diff --git a/packages/agent-runtime/src/session.ts b/packages/agent-runtime/src/session.ts index 3d2957a71..c9ccad7e9 100644 --- a/packages/agent-runtime/src/session.ts +++ b/packages/agent-runtime/src/session.ts @@ -49,6 +49,33 @@ class AsyncEventBuffer implements AsyncIterable { } } +/** + * Splits incoming chunks into newline-terminated lines for harness adapters + * to parse. Carries a partial-line buffer between chunks so an event that + * arrives split across multiple TCP packets is still parsed as one line. + */ +class LineSplitter { + private buffer = ""; + + push(chunk: string, onLine: (line: string) => void): void { + this.buffer += chunk; + let nl = this.buffer.indexOf("\n"); + while (nl !== -1) { + const line = this.buffer.slice(0, nl); + this.buffer = this.buffer.slice(nl + 1); + const stripped = line.endsWith("\r") ? line.slice(0, -1) : line; + if (stripped.trim()) onLine(stripped); + nl = this.buffer.indexOf("\n"); + } + } + + flush(onLine: (line: string) => void): void { + const remaining = this.buffer; + this.buffer = ""; + if (remaining.trim()) onLine(remaining); + } +} + export class RuntimeAgentSession extends EventEmitter implements AgentSession { readonly sessionId: string; readonly harness: NormalizedAgentSessionConfig["harness"]["kind"]; @@ -57,6 +84,9 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { private readonly eventBuffer = new AsyncEventBuffer(); private readonly observedEvents: TranscriptEvent[] = []; private readonly queuedMessages: string[] = []; + private readonly inputBuffer = new AsyncEventBuffer(); + private readonly abortController = new AbortController(); + private streamingActive = false; private stopped = false; private started = false; @@ -79,36 +109,103 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { this.started = true; const command = this.adapter.buildCommand(this.config); + const fullCommand = [command.command, ...command.args.map(shellQuote)].join( + " ", + ); + const env = { + ...this.config.env, + ...command.env, + ...this.materializeSecrets(), + }; + const cwd = this.config.sandbox.workingDirectory; const startedAt = Date.now(); + try { await this.materializeFiles(); await this.runSetupCommands(); - const result = await this.sandbox.runCommand( - [command.command, ...command.args.map(shellQuote)].join(" "), - { - cwd: this.config.sandbox.workingDirectory, - env: { - ...this.config.env, - ...command.env, - ...this.materializeSecrets(), + + const canStream = + typeof this.sandbox.streamCommand === "function" && + this.sandbox.capabilities.streamingProcess === true; + + let exitCode: number; + if (canStream) { + this.streamingActive = true; + const stdoutSplitter = new LineSplitter(); + const stderrSplitter = new LineSplitter(); + // Only pipe stdin when the caller opts in to interactive input. + // Most one-shot harness CLIs (e.g. `codex exec`) block forever + // on a piped-but-never-closed stdin. + const inputIterable = this.config.interactiveInput + ? this.inputBuffer + : undefined; + const result = await this.sandbox.streamCommand!(fullCommand, { + cwd, + env, + signal: this.abortController.signal, + input: inputIterable, + onStdout: (chunk) => { + stdoutSplitter.push(chunk, (line) => { + const event = this.adapter.parseStdoutLine(line, { + sessionId: this.sessionId, + harness: this.harness, + }); + if (event) { + void this.emitEvent(event); + } + }); }, - }, - ); + onStderr: (chunk) => { + stderrSplitter.push(chunk, (line) => { + const event = this.adapter.parseStderrLine?.(line, { + sessionId: this.sessionId, + harness: this.harness, + }); + if (event) { + void this.emitEvent(event); + } + }); + }, + }); + // Flush any trailing partial lines the process did not terminate. + stdoutSplitter.flush((line) => { + const event = this.adapter.parseStdoutLine(line, { + sessionId: this.sessionId, + harness: this.harness, + }); + if (event) void this.emitEvent(event); + }); + stderrSplitter.flush((line) => { + const event = this.adapter.parseStderrLine?.(line, { + sessionId: this.sessionId, + harness: this.harness, + }); + if (event) void this.emitEvent(event); + }); + exitCode = result.exitCode; + } else { + const result = await this.sandbox.runCommand(fullCommand, { cwd, env }); + await this.parseBufferedOutput(result.stdout, "stdout"); + await this.parseBufferedOutput(result.stderr, "stderr"); + exitCode = result.exitCode; + } - await this.parseOutput(result.stdout, "stdout"); - await this.parseOutput(result.stderr, "stderr"); + this.streamingActive = false; + this.inputBuffer.close(); const runtimeResult: AgentSessionResult = { sessionId: this.sessionId, harness: this.harness, - success: result.exitCode === 0 && !this.stopped, - exitCode: result.exitCode, + success: exitCode === 0 && !this.stopped, + exitCode, result: this.adapter.extractResult?.(this.observedEvents), events: [...this.observedEvents], }; this.eventBuffer.close(); return runtimeResult; } catch (error) { + this.streamingActive = false; + this.inputBuffer.close(); const err = error instanceof Error ? error : new Error(String(error)); const failedEvent = this.createEvent("error", { message: err.message, @@ -129,6 +226,17 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { async addMessage(message: string): Promise { this.queuedMessages.push(message); await this.emitEvent(this.createEvent("message.queued", { message })); + // If the harness is actively streaming AND the session was started in + // interactive-input mode, route this message into the running process's + // stdin so it can react live. Otherwise the queue remains observable + // via getQueuedMessages() for callers that want to drain it themselves + // before/after start(). + if (this.streamingActive && this.config.interactiveInput) { + // Newline-terminate so line-oriented consumers (most agent CLIs in + // stream-json mode) see one input per line. + const wire = message.endsWith("\n") ? message : `${message}\n`; + this.inputBuffer.push(wire); + } } async interrupt(reason?: string): Promise { @@ -138,6 +246,8 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { async stop(reason?: string): Promise { this.stopped = true; await this.emitEvent(this.createEvent("stop.requested", { reason })); + this.abortController.abort(); + this.inputBuffer.close(); await this.sandbox.destroy(); this.eventBuffer.close(); } @@ -146,7 +256,7 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { return this.queuedMessages; } - private async parseOutput( + private async parseBufferedOutput( output: string, stream: "stdout" | "stderr", ): Promise { diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts index 7ff88a0bd..7fccff7b4 100644 --- a/packages/agent-runtime/src/types.ts +++ b/packages/agent-runtime/src/types.ts @@ -103,6 +103,14 @@ export interface CreateAgentSessionConfig { sandbox?: RuntimeSandboxConfig; networkEgress?: RuntimeNetworkEgressConfig; metadata?: Record; + /** + * When `true`, opens an interactive stdin pipe to the harness process so + * `addMessage()` chunks reach the running CLI live. Default `false` — + * most one-shot harness CLIs (e.g. `codex exec`) hang if stdin is piped + * without being closed, so this is opt-in. Set to `true` for harnesses + * that consume `--input-format stream-json` or similar. + */ + interactiveInput?: boolean; } export interface TranscriptEvent { @@ -172,6 +180,29 @@ export interface SandboxRunCommandOptions { background?: boolean; } +/** + * Options for {@link RunnerSandbox.streamCommand}. Extends the one-shot + * {@link SandboxRunCommandOptions} with chunk callbacks that fire as bytes + * arrive from the running process. The returned {@link CommandExecutionResult} + * still contains the full buffered output for symmetry with `runCommand`. + */ +export interface SandboxStreamCommandOptions extends SandboxRunCommandOptions { + /** Invoked with each stdout chunk as it arrives. */ + onStdout?: (chunk: string) => void; + /** Invoked with each stderr chunk as it arrives. */ + onStderr?: (chunk: string) => void; + /** Abort the underlying process when this signal aborts. */ + signal?: AbortSignal; + /** + * Optional async iterable of chunks to feed into the process's stdin while + * it runs. Each yielded chunk is delivered to the running command live — + * local providers write to `child.stdin`; Daytona uses + * `sendSessionCommandInput`. The stream is closed (stdin EOF) when the + * iterable completes. + */ + input?: AsyncIterable; +} + export interface RunnerSandboxCapabilities { filesystem: boolean; runCommand: boolean; @@ -192,6 +223,16 @@ export interface RunnerSandbox { command: string, options?: SandboxRunCommandOptions, ): Promise; + /** + * Run a command and stream stdout/stderr chunks through callbacks as they + * arrive. Only available when {@link RunnerSandboxCapabilities.streamingProcess} + * is `true`. Providers that cannot stream do not implement this method; check + * the capability flag before calling. + */ + streamCommand?( + command: string, + options?: SandboxStreamCommandOptions, + ): Promise; destroy(): Promise; } diff --git a/packages/agent-runtime/test-scripts/streaming-spike.mjs b/packages/agent-runtime/test-scripts/streaming-spike.mjs new file mode 100644 index 000000000..56aa4c370 --- /dev/null +++ b/packages/agent-runtime/test-scripts/streaming-spike.mjs @@ -0,0 +1,368 @@ +#!/usr/bin/env node +// Streaming spike for the agent-runtime sandbox abstraction. +// +// Exercises RunnerSandbox.streamCommand() on two providers: +// 1. Local — Node child_process.spawn streams natively. +// 2. Daytona (via ComputeSDK) — uses async sessions + getSessionCommandLogs +// callbacks, reached through ProviderSandbox.getInstance(). +// +// The script prints arrival timestamps for each chunk so you can SEE that +// chunks land before the process exits. For Daytona it also exercises a real +// Claude stream-json run end-to-end. +// +// Usage: +// # Local only (no secrets needed): +// node packages/agent-runtime/test-scripts/streaming-spike.mjs local +// +// # Daytona + Claude (requires DAYTONA_API_KEY + CLAUDE_CODE_OAUTH_TOKEN): +// set -a; source ~/.cyrus/secrets/daytona.env; source ~/.cyrus/secrets/claude.env; set +a +// pnpm --filter cyrus-agent-runtime build +// node packages/agent-runtime/test-scripts/streaming-spike.mjs daytona + +import { createLocalSandboxProvider } from "../dist/sandbox/local.js"; +import { createComputeSdkSandboxProvider } from "../dist/sandbox/compute-sdk.js"; +import { createAgentSession } from "../dist/runtime.js"; + +const mode = process.argv[2] ?? "local"; + +function fmt(ms) { + return `${ms.toString().padStart(5, " ")}ms`; +} + +async function runLocalSpike() { + console.log("\n=== Local streamCommand spike ===\n"); + const provider = createLocalSandboxProvider({ + workingDirectory: process.cwd(), + }); + const sandbox = await provider.create({ provider: "local" }); + + console.log("capabilities:", sandbox.capabilities); + if (!sandbox.streamCommand) { + throw new Error("Local sandbox does not implement streamCommand."); + } + + // Emit 5 lines, each 400ms apart. If streaming works, we'll see chunks + // land at ~400/800/1200/1600/2000ms; if it doesn't, we'll see all of + // them at the end. + const command = + "node -e \"" + + "let i = 0;" + + "const t = setInterval(() => {" + + " i++; console.log('line ' + i + ' at ' + Date.now());" + + " if (i >= 5) { clearInterval(t); console.error('done'); }" + + "}, 400);\""; + + const startedAt = Date.now(); + const arrivals = []; + const result = await sandbox.streamCommand(command, { + onStdout: (chunk) => { + const t = Date.now() - startedAt; + arrivals.push(t); + process.stdout.write(` [stdout @ ${fmt(t)}] ${chunk}`); + }, + onStderr: (chunk) => { + const t = Date.now() - startedAt; + process.stdout.write(` [stderr @ ${fmt(t)}] ${chunk}`); + }, + }); + + console.log("\nresult:", { + exitCode: result.exitCode, + durationMs: result.durationMs, + stdoutLen: result.stdout.length, + stderrLen: result.stderr.length, + }); + const firstArrival = arrivals[0] ?? Infinity; + const lastArrival = arrivals.at(-1) ?? 0; + console.log( + "streaming evidence: first chunk @", + fmt(firstArrival), + "vs final duration", + fmt(result.durationMs), + "; spread across", + fmt(lastArrival - firstArrival), + ); + if (firstArrival >= result.durationMs - 50) { + console.error("\n WARNING: first chunk arrived at the very end —"); + console.error(" this looks buffered, not streamed.\n"); + } else { + console.log("\n ✓ Streaming confirmed: chunks arrived live.\n"); + } +} + +async function runDaytonaSpike() { + console.log("\n=== Daytona streamCommand spike (real remote) ===\n"); + if (!process.env.DAYTONA_API_KEY) { + throw new Error("DAYTONA_API_KEY is not set in the environment."); + } + const claudeToken = + process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN; + if (!claudeToken) { + throw new Error( + "CLAUDE_CODE_OAUTH_TOKEN (or ANTHROPIC_AUTH_TOKEN) is not set; needed for the Claude harness inside Daytona.", + ); + } + + const { daytona } = await import("@computesdk/daytona"); + const { compute } = await import("computesdk"); + compute.setConfig({ + provider: daytona({ + apiKey: process.env.DAYTONA_API_KEY, + timeout: 300_000, + }), + }); + + console.log("creating remote sandbox…"); + const wrapped = await compute.sandbox.create({ + directory: "/home/daytona", + timeout: 300_000, + metadata: { purpose: "agent-runtime-streaming-spike" }, + }); + + // Build a ComputeSdkRunnerSandbox by hand using the same provider wrapper + // our runtime would use, so streamCommand can probe getInstance(). + const provider = createComputeSdkSandboxProvider({ + compute: { + sandbox: { + async create() { + return wrapped; + }, + async getById(id) { + return wrapped.sandboxId === id ? wrapped : null; + }, + }, + }, + }); + const sandbox = await provider.create({ + provider: "daytona", + id: wrapped.sandboxId, + workingDirectory: "/home/daytona", + timeoutMs: 300_000, + }); + + console.log("capabilities:", sandbox.capabilities); + if (!sandbox.streamCommand) { + throw new Error("Daytona sandbox did not expose streamCommand."); + } + if (!sandbox.capabilities.streamingProcess) { + throw new Error( + "Daytona sandbox reported streamingProcess: false — getInstance() probe failed.", + ); + } + + try { + // First: a plain shell streaming probe. Confirms inter-chunk timing + // without involving any agent CLI. + console.log("\n--- probe 1: shell loop (5 lines, ~400ms apart) ---\n"); + const shellStartedAt = Date.now(); + const shellArrivals = []; + const shellResult = await sandbox.streamCommand( + "for i in 1 2 3 4 5; do echo \"shell line $i @ $(date +%s%3N)\"; sleep 0.4; done", + { + onStdout: (chunk) => { + const t = Date.now() - shellStartedAt; + shellArrivals.push(t); + process.stdout.write(` [stdout @ ${fmt(t)}] ${chunk}`); + }, + onStderr: (chunk) => { + const t = Date.now() - shellStartedAt; + process.stdout.write(` [stderr @ ${fmt(t)}] ${chunk}`); + }, + }, + ); + console.log( + "\nshell probe: exit", + shellResult.exitCode, + "duration", + fmt(shellResult.durationMs), + "first chunk @", + fmt(shellArrivals[0] ?? -1), + "spread", + fmt((shellArrivals.at(-1) ?? 0) - (shellArrivals[0] ?? 0)), + ); + + // Second: install Claude Code and stream a real stream-json session. + console.log("\n--- probe 2: install Claude Code remotely ---\n"); + const setupResult = await sandbox.runCommand( + "npm config set prefix /home/daytona/.npm-global && " + + "npm install -g @anthropic-ai/claude-code@latest >/dev/null 2>&1 && " + + "/home/daytona/.npm-global/bin/claude --version", + { timeout: 240_000 }, + ); + console.log( + "setup exit", + setupResult.exitCode, + "->", + setupResult.stdout.trim(), + ); + if (setupResult.exitCode !== 0) { + throw new Error( + `Claude install failed inside Daytona: ${setupResult.stderr}`, + ); + } + + console.log("\n--- probe 3: Claude stream-json over streamCommand ---\n"); + const claudeStartedAt = Date.now(); + const eventArrivals = []; + const lineBuffer = { stdout: "" }; + const claudeResult = await sandbox.streamCommand( + "/home/daytona/.npm-global/bin/claude " + + "-p 'Reply with exactly: streaming spike ok' " + + "--output-format stream-json --verbose", + { + env: { + PATH: "/home/daytona/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + CLAUDE_CODE_OAUTH_TOKEN: claudeToken, + ANTHROPIC_AUTH_TOKEN: claudeToken, + }, + onStdout: (chunk) => { + lineBuffer.stdout += chunk; + let idx = lineBuffer.stdout.indexOf("\n"); + while (idx !== -1) { + const line = lineBuffer.stdout.slice(0, idx); + lineBuffer.stdout = lineBuffer.stdout.slice(idx + 1); + if (line.trim().length > 0) { + const t = Date.now() - claudeStartedAt; + let kind = "unknown"; + try { + kind = JSON.parse(line).type ?? "unknown"; + } catch { + kind = `non-json(${line.slice(0, 40)}…)`; + } + eventArrivals.push({ kind, elapsedMs: t }); + console.log(` [claude event @ ${fmt(t)}] ${kind}`); + } + idx = lineBuffer.stdout.indexOf("\n"); + } + }, + onStderr: (chunk) => { + const t = Date.now() - claudeStartedAt; + process.stderr.write(` [stderr @ ${fmt(t)}] ${chunk}`); + }, + }, + ); + + console.log( + "\nclaude probe: exit", + claudeResult.exitCode, + "duration", + fmt(claudeResult.durationMs), + "events observed:", + eventArrivals.length, + ); + const systemEvent = eventArrivals.find((e) => e.kind === "system"); + const resultEvent = eventArrivals.find((e) => e.kind === "result"); + if (systemEvent && resultEvent) { + const gap = resultEvent.elapsedMs - systemEvent.elapsedMs; + console.log( + "streaming evidence: system event @", + fmt(systemEvent.elapsedMs), + "→ result event @", + fmt(resultEvent.elapsedMs), + "(gap", + fmt(gap), + ")", + ); + if (gap < 200) { + console.error( + "\n WARNING: system and result events arrived within 200ms —", + ); + console.error(" this could still be buffered, double check.\n"); + } else { + console.log( + "\n ✓ Live Daytona streaming confirmed: system event landed", + fmt(gap), + "before result event.\n", + ); + } + } else { + console.error( + "\n WARNING: did not observe both system and result events;", + "can't compare timing.\n", + ); + } + } finally { + console.log("destroying sandbox…"); + await sandbox.destroy(); + } +} + +async function runRuntimeLocalSpike() { + console.log("\n=== createAgentSession local streaming spike ===\n"); + // Proves the session prefers streamCommand and emits TranscriptEvents + // live, line-by-line, as the harness CLI emits them. + const startedAt = Date.now(); + const arrivals = []; + const session = await createAgentSession( + { + sessionId: "runtime-stream-spike", + harness: { kind: "codex", model: "gpt-5.2" }, + userPrompt: + "Reply exactly: runtime stream spike ok. Do not call any tools.", + sandbox: { provider: "local", workingDirectory: process.cwd() }, + }, + { + callbacks: { + onTranscriptEvent(event) { + const t = Date.now() - startedAt; + arrivals.push({ kind: event.kind, elapsedMs: t }); + console.log(` [event @ ${fmt(t)}] ${event.kind}`); + }, + }, + }, + ); + + const result = await session.start(); + console.log("\nsession result:", { + success: result.success, + exitCode: result.exitCode, + extracted: result.result, + events: result.events.length, + }); + + // Look for two distinct event arrival times to prove streaming. + const firstNonSetup = arrivals.find((a) => !a.kind.startsWith("setup.")); + const last = arrivals.at(-1); + if (firstNonSetup && last && last !== firstNonSetup) { + const gap = last.elapsedMs - firstNonSetup.elapsedMs; + console.log( + `streaming evidence: first non-setup event @ ${fmt(firstNonSetup.elapsedMs)} → last event @ ${fmt(last.elapsedMs)} (gap ${fmt(gap)})`, + ); + if (gap < 50) { + console.warn( + "\n WARNING: harness events landed within 50ms of each other —", + "could be buffered. Inspect transcript carefully.\n", + ); + } else { + console.log("\n ✓ Session-level streaming confirmed.\n"); + } + } else { + console.warn( + "\n WARNING: only one (or zero) harness events observed; not enough timing data.\n", + ); + } +} + +(async () => { + try { + if (mode === "local") { + await runLocalSpike(); + } else if (mode === "runtime-local") { + await runRuntimeLocalSpike(); + } else if (mode === "daytona") { + await runDaytonaSpike(); + } else if (mode === "all") { + await runLocalSpike(); + await runRuntimeLocalSpike(); + await runDaytonaSpike(); + } else { + console.error( + `unknown mode: ${mode} (expected 'local', 'runtime-local', 'daytona', or 'all')`, + ); + process.exit(1); + } + } catch (error) { + console.error("\nSpike failed:", error); + process.exit(1); + } +})(); diff --git a/packages/agent-runtime/test/runtime.test.ts b/packages/agent-runtime/test/runtime.test.ts index 304551c2c..433e30f4c 100644 --- a/packages/agent-runtime/test/runtime.test.ts +++ b/packages/agent-runtime/test/runtime.test.ts @@ -6,6 +6,7 @@ import type { RunnerSandboxCapabilities, SandboxFilesystem, SandboxProvider, + SandboxStreamCommandOptions, } from "../src/types.js"; describe("AgentRuntime", () => { @@ -115,6 +116,166 @@ describe("AgentRuntime", () => { ]); }); + it("prefers streamCommand and emits transcript events live, line-by-line", async () => { + // Three Codex events delivered as separate chunks with delays — proves + // the session parses each line as it arrives, not after the command exits. + const streamingSandbox = new StreamingFakeSandbox([ + { + delayMs: 0, + stdout: `${JSON.stringify({ + type: "item.started", + item: { type: "thought", text: "starting" }, + })}\n`, + }, + { + delayMs: 80, + stdout: `${JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "midway" }, + })}\n`, + }, + { + delayMs: 80, + stdout: `${JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "done" }, + })}\n`, + }, + ]); + + const arrivals: Array<{ kind: string; elapsedMs: number }> = []; + const startedAt = Date.now(); + const session = await createAgentSession( + { + sessionId: "session-stream", + harness: "codex", + userPrompt: "Do it", + }, + { + sandboxProviders: { local: new FakeSandboxProvider(streamingSandbox) }, + callbacks: { + onTranscriptEvent(event) { + arrivals.push({ + kind: event.kind, + elapsedMs: Date.now() - startedAt, + }); + }, + }, + }, + ); + + const result = await session.start(); + + expect(streamingSandbox.streamCalls).toBe(1); + expect(streamingSandbox.runCalls).toBe(0); + expect(result.success).toBe(true); + expect(result.result).toBe("done"); + expect(arrivals.map((a) => a.kind)).toEqual([ + "item.started", + "item.completed", + "item.completed", + ]); + // The first event must arrive before the command exits — that's the + // "live" part. Each scheduled chunk is 80ms apart so the third event + // lands at least ~160ms after the first. + const firstToLast = arrivals[2]!.elapsedMs - arrivals[0]!.elapsedMs; + expect(firstToLast).toBeGreaterThanOrEqual(100); + }); + + it("falls back to runCommand when streamingProcess capability is false", async () => { + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "buffered" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-buffered", + harness: "codex", + userPrompt: "fallback", + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + }, + ); + const result = await session.start(); + expect(result.success).toBe(true); + expect(result.result).toBe("buffered"); + // Non-streaming sandboxes still get the harness command through runCommand. + expect(sandbox.commands).toHaveLength(1); + }); + + it("does NOT pipe stdin when interactiveInput is false (default)", async () => { + // Reproduces the codex-hang scenario: many one-shot CLIs block on a + // piped-but-never-closed stdin. The session must default to NOT + // attaching an input iterable. + const streamingSandbox = new StreamingFakeSandbox([ + { + delayMs: 0, + stdout: `${JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "ok" }, + })}\n`, + }, + ]); + const session = await createAgentSession( + { + sessionId: "session-no-stdin", + harness: "codex", + userPrompt: "no stdin please", + }, + { + sandboxProviders: { local: new FakeSandboxProvider(streamingSandbox) }, + }, + ); + // Push messages before start — under no-pipe contract these stay in + // the queue and never reach the fake's stdinChunks. + await session.addMessage("queued-only"); + const result = await session.start(); + expect(result.success).toBe(true); + expect(streamingSandbox.stdinChunks).toEqual([]); + expect(session.getQueuedMessages()).toEqual(["queued-only"]); + }); + + it("routes addMessage into the running process's stdin while streaming", async () => { + const streamingSandbox = new StreamingFakeSandbox([ + { + delayMs: 30, + stdout: `${JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "ack" }, + })}\n`, + }, + ]); + + const session = await createAgentSession( + { + sessionId: "session-stdin", + harness: "codex", + userPrompt: "open a stream", + interactiveInput: true, + }, + { + sandboxProviders: { local: new FakeSandboxProvider(streamingSandbox) }, + }, + ); + + // Kick the session, then push messages while it's streaming. Capture + // what reaches the fake's stdin in real time. + const sessionPromise = session.start(); + // Give the sandbox a moment to begin reading its input iterable. + await new Promise((resolve) => setTimeout(resolve, 10)); + await session.addMessage("hello"); + await session.addMessage("world"); + + const result = await sessionPromise; + expect(result.success).toBe(true); + // Messages should have been delivered to the fake's stdin as + // newline-terminated wire lines, ordered. + expect(streamingSandbox.stdinChunks).toEqual(["hello\n", "world\n"]); + }); + it("materializes sensitive files before setup without exposing contents", async () => { const sandbox = new FakeSandbox( JSON.stringify({ @@ -173,6 +334,91 @@ class FakeSandboxProvider implements SandboxProvider { } } +interface ScheduledChunk { + delayMs: number; + stdout?: string; + stderr?: string; +} + +class StreamingFakeSandbox implements RunnerSandbox { + readonly sandboxId = "fake-stream"; + readonly provider = "local"; + readonly capabilities: RunnerSandboxCapabilities = { + filesystem: true, + runCommand: true, + streamingProcess: true, + }; + readonly filesystem: SandboxFilesystem = { + async readFile() { + return ""; + }, + async writeFile() {}, + async readdir() { + return []; + }, + async mkdir() {}, + async exists() { + return true; + }, + async remove() {}, + }; + readonly stdinChunks: string[] = []; + streamCalls = 0; + runCalls = 0; + + constructor(private readonly schedule: readonly ScheduledChunk[]) {} + + async runCommand(): Promise { + this.runCalls += 1; + return { stdout: "", stderr: "", exitCode: 0, durationMs: 0 }; + } + + async streamCommand( + _command: string, + options: SandboxStreamCommandOptions = {}, + ): Promise { + this.streamCalls += 1; + const startedAt = Date.now(); + + // Drain the input iterable concurrently — fire-and-forget; the caller + // owns the iterable's lifetime and closes it after streamCommand + // returns. Mirrors the local + Daytona contract. + const inputDrainer = options.input + ? (async () => { + for await (const chunk of options.input!) { + this.stdinChunks.push(chunk); + } + })() + : undefined; + inputDrainer?.catch(() => {}); + + let stdoutBuf = ""; + let stderrBuf = ""; + for (const event of this.schedule) { + await new Promise((resolve) => setTimeout(resolve, event.delayMs)); + if (event.stdout) { + stdoutBuf += event.stdout; + options.onStdout?.(event.stdout); + } + if (event.stderr) { + stderrBuf += event.stderr; + options.onStderr?.(event.stderr); + } + } + // Give the input drainer a tick to pick up any messages pushed + // during the schedule before we return. + await new Promise((resolve) => setTimeout(resolve, 10)); + return { + stdout: stdoutBuf, + stderr: stderrBuf, + exitCode: 0, + durationMs: Date.now() - startedAt, + }; + } + + async destroy(): Promise {} +} + class FakeSandbox implements RunnerSandbox { readonly sandboxId = "fake"; readonly provider = "local"; diff --git a/packages/agent-runtime/test/sandbox.test.ts b/packages/agent-runtime/test/sandbox.test.ts index d12726da3..570689f74 100644 --- a/packages/agent-runtime/test/sandbox.test.ts +++ b/packages/agent-runtime/test/sandbox.test.ts @@ -18,7 +18,7 @@ describe("LocalSandboxProvider", () => { expect(sandbox.provider).toBe("local"); expect(sandbox.capabilities.filesystem).toBe(true); expect(sandbox.capabilities.runCommand).toBe(true); - expect(sandbox.capabilities.streamingProcess).toBe(false); + expect(sandbox.capabilities.streamingProcess).toBe(true); await sandbox.filesystem.mkdir("nested"); await sandbox.filesystem.writeFile("nested/hello.txt", "hello"); @@ -44,6 +44,85 @@ describe("LocalSandboxProvider", () => { await rm(root, { recursive: true, force: true }); } }); + + it("feeds an input AsyncIterable into the running process's stdin", async () => { + const root = await mkdtemp(join(tmpdir(), "agent-runtime-local-stdin-")); + try { + const provider = createLocalSandboxProvider({ workingDirectory: root }); + const sandbox = await provider.create({ provider: "local" }); + + // A simple line-echo loop: read lines from stdin, echo each back + // with a "got:" prefix, until stdin closes. + const command = + 'node -e "' + + "const rl = require('readline').createInterface({ input: process.stdin });" + + "rl.on('line', l => console.log('got:', l));" + + "rl.on('close', () => console.log('closed'));" + + '"'; + + // Build an async iterable that yields three lines with delays + // between them — proves stdin chunks land while the process runs. + async function* messages() { + yield "hello\n"; + await new Promise((r) => setTimeout(r, 30)); + yield "world\n"; + await new Promise((r) => setTimeout(r, 30)); + yield "fin\n"; + } + + const result = await sandbox.streamCommand!(command, { + input: messages(), + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("got: hello"); + expect(result.stdout).toContain("got: world"); + expect(result.stdout).toContain("got: fin"); + expect(result.stdout).toContain("closed"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("streams stdout/stderr chunks live via streamCommand", async () => { + const root = await mkdtemp(join(tmpdir(), "agent-runtime-local-stream-")); + try { + const provider = createLocalSandboxProvider({ workingDirectory: root }); + const sandbox = await provider.create({ provider: "local" }); + + expect(sandbox.streamCommand).toBeDefined(); + + const stdoutChunks: Array<{ chunk: string; elapsedMs: number }> = []; + const startedAt = Date.now(); + + // Emit three lines on stdout with a 100ms gap, and one line on + // stderr at the end. If streaming works, we'll see the first chunk + // arrive well before the command exits (~300ms total). + const command = + 'node -e "' + + "setTimeout(() => console.log('one'), 0);" + + "setTimeout(() => console.log('two'), 100);" + + "setTimeout(() => { console.log('three'); console.error('done'); }, 200);" + + '"'; + + const result = await sandbox.streamCommand!(command, { + onStdout: (chunk) => { + stdoutChunks.push({ chunk, elapsedMs: Date.now() - startedAt }); + }, + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("one"); + expect(result.stdout).toContain("two"); + expect(result.stdout).toContain("three"); + expect(result.stderr).toContain("done"); + // We must have observed at least one chunk strictly before exit. + expect(stdoutChunks.length).toBeGreaterThan(0); + expect(stdoutChunks[0]!.elapsedMs).toBeLessThan(result.durationMs); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); }); describe("ComputeSdkSandboxProvider", () => { @@ -163,4 +242,151 @@ describe("ComputeSdkSandboxProvider", () => { ["destroy"], ]); }); + + it("streamCommand drives a Daytona-shaped native sandbox via async sessions", async () => { + const events: string[] = []; + // Synthetic Daytona Process shape — proves that streamCommand dispatches + // to createSession → executeSessionCommand(runAsync) → getSessionCommandLogs + // with live callbacks → getSessionCommand → deleteSession. + const daytonaProcess = { + async createSession(sessionId: string) { + events.push(`createSession:${sessionId}`); + }, + async executeSessionCommand( + sessionId: string, + req: { command: string; runAsync?: boolean }, + ) { + events.push( + `executeSessionCommand:${sessionId}:${req.command}:async=${req.runAsync}`, + ); + return { cmdId: "cmd-42" }; + }, + async getSessionCommandLogs( + _sessionId: string, + _commandId: string, + onStdout: (chunk: string) => void, + onStderr: (chunk: string) => void, + ) { + onStdout("hello\n"); + onStdout("world\n"); + onStderr("warning\n"); + }, + async getSessionCommand(_sessionId: string, _commandId: string) { + return { exitCode: 0 }; + }, + async deleteSession(sessionId: string) { + events.push(`deleteSession:${sessionId}`); + }, + }; + const nativeSandbox = { process: daytonaProcess }; + const fakeSandbox: ComputeSdkSandboxLike = { + sandboxId: "sbx_daytona", + provider: "daytona", + workingDirectory: "/home/daytona", + filesystem: { + async readFile() { + return ""; + }, + async writeFile() {}, + async mkdir() {}, + async readdir() { + return []; + }, + async exists() { + return true; + }, + async remove() {}, + }, + async runCommand() { + return { stdout: "", stderr: "", exitCode: 0, durationMs: 0 }; + }, + getInstance() { + return nativeSandbox; + }, + async destroy() {}, + }; + const compute = { + sandbox: { + async create() { + return fakeSandbox; + }, + }, + }; + const provider = createComputeSdkSandboxProvider({ compute }); + const sandbox = await provider.create({ provider: "daytona" }); + + // Capability flag must surface streaming when getInstance reveals a + // Daytona-shaped native sandbox, even though the wrapped sandbox's + // runCommand alone wouldn't expose it. + expect(sandbox.capabilities.streamingProcess).toBe(true); + expect(sandbox.streamCommand).toBeDefined(); + + const stdoutChunks: string[] = []; + const stderrChunks: string[] = []; + const result = await sandbox.streamCommand!( + "claude -p hi --output-format stream-json", + { + cwd: "/home/daytona", + env: { FOO: "bar" }, + onStdout: (chunk) => stdoutChunks.push(chunk), + onStderr: (chunk) => stderrChunks.push(chunk), + }, + ); + + expect(stdoutChunks).toEqual(["hello\n", "world\n"]); + expect(stderrChunks).toEqual(["warning\n"]); + expect(result.stdout).toBe("hello\nworld\n"); + expect(result.stderr).toBe("warning\n"); + expect(result.exitCode).toBe(0); + + // Verify the orchestration: createSession first, executeSessionCommand + // with runAsync=true and env/cwd folded into the command, deleteSession last. + expect(events[0]).toMatch(/^createSession:agent-runtime-stream-/); + expect(events[1]).toMatch( + /^executeSessionCommand:agent-runtime-stream-[^:]+:cd "\/home\/daytona" && FOO="bar" claude -p hi --output-format stream-json:async=true$/, + ); + expect(events[2]).toMatch(/^deleteSession:agent-runtime-stream-/); + }); + + it("streamCommand rejects when the underlying provider has no streaming primitive", async () => { + const fakeSandbox: ComputeSdkSandboxLike = { + sandboxId: "sbx_nostream", + provider: "blaxel", + filesystem: { + async readFile() { + return ""; + }, + async writeFile() {}, + async mkdir() {}, + async readdir() { + return []; + }, + async exists() { + return false; + }, + async remove() {}, + }, + async runCommand() { + return { stdout: "", stderr: "", exitCode: 0, durationMs: 0 }; + }, + // No getInstance — providers that haven't been wired into the + // streaming primitive should report streamingProcess: false and + // surface a clear error if streamCommand is called anyway. + async destroy() {}, + }; + const compute = { + sandbox: { + async create() { + return fakeSandbox; + }, + }, + }; + const provider = createComputeSdkSandboxProvider({ compute }); + const sandbox = await provider.create({ provider: "blaxel" }); + + expect(sandbox.capabilities.streamingProcess).toBe(false); + await expect(sandbox.streamCommand!("echo hi", {})).rejects.toThrow( + /streaming/i, + ); + }); }); From c91a3cca4a9d18db620e716326c1d3b29dc7a7c0 Mon Sep 17 00:00:00 2001 From: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Date: Fri, 15 May 2026 15:50:49 -0700 Subject: [PATCH 4/6] feat(agent-runtime): folders and repositories as first-class session config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two materialization concepts to `CreateAgentSessionConfig`, deliberately distinct from the existing `volumes` (provider-attached persistent storage): - `RuntimeFolderConfig` — exposes a host filesystem folder inside the sandbox. Walks the host tree and uploads each file via `SandboxFilesystem.writeFile`. Supports `exclude` globs. With `access: "readwrite"` the runtime syncs sandbox edits and any newly-created files back to the host folder after the harness command completes. - `RuntimeRepositoryConfig` — runs `git clone` inside the sandbox at `mountPath` with optional `branch` checkout and `depth` shallow-clone. Local-path sources are rewritten to `file://...` to preserve git semantics. Shallow clones with a branch use `--branch` on the clone itself, since `git checkout` of a non-default branch fails after a shallow clone. Both emit lifecycle transcript events (`folder.materialize.*`, `folder.syncback.*`, `repository.materialize.*`) and run after files but before package setup commands, so setup steps that depend on the cloned tree or the mounted folder see them ready. 27 tests pass (5 new): one materializer unit test per concept and one runtime-level integration test verifying that the session wires each through to the right sandbox calls and emits the right events. --- CHANGELOG.internal.md | 1 + packages/agent-runtime/src/index.ts | 1 + .../src/materializers/folders.ts | 175 ++++++++++++++++++ .../agent-runtime/src/materializers/index.ts | 2 + .../src/materializers/repositories.ts | 103 +++++++++++ packages/agent-runtime/src/schemas.ts | 21 +++ packages/agent-runtime/src/session.ts | 144 ++++++++++++++ packages/agent-runtime/src/types.ts | 67 +++++++ .../agent-runtime/test/materializers.test.ts | 173 +++++++++++++++++ packages/agent-runtime/test/runtime.test.ts | 101 ++++++++++ 10 files changed, 788 insertions(+) create mode 100644 packages/agent-runtime/src/materializers/folders.ts create mode 100644 packages/agent-runtime/src/materializers/index.ts create mode 100644 packages/agent-runtime/src/materializers/repositories.ts create mode 100644 packages/agent-runtime/test/materializers.test.ts diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index 585bf3688..7fa27b396 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -7,6 +7,7 @@ This changelog documents internal development changes, refactors, tooling update ### Added - Added `cyrus-agent-runtime`, a standalone experimental TypeScript package for unified agent session orchestration across harnesses and sandbox providers. It includes normalized session config, transcript envelopes, local and ComputeSDK-backed sandbox abstractions, harness adapters for Claude/Codex/Cursor/Gemini plus provisional PI/OpenCode adapters, and focused tests for config, runtime lifecycle, sandbox execution, and transcript parsing. - Added live process streaming to `cyrus-agent-runtime`. New optional `RunnerSandbox.streamCommand(command, options)` capability surfaces stdout/stderr chunks to callbacks as they arrive, with `signal: AbortSignal` for cancellation and `input: AsyncIterable` for live stdin. Implemented natively in `LocalSandboxProvider` (via `child_process.spawn`) and for Daytona inside `ComputeSdkSandboxProvider` via a pluggable `NativeStreamAdapter` registry that reaches the underlying `@daytonaio/sdk` Sandbox through ComputeSDK's `ProviderSandbox.getInstance()` escape hatch, using async sessions + `getSessionCommandLogs(onStdout, onStderr)`. User-supplied adapters can be registered via `ComputeSdkSandboxProviderOptions.nativeStreamAdapters` for ComputeSDK providers we don't bundle (E2B, Vercel, Blaxel, Modal, Railway, Runloop, Cloudflare, Codesandbox). `RuntimeAgentSession.start()` now prefers `streamCommand` when `capabilities.streamingProcess` is true, line-buffers chunks across packet boundaries, and emits `TranscriptEvent`s live as the harness CLI produces them. New `CreateAgentSessionConfig.interactiveInput` opt-in flag routes `addMessage()` chunks into the running process's stdin (most one-shot CLIs hang on piped-but-never-closed stdin, so this defaults off). Verified end-to-end against real `codex exec` (events emitted ~8.6s before turn end), the local `child_process.spawn` path (chunks landed at the exact 400ms cadence the child produced them), and real Daytona Claude `stream-json` (system event landed 1.7s before result event over a remote sandbox). +- Added `folders` and `repositories` to the `cyrus-agent-runtime` session config — two new materialization concepts that are deliberately distinct from existing `volumes`. `RuntimeFolderConfig` exposes a host filesystem folder inside the sandbox (walks the host tree, uploads each file via `SandboxFilesystem.writeFile`, supports an `exclude` glob list) and with `access: "readwrite"` syncs sandbox edits and any newly-created files back to the host folder after the harness command completes. `RuntimeRepositoryConfig` runs `git clone` inside the sandbox at `mountPath` with optional `branch` checkout and `depth` shallow-clone; local-path sources are converted to `file://...` to preserve git semantics, and shallow clones with a branch use `--branch` on the clone itself (since `git checkout` of a non-default branch fails on a shallow clone). Both emit lifecycle transcript events (`folder.materialize.started/completed/failed`, `folder.syncback.started/completed/failed`, `repository.materialize.started/completed/failed`) and run before the package setup commands so any setup that depends on the cloned tree or the mounted folder sees them ready. ## [0.2.50] - 2026-04-30 diff --git a/packages/agent-runtime/src/index.ts b/packages/agent-runtime/src/index.ts index 04a42291f..0942c1acc 100644 --- a/packages/agent-runtime/src/index.ts +++ b/packages/agent-runtime/src/index.ts @@ -1,4 +1,5 @@ export * from "./harnesses/index.js"; +export * from "./materializers/index.js"; export * from "./runtime.js"; export * from "./sandbox/index.js"; export * from "./schemas.js"; diff --git a/packages/agent-runtime/src/materializers/folders.ts b/packages/agent-runtime/src/materializers/folders.ts new file mode 100644 index 000000000..8346a6673 --- /dev/null +++ b/packages/agent-runtime/src/materializers/folders.ts @@ -0,0 +1,175 @@ +import { readdir, readFile, stat } from "node:fs/promises"; +import { isAbsolute, join, posix, relative, resolve, sep } from "node:path"; +import type { + RunnerSandbox, + RuntimeFolderConfig, + SandboxFileEntry, +} from "../types.js"; + +/** + * Walk a host directory and upload every regular file into the sandbox at + * `mountPath`. Skips entries whose source-relative path matches any glob + * in `exclude`. Returns the list of files materialized (sandbox-relative + * paths) so the caller can sync-back exactly those entries later. + */ +export async function materializeFolderIntoSandbox( + folder: RuntimeFolderConfig, + sandbox: RunnerSandbox, +): Promise<{ filesWritten: string[]; bytes: number }> { + const sourceAbsolute = resolveHostPath(folder.source); + const sourceStat = await stat(sourceAbsolute); + if (!sourceStat.isDirectory()) { + throw new Error( + `RuntimeFolderConfig.source must be a directory: ${folder.source}`, + ); + } + await sandbox.filesystem.mkdir(folder.mountPath); + const filesWritten: string[] = []; + let bytes = 0; + for await (const entry of walkFiles(sourceAbsolute, folder.exclude ?? [])) { + const sandboxPath = joinSandboxPath(folder.mountPath, entry.relativePath); + const dir = sandboxDirname(sandboxPath); + if (dir) await sandbox.filesystem.mkdir(dir); + const content = await readFile(entry.absolutePath, "utf8"); + await sandbox.filesystem.writeFile(sandboxPath, content); + filesWritten.push(sandboxPath); + bytes += content.length; + } + return { filesWritten, bytes }; +} + +/** + * For an `access: "readwrite"` folder, walk the sandbox tree under + * `mountPath` and write each file back to its host counterpart under + * `source`. The set of files synced back is the union of `originalFiles` + * (everything we wrote in) and anything new the sandbox produced under + * `mountPath` — this picks up files the agent created during the run. + * + * Returns the list of host paths written. + */ +export async function syncFolderBackToHost( + folder: RuntimeFolderConfig, + sandbox: RunnerSandbox, + originalFiles: readonly string[], +): Promise<{ filesWritten: string[]; bytes: number }> { + const { writeFile, mkdir } = await import("node:fs/promises"); + const sourceAbsolute = resolveHostPath(folder.source); + const remoteFiles = new Set(); + await walkSandbox(sandbox, folder.mountPath, "", (path) => { + remoteFiles.add(path); + }); + for (const f of originalFiles) remoteFiles.add(f); + + const filesWritten: string[] = []; + let bytes = 0; + for (const sandboxPath of remoteFiles) { + const relativeToMount = sandboxRelative(folder.mountPath, sandboxPath); + if (!relativeToMount) continue; + const hostPath = join(sourceAbsolute, relativeToMount); + let content: string; + try { + content = await sandbox.filesystem.readFile(sandboxPath); + } catch { + // File may have been deleted in-sandbox; skip. + continue; + } + await mkdir(hostDirname(hostPath), { recursive: true }); + await writeFile(hostPath, content); + filesWritten.push(hostPath); + bytes += content.length; + } + return { filesWritten, bytes }; +} + +async function* walkFiles( + root: string, + excludes: readonly string[], + prefix = "", +): AsyncGenerator<{ absolutePath: string; relativePath: string }> { + const entries = await readdir(root, { withFileTypes: true }); + for (const entry of entries) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (excludes.some((pattern) => matchesGlob(rel, pattern))) continue; + const absolutePath = join(root, entry.name); + if (entry.isDirectory()) { + yield* walkFiles(absolutePath, excludes, rel); + } else if (entry.isFile()) { + yield { absolutePath, relativePath: rel }; + } + } +} + +async function walkSandbox( + sandbox: RunnerSandbox, + root: string, + prefix: string, + visit: (sandboxPath: string) => void, +): Promise { + let entries: SandboxFileEntry[]; + try { + entries = await sandbox.filesystem.readdir( + prefix ? joinSandboxPath(root, prefix) : root, + ); + } catch { + return; + } + for (const entry of entries) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + const sandboxPath = joinSandboxPath(root, rel); + if (entry.type === "directory") { + await walkSandbox(sandbox, root, rel, visit); + } else { + visit(sandboxPath); + } + } +} + +/** + * Minimal glob matcher — supports `*` (any segment chars) and `**` (across + * segments). Intentionally lightweight so we don't add a glob dep. + */ +function matchesGlob(path: string, pattern: string): boolean { + const re = new RegExp( + `^${pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*\*/g, "::DOUBLESTAR::") + .replace(/\*/g, "[^/]*") + .replace(/::DOUBLESTAR::/g, ".*")}$`, + ); + return re.test(path); +} + +function resolveHostPath(path: string): string { + return isAbsolute(path) ? path : resolve(process.cwd(), path); +} + +function joinSandboxPath(base: string, sub: string): string { + if (!sub) return base; + return base.endsWith("/") ? `${base}${sub}` : `${base}/${sub}`; +} + +function sandboxDirname(path: string): string | undefined { + const slash = path.lastIndexOf("/"); + if (slash <= 0) return undefined; + return path.slice(0, slash); +} + +function sandboxRelative(mountPath: string, sandboxPath: string): string { + const trimmed = sandboxPath.startsWith(`${mountPath}/`) + ? sandboxPath.slice(mountPath.length + 1) + : sandboxPath === mountPath + ? "" + : sandboxPath; + return trimmed; +} + +function hostDirname(path: string): string { + const slash = path.lastIndexOf(sep); + if (slash <= 0) return path; + return path.slice(0, slash); +} + +// Reference posix/relative to keep TypeScript from removing the imports when +// only used inside helpers above (in case node:path types tighten further). +void posix; +void relative; diff --git a/packages/agent-runtime/src/materializers/index.ts b/packages/agent-runtime/src/materializers/index.ts new file mode 100644 index 000000000..f556dc441 --- /dev/null +++ b/packages/agent-runtime/src/materializers/index.ts @@ -0,0 +1,2 @@ +export * from "./folders.js"; +export * from "./repositories.js"; diff --git a/packages/agent-runtime/src/materializers/repositories.ts b/packages/agent-runtime/src/materializers/repositories.ts new file mode 100644 index 000000000..22795ec26 --- /dev/null +++ b/packages/agent-runtime/src/materializers/repositories.ts @@ -0,0 +1,103 @@ +import { isAbsolute, resolve } from "node:path"; +import type { + CommandExecutionResult, + RunnerSandbox, + RuntimeRepositoryConfig, +} from "../types.js"; + +export interface MaterializeRepositoryResult { + source: string; + resolvedSource: string; + mountPath: string; + branch?: string; + depth?: number; + cloneStdout: string; + cloneStderr: string; + checkoutStdout?: string; + checkoutStderr?: string; + exitCode: number; +} + +/** + * Run `git clone` inside the sandbox to materialize the working tree at + * `mountPath`. Local-path sources are rewritten to `file://...` so git + * preserves repository semantics rather than collapsing them. If `branch` + * is set, a follow-up `git -C checkout ` runs so + * non-default refs (including tags and SHAs) work uniformly. + * + * Authentication is delegated to the sandbox: the runtime does not inject + * credentials. Callers who need auth should supply a tokenized HTTPS URL + * in `source` or pre-materialize an SSH config / GIT_ASKPASS helper via + * `files` and `env`. + */ +export async function materializeRepositoryIntoSandbox( + repository: RuntimeRepositoryConfig, + sandbox: RunnerSandbox, + commandEnv: Record = {}, +): Promise { + const resolvedSource = resolveSource(repository.source); + const access = repository.access ?? "readwrite"; + const depth = repository.depth ?? (access === "read" ? 1 : undefined); + + // When the clone is shallow (`--depth N`), a post-clone `git checkout` of + // a non-default branch fails — only the default branch's history is + // fetched. So in that case we steer the clone with `--branch ` and + // skip the separate checkout. With a full clone, the post-clone checkout + // path still handles tags and arbitrary SHAs that `--branch` rejects. + const useBranchOnClone = Boolean(repository.branch && depth !== undefined); + + const cloneParts = ["git", "clone"]; + if (depth !== undefined) cloneParts.push("--depth", String(depth)); + if (useBranchOnClone && repository.branch) { + cloneParts.push("--branch", shellQuote(repository.branch)); + } + cloneParts.push(shellQuote(resolvedSource), shellQuote(repository.mountPath)); + const cloneCommand = cloneParts.join(" "); + + const cloneResult = await sandbox.runCommand(cloneCommand, { + env: commandEnv, + }); + if (cloneResult.exitCode !== 0) { + throw new Error( + `git clone failed for ${repository.source} (exit ${cloneResult.exitCode}): ${cloneResult.stderr}`, + ); + } + + let checkoutResult: CommandExecutionResult | undefined; + if (repository.branch && !useBranchOnClone) { + checkoutResult = await sandbox.runCommand( + `git -C ${shellQuote(repository.mountPath)} checkout ${shellQuote(repository.branch)}`, + { env: commandEnv }, + ); + if (checkoutResult.exitCode !== 0) { + throw new Error( + `git checkout ${repository.branch} failed (exit ${checkoutResult.exitCode}): ${checkoutResult.stderr}`, + ); + } + } + + return { + source: repository.source, + resolvedSource, + mountPath: repository.mountPath, + branch: repository.branch, + depth, + cloneStdout: cloneResult.stdout, + cloneStderr: cloneResult.stderr, + checkoutStdout: checkoutResult?.stdout, + checkoutStderr: checkoutResult?.stderr, + exitCode: 0, + }; +} + +function resolveSource(source: string): string { + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(source)) return source; + if (source.startsWith("git@")) return source; + const absolute = isAbsolute(source) ? source : resolve(process.cwd(), source); + return `file://${absolute}`; +} + +function shellQuote(value: string): string { + if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) return value; + return `'${value.replaceAll("'", "'\\''")}'`; +} diff --git a/packages/agent-runtime/src/schemas.ts b/packages/agent-runtime/src/schemas.ts index f1554a0a7..a4e6a242c 100644 --- a/packages/agent-runtime/src/schemas.ts +++ b/packages/agent-runtime/src/schemas.ts @@ -96,6 +96,27 @@ export const CreateAgentSessionConfigSchema = z.object({ }), ) .optional(), + folders: z + .array( + z.object({ + source: z.string().min(1), + mountPath: z.string().min(1), + access: z.enum(["read", "readwrite"]).optional(), + exclude: z.array(z.string()).optional(), + }), + ) + .optional(), + repositories: z + .array( + z.object({ + source: z.string().min(1), + mountPath: z.string().min(1), + branch: z.string().min(1).optional(), + access: z.enum(["read", "readwrite"]).optional(), + depth: z.number().int().positive().optional(), + }), + ) + .optional(), mcps: z.record(z.string(), z.unknown()).optional(), permissions: z .object({ diff --git a/packages/agent-runtime/src/session.ts b/packages/agent-runtime/src/session.ts index c9ccad7e9..6d80d4046 100644 --- a/packages/agent-runtime/src/session.ts +++ b/packages/agent-runtime/src/session.ts @@ -1,5 +1,10 @@ import { EventEmitter } from "node:events"; import { dirname } from "node:path"; +import { + materializeFolderIntoSandbox, + materializeRepositoryIntoSandbox, + syncFolderBackToHost, +} from "./materializers/index.js"; import type { AgentSession, AgentSessionResult, @@ -7,6 +12,7 @@ import type { NormalizedAgentSessionConfig, RunnerSandbox, RuntimeCallbacks, + RuntimeFolderConfig, TranscriptEvent, } from "./types.js"; @@ -89,6 +95,14 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { private streamingActive = false; private stopped = false; private started = false; + /** + * Per-readwrite-folder ledger of files we materialized in, so sync-back + * can re-read them even if the agent didn't touch them. + */ + private readonly folderLedger = new Map< + RuntimeFolderConfig, + readonly string[] + >(); constructor( private readonly config: NormalizedAgentSessionConfig, @@ -122,6 +136,8 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { try { await this.materializeFiles(); + await this.materializeFolders(); + await this.materializeRepositories(); await this.runSetupCommands(); const canStream = @@ -193,6 +209,8 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { this.streamingActive = false; this.inputBuffer.close(); + await this.syncFoldersBack(); + const runtimeResult: AgentSessionResult = { sessionId: this.sessionId, harness: this.harness, @@ -317,6 +335,132 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { } } + private async materializeFolders(): Promise { + for (const folder of this.config.folders ?? []) { + const access = folder.access ?? "read"; + await this.emitEvent( + this.createEvent("folder.materialize.started", { + source: folder.source, + mountPath: folder.mountPath, + access, + exclude: folder.exclude, + }), + ); + try { + const result = await materializeFolderIntoSandbox(folder, this.sandbox); + if (access === "readwrite") { + this.folderLedger.set(folder, result.filesWritten); + } + await this.emitEvent( + this.createEvent("folder.materialize.completed", { + source: folder.source, + mountPath: folder.mountPath, + access, + filesWritten: result.filesWritten.length, + bytes: result.bytes, + }), + ); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + await this.emitEvent( + this.createEvent("folder.materialize.failed", { + source: folder.source, + mountPath: folder.mountPath, + access, + error: err.message, + }), + ); + throw err; + } + } + } + + private async materializeRepositories(): Promise { + const env = { + ...this.config.env, + ...this.materializeSecrets(), + }; + for (const repo of this.config.repositories ?? []) { + const access = repo.access ?? "readwrite"; + await this.emitEvent( + this.createEvent("repository.materialize.started", { + source: repo.source, + mountPath: repo.mountPath, + branch: repo.branch, + access, + depth: repo.depth, + }), + ); + try { + const result = await materializeRepositoryIntoSandbox( + repo, + this.sandbox, + env, + ); + await this.emitEvent( + this.createEvent("repository.materialize.completed", { + source: repo.source, + mountPath: repo.mountPath, + branch: repo.branch, + access, + depth: result.depth, + resolvedSource: result.resolvedSource, + }), + ); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + await this.emitEvent( + this.createEvent("repository.materialize.failed", { + source: repo.source, + mountPath: repo.mountPath, + branch: repo.branch, + access, + error: err.message, + }), + ); + throw err; + } + } + } + + private async syncFoldersBack(): Promise { + for (const [folder, originalFiles] of this.folderLedger.entries()) { + await this.emitEvent( + this.createEvent("folder.syncback.started", { + source: folder.source, + mountPath: folder.mountPath, + }), + ); + try { + const result = await syncFolderBackToHost( + folder, + this.sandbox, + originalFiles, + ); + await this.emitEvent( + this.createEvent("folder.syncback.completed", { + source: folder.source, + mountPath: folder.mountPath, + filesWritten: result.filesWritten.length, + bytes: result.bytes, + }), + ); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + await this.emitEvent( + this.createEvent("folder.syncback.failed", { + source: folder.source, + mountPath: folder.mountPath, + error: err.message, + }), + ); + // Sync-back failures are non-fatal — the agent's work in-sandbox + // already completed; we surface the error in the transcript and + // keep going. + } + } + } + private async runSetupCommands(): Promise { const commands = [ ...(this.config.packages?.system?.map( diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts index 7fccff7b4..3fb885b0d 100644 --- a/packages/agent-runtime/src/types.ts +++ b/packages/agent-runtime/src/types.ts @@ -46,6 +46,71 @@ export interface RuntimeFileConfig { sensitive?: boolean; } +/** + * Access mode for folders and repositories materialized into the sandbox. + * - `"read"`: the runtime makes the contents available; changes inside the + * sandbox are not propagated back to the source. + * - `"readwrite"`: the runtime makes the contents available and syncs + * changes inside the sandbox back to the source after the harness + * command completes (folders) or leaves them ready for an explicit push + * (repositories). + */ +export type RuntimeAccessMode = "read" | "readwrite"; + +/** + * Materialize a host filesystem folder into the sandbox. For local + * sandboxes this is a directory copy; for remote sandboxes (e.g. Daytona) + * the runtime walks the host tree and uploads each file via + * {@link SandboxFilesystem.writeFile}. With `access: "readwrite"` the + * runtime syncs changes from the sandbox back to the host after the + * harness command completes — useful for dev loops where the user wants + * to see the agent's edits on their disk. + * + * Conceptually distinct from {@link RuntimeVolumeConfig} (provider-attached + * persistent storage) and {@link RuntimeRepositoryConfig} (git-driven + * trees with branch awareness). + */ +export interface RuntimeFolderConfig { + /** Absolute or runtime-relative host path to expose. */ + source: string; + /** Where in the sandbox to materialize the folder contents. */ + mountPath: string; + /** Default: `"read"`. */ + access?: RuntimeAccessMode; + /** Glob patterns (relative to source) to skip during copy/sync. */ + exclude?: string[]; +} + +/** + * Materialize a git repository into the sandbox. The runtime runs + * `git clone ` inside the sandbox (so credentials, + * proxies, and CA bundles are inherited from the sandbox env) and, if + * `branch` is set, checks out that ref. With `access: "readwrite"` the + * working tree is left configured for push; with `"read"` the clone is + * shallow by default and push is not expected. + */ +export interface RuntimeRepositoryConfig { + /** + * Git URL (HTTPS or SSH) or local path. Local paths are cloned via + * `file://` to preserve git semantics rather than naive copy. + */ + source: string; + /** Where in the sandbox to clone the working tree. */ + mountPath: string; + /** + * Optional ref to check out after clone. Branch, tag, or commit SHA. + * Defaults to remote HEAD. + */ + branch?: string; + /** Default: `"readwrite"`. */ + access?: RuntimeAccessMode; + /** + * Optional shallow-clone depth. Defaults to `1` for `access: "read"` + * and unset (full clone) for `access: "readwrite"`. + */ + depth?: number; +} + export interface RuntimeVolumeConfig { name: string; mountPath: string; @@ -97,6 +162,8 @@ export interface CreateAgentSessionConfig { secrets?: Record; packages?: RuntimePackageConfig; files?: RuntimeFileConfig[]; + folders?: RuntimeFolderConfig[]; + repositories?: RuntimeRepositoryConfig[]; mcps?: Record; permissions?: RuntimePermissionConfig; memory?: RuntimeMemoryConfig; diff --git a/packages/agent-runtime/test/materializers.test.ts b/packages/agent-runtime/test/materializers.test.ts new file mode 100644 index 000000000..5e66ea66e --- /dev/null +++ b/packages/agent-runtime/test/materializers.test.ts @@ -0,0 +1,173 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + materializeFolderIntoSandbox, + materializeRepositoryIntoSandbox, + syncFolderBackToHost, +} from "../src/materializers/index.js"; +import { createLocalSandboxProvider } from "../src/sandbox/index.js"; + +describe("materializeFolderIntoSandbox", () => { + it("walks the host folder and uploads each file into the sandbox", async () => { + const host = await mkdtemp(join(tmpdir(), "agent-runtime-folder-host-")); + const sandboxRoot = await mkdtemp( + join(tmpdir(), "agent-runtime-folder-sbx-"), + ); + try { + await mkdir(join(host, "nested"), { recursive: true }); + await writeFile(join(host, "top.txt"), "alpha"); + await writeFile(join(host, "nested", "deep.txt"), "beta"); + await writeFile(join(host, "skip.tmp"), "should-not-appear"); + + const sandbox = await createLocalSandboxProvider({ + workingDirectory: sandboxRoot, + }).create({ provider: "local" }); + const mount = join(sandboxRoot, "work"); + + const result = await materializeFolderIntoSandbox( + { + source: host, + mountPath: mount, + access: "read", + exclude: ["*.tmp"], + }, + sandbox, + ); + + expect(result.filesWritten.sort()).toEqual( + [`${mount}/nested/deep.txt`, `${mount}/top.txt`].sort(), + ); + await expect( + sandbox.filesystem.readFile(`${mount}/top.txt`), + ).resolves.toBe("alpha"); + await expect( + sandbox.filesystem.readFile(`${mount}/nested/deep.txt`), + ).resolves.toBe("beta"); + await expect( + sandbox.filesystem.exists(`${mount}/skip.tmp`), + ).resolves.toBe(false); + } finally { + await rm(host, { recursive: true, force: true }); + await rm(sandboxRoot, { recursive: true, force: true }); + } + }); +}); + +describe("syncFolderBackToHost", () => { + it("syncs sandbox edits and new files back to the host folder", async () => { + const host = await mkdtemp(join(tmpdir(), "agent-runtime-rw-host-")); + const sandboxRoot = await mkdtemp(join(tmpdir(), "agent-runtime-rw-sbx-")); + try { + await writeFile(join(host, "before.txt"), "original"); + + const sandbox = await createLocalSandboxProvider({ + workingDirectory: sandboxRoot, + }).create({ provider: "local" }); + const mount = join(sandboxRoot, "work"); + + const folder = { + source: host, + mountPath: mount, + access: "readwrite" as const, + }; + const materialized = await materializeFolderIntoSandbox(folder, sandbox); + expect(materialized.filesWritten).toContain(`${mount}/before.txt`); + + // Simulate an agent editing an existing file and creating a new one. + await sandbox.filesystem.writeFile(`${mount}/before.txt`, "edited"); + await sandbox.filesystem.writeFile(`${mount}/created.txt`, "fresh"); + + const result = await syncFolderBackToHost( + folder, + sandbox, + materialized.filesWritten, + ); + + // before.txt should have been overwritten; created.txt should appear. + await expect(readFile(join(host, "before.txt"), "utf8")).resolves.toBe( + "edited", + ); + await expect(readFile(join(host, "created.txt"), "utf8")).resolves.toBe( + "fresh", + ); + // At minimum both must have been synced. + expect(result.filesWritten.length).toBeGreaterThanOrEqual(2); + } finally { + await rm(host, { recursive: true, force: true }); + await rm(sandboxRoot, { recursive: true, force: true }); + } + }); +}); + +describe("materializeRepositoryIntoSandbox", () => { + it("clones a local git repo at the requested branch", async () => { + // Build a tiny upstream repo on disk, then clone it via the sandbox. + const upstreamRoot = await mkdtemp( + join(tmpdir(), "agent-runtime-repo-upstream-"), + ); + const sandboxRoot = await mkdtemp( + join(tmpdir(), "agent-runtime-repo-sbx-"), + ); + try { + const localSandboxFactory = createLocalSandboxProvider({ + workingDirectory: sandboxRoot, + }); + const setupSandbox = await localSandboxFactory.create({ + provider: "local", + workingDirectory: upstreamRoot, + }); + const run = async (command: string) => { + const r = await setupSandbox.runCommand(command, { + cwd: upstreamRoot, + }); + if (r.exitCode !== 0) { + throw new Error( + `${command} failed (${r.exitCode}): ${r.stderr || r.stdout}`, + ); + } + return r; + }; + await run("git init -q -b main"); + await run("git config user.email test@example.com"); + await run("git config user.name Test"); + await writeFile(join(upstreamRoot, "README.md"), "hello main\n"); + await run("git add README.md"); + await run("git commit -q -m main-commit"); + await run("git checkout -q -b feature"); + await writeFile(join(upstreamRoot, "feature.txt"), "branch content\n"); + await run("git add feature.txt"); + await run("git commit -q -m feature-commit"); + await run("git checkout -q main"); + + const sandbox = await localSandboxFactory.create({ provider: "local" }); + + const result = await materializeRepositoryIntoSandbox( + { + source: upstreamRoot, + mountPath: join(sandboxRoot, "clone"), + branch: "feature", + access: "read", + }, + sandbox, + ); + + expect(result.exitCode).toBe(0); + expect(result.resolvedSource).toBe(`file://${upstreamRoot}`); + expect(result.depth).toBe(1); + // Working tree should reflect the requested branch. + await expect( + readFile(join(sandboxRoot, "clone", "feature.txt"), "utf8"), + ).resolves.toBe("branch content\n"); + // Default-branch sentinel file should also be present (clone preserves + // it on the feature branch). + await expect( + readFile(join(sandboxRoot, "clone", "README.md"), "utf8"), + ).resolves.toBe("hello main\n"); + } finally { + await rm(upstreamRoot, { recursive: true, force: true }); + await rm(sandboxRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/agent-runtime/test/runtime.test.ts b/packages/agent-runtime/test/runtime.test.ts index 433e30f4c..3393952a0 100644 --- a/packages/agent-runtime/test/runtime.test.ts +++ b/packages/agent-runtime/test/runtime.test.ts @@ -1,3 +1,6 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { createAgentSession, normalizeConfig } from "../src/runtime.js"; import type { @@ -276,6 +279,104 @@ describe("AgentRuntime", () => { expect(streamingSandbox.stdinChunks).toEqual(["hello\n", "world\n"]); }); + it("materializes folders and syncs read-write edits back to the host", async () => { + // End-to-end through createAgentSession with a real local sandbox: + // host folder is uploaded, setup commands stand in for an agent's + // edits, and syncFoldersBack writes them back to the host. The + // harness is set to `true` so the "session" itself is a no-op. + const host = await mkdtemp(join(tmpdir(), "agent-runtime-rt-folder-")); + const sandboxRoot = await mkdtemp( + join(tmpdir(), "agent-runtime-rt-folder-sbx-"), + ); + try { + await writeFile(join(host, "input.txt"), "before"); + const mount = join(sandboxRoot, "work"); + + const session = await createAgentSession({ + sessionId: "session-folder", + harness: { kind: "codex", command: "true" }, + userPrompt: "edit files please", + sandbox: { provider: "local", workingDirectory: sandboxRoot }, + folders: [{ source: host, mountPath: mount, access: "readwrite" }], + packages: { + // These setup commands stand in for what an agent would do + // during the run: edit one file, create another. + commands: [ + `sh -c 'printf after > ${mount}/input.txt'`, + `sh -c 'printf created > ${mount}/new.txt'`, + ], + }, + }); + + const result = await session.start(); + expect(result.success).toBe(true); + + const kinds = result.events.map((e) => e.kind); + expect(kinds).toContain("folder.materialize.started"); + expect(kinds).toContain("folder.materialize.completed"); + expect(kinds).toContain("folder.syncback.started"); + expect(kinds).toContain("folder.syncback.completed"); + + await expect(readFile(join(host, "input.txt"), "utf8")).resolves.toBe( + "after", + ); + await expect(readFile(join(host, "new.txt"), "utf8")).resolves.toBe( + "created", + ); + } finally { + await rm(host, { recursive: true, force: true }); + await rm(sandboxRoot, { recursive: true, force: true }); + } + }); + + it("routes repository config through git-clone/checkout commands and emits lifecycle events", async () => { + // Session-level wiring test: verify that declaring `repositories` + // causes the runtime to invoke `git clone` (and `git checkout` when a + // branch is set) on the sandbox, before the harness command runs, with + // the right env. Real git behavior is covered by materializers.test.ts. + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "cloned" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-repo", + harness: "codex", + userPrompt: "clone please", + repositories: [ + { + source: "/tmp/upstream", + mountPath: "/work/repo", + branch: "feature", + access: "read", + }, + ], + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const result = await session.start(); + expect(result.success).toBe(true); + + const kinds = result.events.map((e) => e.kind); + expect(kinds).toContain("repository.materialize.started"); + expect(kinds).toContain("repository.materialize.completed"); + + const commands = sandbox.commands.map((c) => c.command); + // Shallow clones (depth=1 because access:"read") steer with --branch + // on the clone itself, because a post-clone `git checkout` of a + // non-default branch fails when only one branch's history is fetched. + expect(commands[0]).toBe( + "git clone --depth 1 --branch feature file:///tmp/upstream /work/repo", + ); + // Harness command runs after the repo command. + expect(commands.at(-1)).toBe( + "codex exec --json --skip-git-repo-check 'clone please'", + ); + }); + it("materializes sensitive files before setup without exposing contents", async () => { const sandbox = new FakeSandbox( JSON.stringify({ From a54d9d83dc381daa464c533e671715b541d1e6e6 Mon Sep 17 00:00:00 2001 From: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Date: Fri, 15 May 2026 15:54:58 -0700 Subject: [PATCH 5/6] feat(agent-runtime): add destroy() to AgentSessionResult MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Equates to ComputeSDK's ProviderSandbox.destroy() for ComputeSDK-backed providers (deletes the remote sandbox, releases compute resources) and is a no-op for the local provider. Lets a caller hold only the result object, consume events/result, then tear down without keeping a reference to the session. Idempotent — backed by a one-shot destroy promise on the session that both `AgentSession.stop()` and `AgentSessionResult.destroy()` share, so callers can call either or both in any order without double-destroying the underlying ComputeSDK / local sandbox. Verified with a new test that asserts: - the returned result exposes destroy() - calling result.destroy() invokes sandbox.destroy() exactly once - calling result.destroy() twice is a no-op the second time - calling session.stop() after result.destroy() does not double-destroy --- CHANGELOG.internal.md | 1 + packages/agent-runtime/src/session.ts | 28 ++++++++++++- packages/agent-runtime/src/types.ts | 9 +++++ packages/agent-runtime/test/runtime.test.ts | 45 ++++++++++++++++++++- 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index 7fa27b396..6fd64db59 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -8,6 +8,7 @@ This changelog documents internal development changes, refactors, tooling update - Added `cyrus-agent-runtime`, a standalone experimental TypeScript package for unified agent session orchestration across harnesses and sandbox providers. It includes normalized session config, transcript envelopes, local and ComputeSDK-backed sandbox abstractions, harness adapters for Claude/Codex/Cursor/Gemini plus provisional PI/OpenCode adapters, and focused tests for config, runtime lifecycle, sandbox execution, and transcript parsing. - Added live process streaming to `cyrus-agent-runtime`. New optional `RunnerSandbox.streamCommand(command, options)` capability surfaces stdout/stderr chunks to callbacks as they arrive, with `signal: AbortSignal` for cancellation and `input: AsyncIterable` for live stdin. Implemented natively in `LocalSandboxProvider` (via `child_process.spawn`) and for Daytona inside `ComputeSdkSandboxProvider` via a pluggable `NativeStreamAdapter` registry that reaches the underlying `@daytonaio/sdk` Sandbox through ComputeSDK's `ProviderSandbox.getInstance()` escape hatch, using async sessions + `getSessionCommandLogs(onStdout, onStderr)`. User-supplied adapters can be registered via `ComputeSdkSandboxProviderOptions.nativeStreamAdapters` for ComputeSDK providers we don't bundle (E2B, Vercel, Blaxel, Modal, Railway, Runloop, Cloudflare, Codesandbox). `RuntimeAgentSession.start()` now prefers `streamCommand` when `capabilities.streamingProcess` is true, line-buffers chunks across packet boundaries, and emits `TranscriptEvent`s live as the harness CLI produces them. New `CreateAgentSessionConfig.interactiveInput` opt-in flag routes `addMessage()` chunks into the running process's stdin (most one-shot CLIs hang on piped-but-never-closed stdin, so this defaults off). Verified end-to-end against real `codex exec` (events emitted ~8.6s before turn end), the local `child_process.spawn` path (chunks landed at the exact 400ms cadence the child produced them), and real Daytona Claude `stream-json` (system event landed 1.7s before result event over a remote sandbox). - Added `folders` and `repositories` to the `cyrus-agent-runtime` session config — two new materialization concepts that are deliberately distinct from existing `volumes`. `RuntimeFolderConfig` exposes a host filesystem folder inside the sandbox (walks the host tree, uploads each file via `SandboxFilesystem.writeFile`, supports an `exclude` glob list) and with `access: "readwrite"` syncs sandbox edits and any newly-created files back to the host folder after the harness command completes. `RuntimeRepositoryConfig` runs `git clone` inside the sandbox at `mountPath` with optional `branch` checkout and `depth` shallow-clone; local-path sources are converted to `file://...` to preserve git semantics, and shallow clones with a branch use `--branch` on the clone itself (since `git checkout` of a non-default branch fails on a shallow clone). Both emit lifecycle transcript events (`folder.materialize.started/completed/failed`, `folder.syncback.started/completed/failed`, `repository.materialize.started/completed/failed`) and run before the package setup commands so any setup that depends on the cloned tree or the mounted folder sees them ready. +- Added `destroy()` to `AgentSessionResult` in `cyrus-agent-runtime` — equates to ComputeSDK's `ProviderSandbox.destroy()` for ComputeSDK-backed providers (deletes the remote sandbox, releases compute resources) and is a no-op for the local provider. Idempotent and shared with `AgentSession.stop()`, so callers can release the sandbox via either method (or both) without double-destroying. Lets consumers hold only the result, consume the events/result, and tear down without keeping a session reference. ## [0.2.50] - 2026-04-30 diff --git a/packages/agent-runtime/src/session.ts b/packages/agent-runtime/src/session.ts index 6d80d4046..6c7acf71b 100644 --- a/packages/agent-runtime/src/session.ts +++ b/packages/agent-runtime/src/session.ts @@ -95,6 +95,8 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { private streamingActive = false; private stopped = false; private started = false; + private sandboxDestroyed = false; + private sandboxDestroyPromise?: Promise; /** * Per-readwrite-folder ledger of files we materialized in, so sync-back * can re-read them even if the agent didn't touch them. @@ -218,6 +220,7 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { exitCode, result: this.adapter.extractResult?.(this.observedEvents), events: [...this.observedEvents], + destroy: () => this.destroySandboxOnce(), }; this.eventBuffer.close(); return runtimeResult; @@ -237,6 +240,7 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { success: false, error: err, events: [...this.observedEvents], + destroy: () => this.destroySandboxOnce(), }; } } @@ -266,10 +270,32 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { await this.emitEvent(this.createEvent("stop.requested", { reason })); this.abortController.abort(); this.inputBuffer.close(); - await this.sandbox.destroy(); + await this.destroySandboxOnce(); this.eventBuffer.close(); } + /** + * Idempotent sandbox teardown. Backs both `AgentSession.stop()` and + * the `destroy()` method on returned `AgentSessionResult`s, so callers + * can safely call either or both without double-destroying the + * underlying ComputeSDK / local sandbox. + */ + private async destroySandboxOnce(): Promise { + if (this.sandboxDestroyed) return; + if (this.sandboxDestroyPromise) { + await this.sandboxDestroyPromise; + return; + } + this.sandboxDestroyPromise = (async () => { + try { + await this.sandbox.destroy(); + } finally { + this.sandboxDestroyed = true; + } + })(); + await this.sandboxDestroyPromise; + } + getQueuedMessages(): readonly string[] { return this.queuedMessages; } diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts index 3fb885b0d..ed79924d6 100644 --- a/packages/agent-runtime/src/types.ts +++ b/packages/agent-runtime/src/types.ts @@ -336,6 +336,15 @@ export interface AgentSessionResult { result?: string; error?: Error; events: TranscriptEvent[]; + /** + * Release the underlying sandbox. Equates to ComputeSDK's + * `ProviderSandbox.destroy()` for ComputeSDK-backed providers (deletes + * the remote sandbox and releases compute resources); for the local + * provider it is a no-op. Idempotent — safe to call multiple times, + * and safe to call alongside `AgentSession.stop()` (they share the + * same one-shot destroy). + */ + destroy(): Promise; } export interface NormalizedAgentSessionConfig diff --git a/packages/agent-runtime/test/runtime.test.ts b/packages/agent-runtime/test/runtime.test.ts index 3393952a0..b4e0c5ae9 100644 --- a/packages/agent-runtime/test/runtime.test.ts +++ b/packages/agent-runtime/test/runtime.test.ts @@ -377,6 +377,48 @@ describe("AgentRuntime", () => { ); }); + it("exposes destroy() on AgentSessionResult that releases the sandbox exactly once", async () => { + // Verifies that the ComputeSDK-style destroy escape hatch is reachable + // from the result object, that it's idempotent, and that the session's + // stop() shares the same one-shot — so callers can call either or both + // without double-destroying the underlying sandbox. + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "done" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-destroy", + harness: "codex", + userPrompt: "anything", + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const result = await session.start(); + expect(result.success).toBe(true); + expect(typeof result.destroy).toBe("function"); + + // Before destroy: sandbox.destroy has not been called. + expect( + sandbox.commands.find((c) => c.command === "DESTROY"), + ).toBeUndefined(); + expect(sandbox.destroyed).toBe(0); + + await result.destroy(); + expect(sandbox.destroyed).toBe(1); + + // Idempotent — calling again must not invoke sandbox.destroy a second time. + await result.destroy(); + expect(sandbox.destroyed).toBe(1); + + // And stop() shares the same one-shot, so it doesn't double-destroy either. + await session.stop(); + expect(sandbox.destroyed).toBe(1); + }); + it("materializes sensitive files before setup without exposing contents", async () => { const sandbox = new FakeSandbox( JSON.stringify({ @@ -553,6 +595,7 @@ class FakeSandbox implements RunnerSandbox { command: string; options: unknown; }> = []; + destroyed = 0; constructor(private readonly stdout: string) {} @@ -570,6 +613,6 @@ class FakeSandbox implements RunnerSandbox { } async destroy(): Promise { - return; + this.destroyed += 1; } } From 5e085ae1200f787c0bd33058e880dc4f91bab8c4 Mon Sep 17 00:00:00 2001 From: Connor Turland <1409121+Connoropolous@users.noreply.github.com> Date: Fri, 15 May 2026 16:28:13 -0700 Subject: [PATCH 6/6] refactor(agent-runtime): decouple stop() from sandbox destruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `stop()` and `destroy()` were doing two unrelated things bundled into one method. Split them. `stop()` now cancels the in-flight run only — aborts the harness process, closes the live event stream, closes the input pipe — and leaves the sandbox alive. This enables future workflows that reuse a warm sandbox across runs (per CYPACK-1209): a single run's `stop()` no longer destroys shared compute. `destroy()` is the sole sandbox-release path. It exists symmetrically on both `AgentSession` and `AgentSessionResult` (sharing a one-shot internal teardown promise). `AgentSession.destroy()` also implicitly cancels an in-flight run via `stop()` before releasing the sandbox, so callers don't need a two-step. Pre-1.0 package, clean break — no consumers to migrate. --- CHANGELOG.internal.md | 3 +- packages/agent-runtime/src/session.ts | 14 ++- packages/agent-runtime/src/types.ts | 17 ++++ packages/agent-runtime/test/runtime.test.ts | 97 +++++++++++++++++---- 4 files changed, 113 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index 6fd64db59..ba61998a0 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -8,7 +8,8 @@ This changelog documents internal development changes, refactors, tooling update - Added `cyrus-agent-runtime`, a standalone experimental TypeScript package for unified agent session orchestration across harnesses and sandbox providers. It includes normalized session config, transcript envelopes, local and ComputeSDK-backed sandbox abstractions, harness adapters for Claude/Codex/Cursor/Gemini plus provisional PI/OpenCode adapters, and focused tests for config, runtime lifecycle, sandbox execution, and transcript parsing. - Added live process streaming to `cyrus-agent-runtime`. New optional `RunnerSandbox.streamCommand(command, options)` capability surfaces stdout/stderr chunks to callbacks as they arrive, with `signal: AbortSignal` for cancellation and `input: AsyncIterable` for live stdin. Implemented natively in `LocalSandboxProvider` (via `child_process.spawn`) and for Daytona inside `ComputeSdkSandboxProvider` via a pluggable `NativeStreamAdapter` registry that reaches the underlying `@daytonaio/sdk` Sandbox through ComputeSDK's `ProviderSandbox.getInstance()` escape hatch, using async sessions + `getSessionCommandLogs(onStdout, onStderr)`. User-supplied adapters can be registered via `ComputeSdkSandboxProviderOptions.nativeStreamAdapters` for ComputeSDK providers we don't bundle (E2B, Vercel, Blaxel, Modal, Railway, Runloop, Cloudflare, Codesandbox). `RuntimeAgentSession.start()` now prefers `streamCommand` when `capabilities.streamingProcess` is true, line-buffers chunks across packet boundaries, and emits `TranscriptEvent`s live as the harness CLI produces them. New `CreateAgentSessionConfig.interactiveInput` opt-in flag routes `addMessage()` chunks into the running process's stdin (most one-shot CLIs hang on piped-but-never-closed stdin, so this defaults off). Verified end-to-end against real `codex exec` (events emitted ~8.6s before turn end), the local `child_process.spawn` path (chunks landed at the exact 400ms cadence the child produced them), and real Daytona Claude `stream-json` (system event landed 1.7s before result event over a remote sandbox). - Added `folders` and `repositories` to the `cyrus-agent-runtime` session config — two new materialization concepts that are deliberately distinct from existing `volumes`. `RuntimeFolderConfig` exposes a host filesystem folder inside the sandbox (walks the host tree, uploads each file via `SandboxFilesystem.writeFile`, supports an `exclude` glob list) and with `access: "readwrite"` syncs sandbox edits and any newly-created files back to the host folder after the harness command completes. `RuntimeRepositoryConfig` runs `git clone` inside the sandbox at `mountPath` with optional `branch` checkout and `depth` shallow-clone; local-path sources are converted to `file://...` to preserve git semantics, and shallow clones with a branch use `--branch` on the clone itself (since `git checkout` of a non-default branch fails on a shallow clone). Both emit lifecycle transcript events (`folder.materialize.started/completed/failed`, `folder.syncback.started/completed/failed`, `repository.materialize.started/completed/failed`) and run before the package setup commands so any setup that depends on the cloned tree or the mounted folder sees them ready. -- Added `destroy()` to `AgentSessionResult` in `cyrus-agent-runtime` — equates to ComputeSDK's `ProviderSandbox.destroy()` for ComputeSDK-backed providers (deletes the remote sandbox, releases compute resources) and is a no-op for the local provider. Idempotent and shared with `AgentSession.stop()`, so callers can release the sandbox via either method (or both) without double-destroying. Lets consumers hold only the result, consume the events/result, and tear down without keeping a session reference. +- Added `destroy()` to `AgentSessionResult` in `cyrus-agent-runtime` — equates to ComputeSDK's `ProviderSandbox.destroy()` for ComputeSDK-backed providers (deletes the remote sandbox, releases compute resources) and is a no-op for the local provider. Idempotent. Lets consumers hold only the result, consume the events/result, and tear down without keeping a session reference. +- Decoupled `AgentSession.stop()` from sandbox destruction. `stop()` now cancels the in-flight harness only — aborts the running process, closes the live event stream, closes the input pipe — and leaves the sandbox alive. Sandbox teardown is the sole responsibility of the new `destroy()` method, which exists symmetrically on both `AgentSession` and `AgentSessionResult` (sharing a one-shot internal teardown promise). `AgentSession.destroy()` also implicitly cancels an in-flight run via `stop()` before releasing the sandbox, so callers don't need a two-step. Decoupling enables future workflows that reuse a warm sandbox across runs (per CYPACK-1209) — a single run's `stop()` no longer destroys shared compute. ## [0.2.50] - 2026-04-30 diff --git a/packages/agent-runtime/src/session.ts b/packages/agent-runtime/src/session.ts index 6c7acf71b..58b5f912c 100644 --- a/packages/agent-runtime/src/session.ts +++ b/packages/agent-runtime/src/session.ts @@ -266,16 +266,26 @@ export class RuntimeAgentSession extends EventEmitter implements AgentSession { } async stop(reason?: string): Promise { + if (this.stopped) return; this.stopped = true; await this.emitEvent(this.createEvent("stop.requested", { reason })); this.abortController.abort(); this.inputBuffer.close(); - await this.destroySandboxOnce(); this.eventBuffer.close(); } + async destroy(): Promise { + // If a run is still in flight, cancel it first so the harness process + // terminates cleanly before we tear down the sandbox. Idempotent — + // safe to call after a run has already completed or been stopped. + if (this.started && !this.stopped) { + await this.stop("destroy"); + } + await this.destroySandboxOnce(); + } + /** - * Idempotent sandbox teardown. Backs both `AgentSession.stop()` and + * Idempotent sandbox teardown. Backs both `AgentSession.destroy()` and * the `destroy()` method on returned `AgentSessionResult`s, so callers * can safely call either or both without double-destroying the * underlying ComputeSDK / local sandbox. diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts index ed79924d6..829e04b77 100644 --- a/packages/agent-runtime/src/types.ts +++ b/packages/agent-runtime/src/types.ts @@ -364,5 +364,22 @@ export interface AgentSession { start(): Promise; addMessage(message: string): Promise; interrupt(reason?: string): Promise; + /** + * Cancel the in-flight run. Aborts the running harness process, closes + * the live event stream, and closes the input pipe. Does NOT release + * the underlying sandbox — call {@link destroy} for that. Idempotent. + */ stop(reason?: string): Promise; + /** + * Release the underlying sandbox. Equates to ComputeSDK's + * `ProviderSandbox.destroy()` for ComputeSDK-backed providers + * (deletes the remote sandbox and releases compute resources); for + * the local provider it is a no-op. If a run is still in flight, + * cancels it first via {@link stop} so the harness process + * terminates cleanly before teardown. Idempotent. + * + * Shares its one-shot teardown with {@link AgentSessionResult.destroy}, + * so calling either or both in any order is safe. + */ + destroy(): Promise; } diff --git a/packages/agent-runtime/test/runtime.test.ts b/packages/agent-runtime/test/runtime.test.ts index b4e0c5ae9..58e9a6e26 100644 --- a/packages/agent-runtime/test/runtime.test.ts +++ b/packages/agent-runtime/test/runtime.test.ts @@ -377,11 +377,12 @@ describe("AgentRuntime", () => { ); }); - it("exposes destroy() on AgentSessionResult that releases the sandbox exactly once", async () => { - // Verifies that the ComputeSDK-style destroy escape hatch is reachable - // from the result object, that it's idempotent, and that the session's - // stop() shares the same one-shot — so callers can call either or both - // without double-destroying the underlying sandbox. + it("decouples stop() from sandbox destruction; destroy() is the only release path", async () => { + // stop() cancels the run; destroy() releases the sandbox. They are + // separate operations: stop() must NOT destroy, and destroy() can + // be called independently. Both AgentSession.destroy() and + // AgentSessionResult.destroy() share a one-shot, so calling either + // or both is safe. const sandbox = new FakeSandbox( JSON.stringify({ type: "item.completed", @@ -400,22 +401,63 @@ describe("AgentRuntime", () => { const result = await session.start(); expect(result.success).toBe(true); expect(typeof result.destroy).toBe("function"); + expect(typeof session.destroy).toBe("function"); + expect(sandbox.destroyed).toBe(0); - // Before destroy: sandbox.destroy has not been called. - expect( - sandbox.commands.find((c) => c.command === "DESTROY"), - ).toBeUndefined(); + // stop() must NOT destroy the sandbox. + await session.stop(); expect(sandbox.destroyed).toBe(0); + // destroy() on the result releases the sandbox exactly once. await result.destroy(); expect(sandbox.destroyed).toBe(1); - // Idempotent — calling again must not invoke sandbox.destroy a second time. + // Idempotent — calling result.destroy() again is a no-op. await result.destroy(); expect(sandbox.destroyed).toBe(1); - // And stop() shares the same one-shot, so it doesn't double-destroy either. - await session.stop(); + // Calling session.destroy() afterward shares the one-shot, also no-op. + await session.destroy(); + expect(sandbox.destroyed).toBe(1); + }); + + it("session.destroy() cancels an in-flight run and releases the sandbox", async () => { + // destroy() on the live session should: (a) cancel the harness if + // still running via stop(), (b) release the sandbox exactly once. + // The streaming fake's schedule is intentionally long enough that + // we can call destroy() mid-run. + const sandbox = new StreamingFakeSandbox([ + { delayMs: 50, stdout: "" }, + { + delayMs: 500, + stdout: `${JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "should-not-arrive" }, + })}\n`, + }, + ]); + const session = await createAgentSession( + { + sessionId: "session-destroy-live", + harness: "codex", + userPrompt: "anything", + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const startPromise = session.start(); + await new Promise((resolve) => setTimeout(resolve, 80)); + // Run is in flight; destroy must both cancel and release. + await session.destroy(); + expect(sandbox.destroyed).toBe(1); + + const result = await startPromise; + // The destroy() path goes through stop() which emits stop.requested. + expect(result.events.some((e) => e.kind === "stop.requested")).toBe(true); + + // Idempotent — calling either destroy again is a no-op. + await session.destroy(); + await result.destroy(); expect(sandbox.destroyed).toBe(1); }); @@ -508,6 +550,7 @@ class StreamingFakeSandbox implements RunnerSandbox { readonly stdinChunks: string[] = []; streamCalls = 0; runCalls = 0; + destroyed = 0; constructor(private readonly schedule: readonly ScheduledChunk[]) {} @@ -537,8 +580,30 @@ class StreamingFakeSandbox implements RunnerSandbox { let stdoutBuf = ""; let stderrBuf = ""; + let exitCode = 0; for (const event of this.schedule) { - await new Promise((resolve) => setTimeout(resolve, event.delayMs)); + // Honor cancellation so callers that abort via session.stop() / + // session.destroy() get a timely return rather than waiting out + // the schedule. + if (options.signal?.aborted) { + exitCode = 137; // SIGKILL-ish, common convention for cancelled + break; + } + await new Promise((resolve) => { + const timer = setTimeout(resolve, event.delayMs); + options.signal?.addEventListener( + "abort", + () => { + clearTimeout(timer); + resolve(); + }, + { once: true }, + ); + }); + if (options.signal?.aborted) { + exitCode = 137; + break; + } if (event.stdout) { stdoutBuf += event.stdout; options.onStdout?.(event.stdout); @@ -554,12 +619,14 @@ class StreamingFakeSandbox implements RunnerSandbox { return { stdout: stdoutBuf, stderr: stderrBuf, - exitCode: 0, + exitCode, durationMs: Date.now() - startedAt, }; } - async destroy(): Promise {} + async destroy(): Promise { + this.destroyed += 1; + } } class FakeSandbox implements RunnerSandbox {