diff --git a/CHANGELOG.internal.md b/CHANGELOG.internal.md index 0368fff0c..79ea2a0af 100644 --- a/CHANGELOG.internal.md +++ b/CHANGELOG.internal.md @@ -4,6 +4,19 @@ 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/OpenCode, 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. 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. +- Added session resume primitives to `cyrus-agent-runtime`. `CreateAgentSessionConfig.resumeHarnessSessionId` is caller-supplied — Claude adapter translates it into `--resume `; Cursor adapter translates it into `--agent-id ` for `@cyrus-ai/cursor-runner`. `AgentSessionResult.harnessSessionId` is the new harness-native id observed in this run, captured by `HarnessAdapter.extractSessionId(events)` (implemented for Claude against `system.init.session_id`, for Cursor against `SDKMessage.agent_id`) and surfaced for callers to persist. The caller owns the mapping between its session records and harness-native ids; the runtime does not persist transcripts itself. +- Added `sandbox.persistentState: { volume, bindingId }` to `cyrus-agent-runtime` — caller-facing abstraction that hides the per-harness state-env-var math. The runtime mounts the caller's volume at a fixed internal path with `bindingId` as the subpath, then calls each adapter's new `buildStateEnv(mountPath)` hook to inject the right env vars so the harness writes its state-dir there. Verified upstream per harness: Claude → `CLAUDE_CONFIG_DIR=${m}/.claude`; Cursor → `CURSOR_DATA_DIR=${m}/.cursor`; Codex → `CODEX_HOME=${m}/.codex`; Gemini → `GEMINI_CLI_HOME=${m}` (CLI appends `.gemini` itself); OpenCode → all four `XDG_*_HOME` dirs under `${m}/.opencode-xdg/{config,data,state,cache}` (no app-specific override exists). +- Extracted `@cyrus-ai/cursor-runner` as a publishable package — a thin CLI wrapper around `@cursor/sdk` that emits `SDKMessage` JSONL. Lets the agent-runtime cursor adapter consume a typed, version-pinned wire format that we own (no schema drift vs `cursor-agent`). +- Added `RuntimePlugin` to `cyrus-agent-runtime` — bundles MCP servers + hooks + skills with per-harness materializers translating one declaration into Claude / Cursor / Codex native filesystem state inside the sandbox. The bundled MCP-config path replaces the old standalone `mcps` field on session config. +- Added Daytona volume mounting (`RuntimeVolumeConfig` with provider-driven `kind: "bind" | "fuse" | "provider"` and `subpath` for per-binding isolation), `destroyWhileInactive` (pauses the underlying sandbox between `run()` calls — Daytona stop/start preserves on-disk state at a few-second resume cost), and Daytona base-snapshot harness binaries (`harness.command` lets adapters spawn snapshot-resident binaries directly; Cursor uses this for `cursor-runner`). +- Wired `cyrus-edge-worker` `AgentChatSessionHandler` to the agent-runtime: provider chosen from `EdgeConfig.defaultProvider`, MCP servers forwarded via the new plugin shape, Daytona chat sandbox snapshot + custom layout + bypass perms threaded through, `ANTHROPIC_API_KEY` vs `ANTHROPIC_AUTH_TOKEN` precedence fixed. + ## [0.2.52] - 2026-05-13 _No internal-only changes._ diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e873ef6..30882ed4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. ### Added - **User skills can now be scoped to specific repositories, Linear teams, or Linear labels** — Skills synced from cyrus-hosted with `repositoryIds`, `linearTeamIds`, or `linearLabelIds` are only loaded into sessions whose context matches every populated dimension (AND across dimensions, OR within each list). Unscoped skills continue to load for every session, and old payloads without scope fields keep working as global. Scope is persisted as a `scope.json` sidecar alongside `SKILL.md` and enforced at runtime via the Claude Agent SDK's `skills` option so the model can't see or invoke out-of-scope skills. ([CYPACK-1156](https://linear.app/ceedar/issue/CYPACK-1156), [#1205](https://github.com/cyrusagents/cyrus/pull/1205)) - **Shared auto-memory across Slack chat sessions** — Slack-triggered chat sessions now share a persistent Claude auto-memory directory at `/slack-memory/`, so memory built up in one Slack thread carries over to every other Slack thread. ([CYPACK-1190](https://linear.app/ceedar/issue/CYPACK-1190), [#1199](https://github.com/cyrusagents/cyrus/pull/1199)) +- **Base snapshot for Daytona chat sandboxes via `DAYTONA_SNAPSHOT`** — When `DAYTONA_SNAPSHOT` is set in the environment, Daytona-backed chat sessions seed their sandboxes from that pre-built Daytona snapshot instead of the default base image. When a snapshot is in use the npm-install bootstrap is skipped (the snapshot is expected to ship Claude Code preinstalled) and the CLI defaults to `claude` on `PATH`. Two companion overrides let snapshots use any home layout: `DAYTONA_WORKING_DIR` (default `/home/daytona`) sets the in-sandbox working directory, and `DAYTONA_CLAUDE_CLI_PATH` overrides the `claude` binary path. ### Fixed - **Session Stop hook now actually reminds the agent to ship before stopping** — Replaced the broken Stop-hook return shape (`additionalContext` + `continue: true`, which the Claude Agent SDK silently drops) with the SDK's documented `decision: "block"` + `reason` form. The first stop attempt now blocks and feeds the commit/push/PR reminder back into the next turn; a second stop (with `stop_hook_active === true`) proceeds, preventing infinite loops. ([CYPACK-1204](https://linear.app/ceedar/issue/CYPACK-1204), [#1210](https://github.com/cyrusagents/cyrus/pull/1210)) @@ -19,6 +20,7 @@ All notable changes to this project will be documented in this file. ### Changed - **Slack mention prompt nudges agents toward `linear_agent_give_feedback` for live child sessions** — When responding in Slack, Cyrus is now told to send mid-flight corrections to a running child agent session via `mcp__cyrus-tools__linear_agent_give_feedback` instead of falling back to `mcp__linear__save_comment`. Produces a stronger signal when correcting work that is already in progress. ([CYPACK-1189](https://linear.app/ceedar/issue/CYPACK-1189), [#1198](https://github.com/cyrusagents/cyrus/pull/1198)) +- **Daytona chat sessions now bypass Claude's permission prompts** — Since the Daytona sandbox is itself the isolation boundary, blocking on per-tool prompts (which no user can answer) was preventing the agent from running shell commands inside the sandbox. Local sessions still prompt as before. ### Packages diff --git a/apps/cli/src/services/WorkerService.ts b/apps/cli/src/services/WorkerService.ts index db55572b0..7755e1455 100644 --- a/apps/cli/src/services/WorkerService.ts +++ b/apps/cli/src/services/WorkerService.ts @@ -222,6 +222,7 @@ export class WorkerService { | "codex" | "cursor" | undefined) || edgeConfig.defaultRunner, + defaultProvider: edgeConfig.defaultProvider, issueUpdateTrigger: edgeConfig.issueUpdateTrigger, promptDefaults: edgeConfig.promptDefaults, linearWorkspaces: edgeConfig.linearWorkspaces, diff --git a/package.json b/package.json index e0ce41172..e549ca865 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,13 @@ "onlyBuiltDependencies": [ "sqlite3" ], + "packageExtensions": { + "@daytonaio/sdk": { + "dependencies": { + "tslib": "^2" + } + } + }, "overrides": { "jws": ">=4.0.1", "@modelcontextprotocol/sdk": ">=1.26.0", @@ -54,11 +61,6 @@ "vite": ">=7.1.11", "zod": "4.3.6", "hono": ">=4.12.18", - "fast-uri": ">=3.1.2", - "ip-address": ">=10.1.1", - "@anthropic-ai/sdk": ">=0.91.1", - "@opentelemetry/sdk-node": ">=0.217.0", - "@opentelemetry/exporter-prometheus": ">=0.217.0", "@hono/node-server": ">=1.19.10", "rollup": ">=4.59.0", "flatted": ">=3.4.0", @@ -72,7 +74,20 @@ "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", + "protobufjs": ">=7.5.8", + "ws": ">=8.20.1", + "brace-expansion": ">=5.0.6", + "@anthropic-ai/sdk": ">=0.91.1", + "@daytonaio/sdk": ">=0.175.0" + }, + "patchedDependencies": { + "@daytonaio/sdk@0.175.0": "patches/@daytonaio__sdk@0.175.0.patch" } }, "lint-staged": { diff --git a/packages/agent-runtime/ASSUMPTIONS.md b/packages/agent-runtime/ASSUMPTIONS.md new file mode 100644 index 000000000..04f012e9f --- /dev/null +++ b/packages/agent-runtime/ASSUMPTIONS.md @@ -0,0 +1,53 @@ +# 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. +- `RuntimeVolumeConfig.subpath` carries the provider-defined prefix used to scope a shared volume. The Daytona Volumes pattern is the reference use case; other providers map `subpath` as appropriate. + +## Session Resume Contract + +- The runtime exposes two resume primitives. The caller (Cyrus's `AgentSessionManager`) owns the mapping between its session records and harness-native session ids. + - `CreateAgentSessionConfig.resumeHarnessSessionId`: caller-supplied prior id. Harness adapters translate it into the right CLI flag (e.g. `--resume ` for Claude). + - `AgentSessionResult.harnessSessionId`: the new harness-native id observed in this run's transcript, surfaced for the caller to persist for next time. +- Harness adapters extract the harness-native session id from transcript events via `extractSessionId(events)`. Claude's `system.init.session_id` is the canonical example. +- The runtime does not persist transcripts itself. For the harness to actually see prior conversation on resume, the caller must arrange durable storage for the harness's config dir — for example by attaching a `RuntimeVolumeConfig` (Daytona Volumes are the reference) mounted at the harness's config path and setting the matching env var (`CLAUDE_CONFIG_DIR` for Claude). +- 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 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 + +- `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..5cd42dfc9 --- /dev/null +++ b/packages/agent-runtime/VALIDATION.md @@ -0,0 +1,223 @@ +# 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 Smoke + +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 remote Claude session completed successfully with the exact requested result. + +Observed runtime result: + +```json +{ + "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"] +} +``` diff --git a/packages/agent-runtime/package.json b/packages/agent-runtime/package.json new file mode 100644 index 000000000..e29508232 --- /dev/null +++ b/packages/agent-runtime/package.json @@ -0,0 +1,39 @@ +{ + "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", + "@cyrus-ai/cursor-runner": "workspace:*", + "computesdk": "^4.0.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@anthropic-ai/claude-agent-sdk": "0.2.141", + "@cursor/sdk": "1.0.13", + "@google/gemini-cli-core": "0.42.0", + "@openai/codex-sdk": "0.131.0", + "@opencode-ai/sdk": "1.15.5", + "@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..f79613f2a --- /dev/null +++ b/packages/agent-runtime/src/harnesses/claude.ts @@ -0,0 +1,143 @@ +import type { + HarnessAdapter, + HarnessRunOptions, + NormalizedAgentSessionConfig, + PermissionMode, +} from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +// Translate Cyrus's cross-harness PermissionMode into Claude Code's +// `--permission-mode` CLI values. Claude accepts `acceptEdits`, `auto`, +// `bypassPermissions`, `default`, `dontAsk`, `plan` — our enum is shaped +// to be portable across harnesses, so a couple of values need mapping. +function toClaudePermissionMode(mode: PermissionMode): string { + switch (mode) { + case "bypass": + return "bypassPermissions"; + case "ask": + return "default"; + default: + return mode; + } +} + +export const claudeHarness: HarnessAdapter = { + kind: "claude", + stateDirectories: [".claude"], + buildCommand( + config: NormalizedAgentSessionConfig, + options: HarnessRunOptions, + ) { + const args = [ + "-p", + options.userPrompt, + "--output-format", + "stream-json", + "--verbose", + ]; + if (options.continueSession) { + // Resume the most recent session in the current cwd. Claude tracks + // sessions per cwd, so the runtime's per-session HOME isolation + // guarantees we pick up the right conversation. + args.push("--continue"); + } + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt && !options.continueSession) { + // On continue, Claude already has the system prompt baked in. + args.push("--append-system-prompt", config.systemPrompt); + } + + if (config.permissions?.mode) { + args.push( + "--permission-mode", + toClaudePermissionMode(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(","), + ); + } + + // Plugin wiring — materializer output. + const claudePluginDirs = options.pluginOutputs?.claudePluginDirs ?? []; + for (const dir of claudePluginDirs) { + args.push("--plugin-dir", dir); + } + if (options.pluginOutputs?.claudeMcpConfigPath) { + args.push("--mcp-config", options.pluginOutputs.claudeMcpConfigPath); + args.push("--strict-mcp-config"); + } + + // Caller-supplied harness session resume (from the volumes branch). + // `resumeHarnessSessionId` is the session id returned in a prior + // AgentSessionResult.harnessSessionId — Claude maps this to its + // `--resume ` flag, which loads the transcript at the + // matching id from the harness's state-backing. + if (config.resumeHarnessSessionId) { + args.push("--resume", config.resumeHarnessSessionId); + } + + 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; + }, + buildStateEnv(mountPath) { + // Claude Code reads/writes its session transcripts, OAuth creds, and + // config from `$CLAUDE_CONFIG_DIR` when set (otherwise `~/.claude/`). + // Joining a `.claude` suffix under the runtime's shared state mount + // keeps the layout identical to a local install and leaves the + // sibling mount safe for other harnesses' state dirs. + return { CLAUDE_CONFIG_DIR: `${mountPath}/.claude` }; + }, + extractSessionId(events) { + // Claude Code's stream-json emits a `system` event with + // `subtype: "init"` and a `session_id` at the start of every run. + // That value is the only stable harness-native session id, and + // `claude --resume ` accepts it verbatim. Scan in arrival + // order — the first init carries the session id; later events + // (assistant, result) repeat it but the init is canonical. + for (const event of events) { + if (!isRecord(event.raw)) continue; + const sessionId = + stringField(event.raw, "session_id") ?? + stringField(event.raw, "sessionId"); + if (sessionId) return sessionId; + } + return undefined; + }, +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stringField( + record: Record, + key: string, +): string | undefined { + const value = record[key]; + return typeof value === "string" ? value : undefined; +} diff --git a/packages/agent-runtime/src/harnesses/codex.ts b/packages/agent-runtime/src/harnesses/codex.ts new file mode 100644 index 000000000..47c1d139b --- /dev/null +++ b/packages/agent-runtime/src/harnesses/codex.ts @@ -0,0 +1,83 @@ +import type { + HarnessAdapter, + HarnessRunOptions, + NormalizedAgentSessionConfig, +} from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const codexHarness: HarnessAdapter = { + kind: "codex", + stateDirectories: [".codex"], + buildCommand( + config: NormalizedAgentSessionConfig, + options: HarnessRunOptions, + ) { + const args = ["exec", "--json", "--skip-git-repo-check"]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt && !options.continueSession) { + args.push( + "-c", + `developer_instructions=${JSON.stringify(config.systemPrompt)}`, + ); + } + + if (config.permissions?.mode) { + args.push( + "-c", + `approval_policy=${JSON.stringify(config.permissions.mode)}`, + ); + } + + // Plugin wiring — codex MCP servers come through as inline TOML + // overrides; skills are materialized to `$HOME/.agents/skills/` + // and the session sets HOME accordingly via its env merge. + for (const override of options.pluginOutputs?.codexConfigOverrides ?? []) { + args.push("-c", override); + } + + // Codex's resume/continue flag varies by CLI version; the runtime + // currently passes the prompt as a positional arg either way. When a + // real codex resume mechanism is wired, branch on options.continueSession. + args.push(options.userPrompt); + + return createCommand(config, "codex", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("codex", line, context); + }, + buildStateEnv(mountPath) { + // Codex (Rust binary) reads `CODEX_HOME` for its config / credentials + // / sessions / skills dir (default `~/.codex/`). No XDG fallback — + // see `codex-rs/utils/home-dir/src/lib.rs::find_codex_home`. The + // binary errors out if the directory doesn't already exist, so the + // runtime's persistent-state mount must already be writable when + // the harness starts; we assume the volume mount handles that + // (subpath dirs are created on first bind for Daytona volumes). + // Joining a `.codex` suffix keeps the layout consistent with + // claude/cursor and lets multiple harnesses share one binding. + return { CODEX_HOME: `${mountPath}/.codex` }; + }, + 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..3a9e5d49f --- /dev/null +++ b/packages/agent-runtime/src/harnesses/cursor.ts @@ -0,0 +1,158 @@ +import { createRequire } from "node:module"; +import type { SDKMessage } from "@cursor/sdk"; +import type { + HarnessAdapter, + HarnessRunOptions, + NormalizedAgentSessionConfig, +} from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +/** + * Host-side fallback path to the `@cyrus-ai/cursor-runner` CLI, resolved + * once at module load via Node's standard module resolution. + * + * Used when the caller does NOT supply `harness.command` — i.e. for the + * local provider, where `@cyrus-ai/cursor-runner` ships under the host's + * `node_modules/` and `createRequire` finds it there. + * + * For the daytona provider, callers should supply `harness.command` + * pointing at the runner inside the sandbox (typically `"cursor-runner"` + * resolved via PATH, since snapshots ship the runner preinstalled + * alongside the harness binaries — same model as `DAYTONA_CLAUDE_CLI_PATH`). + * + * Why a separately published package: `@cyrus-ai/cursor-runner` is a thin + * SDK driver that wraps `@cursor/sdk` and emits `SDKMessage` events as + * JSONL — exactly the wire format `parseJsonLine` parses below. Owning + * the producer means the cursor stream IS the SDK union by construction + * (no schema drift), and shipping it as a standalone CLI keeps the + * Cursor `@cursor/sdk` runtime dependency out of agent-runtime's + * surface (it's a devDep here, just for the `SDKMessage` type import). + * + * Resolved with `createRequire(import.meta.url)` rather than a relative + * `import.meta.url` URL so the path follows wherever pnpm/npm linked + * the package — right behavior for both workspace symlinks and + * node_modules installs on the host. + */ +const HOST_CURSOR_RUNNER_PATH = createRequire(import.meta.url).resolve( + "@cyrus-ai/cursor-runner", +); + +export const cursorHarness: HarnessAdapter = { + kind: "cursor", + // Cursor's SDK does support agent resume via `Agent.resume(agentId)`, + // but the agentId lives in our driver's process state, not in a + // filesystem state directory — we persist it via `--agent-id-file` + // (a sibling of the session's state backing). No HOME-relative + // directory to declare here. + stateDirectories: [], + buildCommand( + config: NormalizedAgentSessionConfig, + options: HarnessRunOptions, + ) { + // We invoke `@cyrus-ai/cursor-runner` instead of `cursor-agent` so the + // wire format matches `@cursor/sdk`'s `SDKMessage` union by + // construction. See `@cyrus-ai/cursor-runner`'s README for the why. + // + // Two invocation shapes: + // - When `harness.command` is set (daytona snapshots), it's the + // path to (or name of) the `cursor-runner` bin inside the + // sandbox. The bin has a `#!/usr/bin/env node` shebang, so we + // spawn it directly. Caller-provided value flows through + // verbatim — `"cursor-runner"` to use PATH resolution, + // absolute path to pin a specific copy. + // - When `harness.command` is unset (local provider), we fall + // back to spawning the host-resolved runner via `node `. + // `node` rather than direct exec because the resolved path + // points at the package's `dist/index.js` inside pnpm's + // .pnpm store, which may not be marked executable. + const command = config.harness.command ?? "node"; + const args = + config.harness.command !== undefined + ? ["--prompt", options.userPrompt] + : [HOST_CURSOR_RUNNER_PATH, "--prompt", options.userPrompt]; + + const model = resolveModel(config); + if (model) { + args.push("--model", model); + } + + // Working directory inside the sandbox — Cursor's local-agent + // mode needs an explicit cwd so it knows where to walk file + // contexts from. + if (config.sandbox?.workingDirectory) { + args.push("--cwd", config.sandbox.workingDirectory); + } + + // systemPrompt is prepended to the user prompt by the driver + // (Cursor doesn't expose a separate system-instructions field + // at the local-agent layer the way Claude does). + if (config.systemPrompt && !options.continueSession) { + args.push("--system-prompt", config.systemPrompt); + } + + // Cross-turn resume: caller persists the agentId returned in a + // prior AgentSessionResult.harnessSessionId and hands it back as + // `config.resumeHarnessSessionId`. The cursor-runner translates + // it to `Agent.resume()` via its `--agent-id` flag, so the + // run picks up the prior conversation. Driver-side + // `--agent-id-file` still works for callers that want the runner + // to record the agentId itself; we just don't need it from this + // adapter because the runtime now surfaces the id via + // extractSessionId below. + if (config.resumeHarnessSessionId) { + args.push("--agent-id", config.resumeHarnessSessionId); + } + + return createCommand(config, command, args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("cursor", line, context); + }, + extractResult(events) { + // Walk backwards for the last assistant text block. The driver + // emits `SDKMessage` directly, so `event.raw.type === "assistant"` + // narrows to `SDKAssistantMessage` with full content typing — + // no manual guards. + for (let i = events.length - 1; i >= 0; i -= 1) { + const event = events[i]; + if (!event) continue; + const raw = event.raw as SDKMessage | undefined; + if (raw?.type !== "assistant") continue; + for (const block of raw.message.content) { + if (block.type === "text" && typeof block.text === "string") { + return block.text; + } + } + } + return undefined; + }, + buildStateEnv(mountPath) { + // `@cursor/sdk`'s local mode persists agents to a sqlite db under + // `$CURSOR_DATA_DIR` (default `~/.cursor/`). Joining `.cursor` under + // the runtime's shared state mount keeps the layout identical to + // a local install while leaving room for other harnesses to share + // the same persistent-state binding. + return { CURSOR_DATA_DIR: `${mountPath}/.cursor` }; + }, + extractSessionId(events) { + // `@cursor/sdk` puts `agent_id` on every SDKMessage variant + // (status, tool_call, assistant, user, thinking, request, task, + // system). It's stable across the run — first event on the + // stream carries the canonical id, and it doesn't change after. + // Scan in arrival order and return the first non-empty value. + // + // `event.raw` is typed `SDKMessage` for `harness: "cursor"`, but + // runtime lifecycle events (e.g. plain text from stderr) emit + // strings or other shapes through the same stream. Guard with a + // plain-object check before peeking at `agent_id`. + for (const event of events) { + const raw = event.raw; + if (typeof raw !== "object" || raw === null) continue; + const agentId = (raw as { agent_id?: unknown }).agent_id; + if (typeof agentId === "string" && agentId.length > 0) { + return agentId; + } + } + return undefined; + }, +}; diff --git a/packages/agent-runtime/src/harnesses/gemini.ts b/packages/agent-runtime/src/harnesses/gemini.ts new file mode 100644 index 000000000..a493b42ff --- /dev/null +++ b/packages/agent-runtime/src/harnesses/gemini.ts @@ -0,0 +1,47 @@ +import type { + HarnessAdapter, + HarnessRunOptions, + NormalizedAgentSessionConfig, +} from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const geminiHarness: HarnessAdapter = { + kind: "gemini", + stateDirectories: [".gemini"], + buildCommand( + config: NormalizedAgentSessionConfig, + options: HarnessRunOptions, + ) { + 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", options.userPrompt); + + return createCommand(config, "gemini", args, { + env: { + GEMINI_SYSTEM_MD: options.continueSession + ? undefined + : config.systemPrompt, + }, + }); + }, + parseStdoutLine(line, context) { + return parseJsonLine("gemini", line, context); + }, + buildStateEnv(mountPath) { + // Gemini CLI doesn't have a dir-specific override env var; it + // overrides what its `homedir()` helper returns via + // `GEMINI_CLI_HOME` (see `@google/gemini-cli-core` → + // `dist/src/utils/paths.js::homedir`). The dir suffix is hardcoded + // to `.gemini`, so the CLI ends up reading/writing + // `${mountPath}/.gemini/` — which sits as a sibling to other + // harnesses' `./` dirs under the same mount. + return { GEMINI_CLI_HOME: mountPath }; + }, +}; diff --git a/packages/agent-runtime/src/harnesses/index.ts b/packages/agent-runtime/src/harnesses/index.ts new file mode 100644 index 000000000..1aa40dffa --- /dev/null +++ b/packages/agent-runtime/src/harnesses/index.ts @@ -0,0 +1,47 @@ +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"; + +export type { + HarnessAdapter, + HarnessCommand, + TranscriptParseContext, +} from "../types.js"; + +export { + claudeHarness, + codexHarness, + cursorHarness, + geminiHarness, + opencodeHarness, +}; + +export const harnessAdapters: Record = { + claude: claudeHarness, + codex: codexHarness, + cursor: cursorHarness, + gemini: geminiHarness, + opencode: opencodeHarness, +}; + +export function getHarnessAdapter(kind: HarnessKind): HarnessAdapter { + return harnessAdapters[kind]; +} + +export function buildHarnessInvocation( + config: NormalizedAgentSessionConfig, + options: { userPrompt: string; continueSession?: boolean }, +): HarnessCommand { + return getHarnessAdapter(config.harness.kind).buildCommand(config, { + userPrompt: options.userPrompt, + continueSession: options.continueSession ?? false, + }); +} diff --git a/packages/agent-runtime/src/harnesses/opencode.ts b/packages/agent-runtime/src/harnesses/opencode.ts new file mode 100644 index 000000000..89c8c504c --- /dev/null +++ b/packages/agent-runtime/src/harnesses/opencode.ts @@ -0,0 +1,59 @@ +import type { + HarnessAdapter, + HarnessRunOptions, + NormalizedAgentSessionConfig, +} from "../types.js"; +import { createCommand, parseJsonLine, resolveModel } from "./common.js"; + +export const opencodeHarness: HarnessAdapter = { + kind: "opencode", + stateDirectories: [], + buildCommand( + config: NormalizedAgentSessionConfig, + options: HarnessRunOptions, + ) { + // `--format json` (not `--output-format json`) — the CLI's actual flag + // per `opencode run --help` on v1.15.5. Mis-named in earlier versions + // of this adapter; would have failed at runtime on first invocation. + const args = ["run", "--format", "json"]; + const model = resolveModel(config); + + if (model) { + args.push("--model", model); + } + + if (config.systemPrompt && !options.continueSession) { + args.push("--system", config.systemPrompt); + } + + args.push(options.userPrompt); + + return createCommand(config, "opencode", args); + }, + parseStdoutLine(line, context) { + return parseJsonLine("opencode", line, context); + }, + buildStateEnv(mountPath) { + // opencode doesn't ship a single state-dir override env var. Its + // `Global.make()` (see `packages/core/src/global.ts` in + // `github.com/sst/opencode`) resolves all four storage roots via + // the `xdg-basedir` npm package and appends `/opencode` to each. + // To corral every dir under our persistent mount we must override + // all four XDG vars. We scope them under `.opencode-xdg/` so we + // don't accidentally claim the XDG hierarchy for unrelated tools + // that happen to run in the sandbox (git, npm, etc.). + // + // Resulting on-disk layout under the mount: + // .opencode-xdg/config/opencode/ (config files) + // .opencode-xdg/data/opencode/ (logs, repos) + // .opencode-xdg/state/opencode/ (sessions, flock) + // .opencode-xdg/cache/opencode/ (bin cache) + const root = `${mountPath}/.opencode-xdg`; + return { + XDG_CONFIG_HOME: `${root}/config`, + XDG_DATA_HOME: `${root}/data`, + XDG_STATE_HOME: `${root}/state`, + XDG_CACHE_HOME: `${root}/cache`, + }; + }, +}; diff --git a/packages/agent-runtime/src/index.ts b/packages/agent-runtime/src/index.ts new file mode 100644 index 000000000..c39a32509 --- /dev/null +++ b/packages/agent-runtime/src/index.ts @@ -0,0 +1,8 @@ +export * from "./harnesses/index.js"; +export * from "./materializers/index.js"; +export * from "./plugins/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/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/plugins/index.ts b/packages/agent-runtime/src/plugins/index.ts new file mode 100644 index 000000000..8e8b5f528 --- /dev/null +++ b/packages/agent-runtime/src/plugins/index.ts @@ -0,0 +1,3 @@ +export * from "./materializers/index.js"; +export * from "./resolver.js"; +export * from "./skill-md.js"; diff --git a/packages/agent-runtime/src/plugins/materializers/claude.ts b/packages/agent-runtime/src/plugins/materializers/claude.ts new file mode 100644 index 000000000..ea0b42f22 --- /dev/null +++ b/packages/agent-runtime/src/plugins/materializers/claude.ts @@ -0,0 +1,114 @@ +import type { RunnerSandbox, RuntimePlugin } from "../../types.js"; +import { renderSkillMd } from "../skill-md.js"; + +export interface ClaudeMaterializeResult { + /** Pass this to `claude` as `--plugin-dir `. */ + pluginDir: string; + /** Optional: pass this to `claude` as `--mcp-config `. */ + mcpConfigPath: string | null; + /** Files written into the sandbox (sandbox-absolute paths). */ + filesWritten: string[]; +} + +/** + * Materialize a single RuntimePlugin as a Claude Code plugin under + * `//`. Produces: + * + * //.claude-plugin/plugin.json + * //.mcp.json (when mcpServers present) + * //hooks/hooks.json (when hooks present) + * //skills//SKILL.md + * //skills//... + * + * Caller wires `--plugin-dir ` into the `claude -p ...` + * invocation. + */ +export async function materializePluginForClaude( + plugin: RuntimePlugin, + sandbox: RunnerSandbox, + pluginsRoot: string, +): Promise { + const pluginDir = joinPath(pluginsRoot, plugin.name); + const filesWritten: string[] = []; + + const manifest: Record = { name: plugin.name }; + if (plugin.version) manifest.version = plugin.version; + if (plugin.description) manifest.description = plugin.description; + + await sandbox.filesystem.mkdir(joinPath(pluginDir, ".claude-plugin")); + const manifestPath = joinPath(pluginDir, ".claude-plugin/plugin.json"); + await sandbox.filesystem.writeFile( + manifestPath, + JSON.stringify(manifest, null, 2), + ); + filesWritten.push(manifestPath); + + let mcpConfigPath: string | null = null; + if (plugin.mcpServers && Object.keys(plugin.mcpServers).length > 0) { + mcpConfigPath = joinPath(pluginDir, ".mcp.json"); + await sandbox.filesystem.writeFile( + mcpConfigPath, + JSON.stringify({ mcpServers: plugin.mcpServers }, null, 2), + ); + filesWritten.push(mcpConfigPath); + } + + if (plugin.hooks && plugin.hooks.length > 0) { + // Claude hooks.json shape: { hooks: { Event: [{ matcher, hooks: [{ type, command, timeout }] }] } } + const grouped: Record>> = {}; + for (const hook of plugin.hooks) { + const entry: Record = { + hooks: [ + { + type: "command", + command: hook.command, + ...(hook.timeout ? { timeout: hook.timeout } : {}), + }, + ], + }; + if (hook.matcher) entry.matcher = hook.matcher; + if (!grouped[hook.event]) grouped[hook.event] = []; + grouped[hook.event]!.push(entry); + } + const hooksPath = joinPath(pluginDir, "hooks/hooks.json"); + await sandbox.filesystem.mkdir(joinPath(pluginDir, "hooks")); + await sandbox.filesystem.writeFile( + hooksPath, + JSON.stringify({ hooks: grouped }, null, 2), + ); + filesWritten.push(hooksPath); + } + + if (plugin.skills && plugin.skills.length > 0) { + await sandbox.filesystem.mkdir(joinPath(pluginDir, "skills")); + for (const skill of plugin.skills) { + const skillDir = joinPath(pluginDir, "skills", skill.name); + await sandbox.filesystem.mkdir(skillDir); + const skillPath = joinPath(skillDir, "SKILL.md"); + await sandbox.filesystem.writeFile(skillPath, renderSkillMd(skill)); + filesWritten.push(skillPath); + for (const asset of skill.assets ?? []) { + const assetPath = joinPath(skillDir, asset.path); + const assetDir = dirnameOf(assetPath); + if (assetDir) await sandbox.filesystem.mkdir(assetDir); + await sandbox.filesystem.writeFile(assetPath, asset.content); + filesWritten.push(assetPath); + } + } + } + + return { pluginDir, mcpConfigPath, filesWritten }; +} + +function joinPath(...parts: string[]): string { + return parts + .filter((p) => p !== "") + .map((p) => p.replace(/\/+$/, "")) + .join("/") + .replace(/\/{2,}/g, "/"); +} + +function dirnameOf(path: string): string | undefined { + const idx = path.lastIndexOf("/"); + return idx > 0 ? path.slice(0, idx) : undefined; +} diff --git a/packages/agent-runtime/src/plugins/materializers/codex.ts b/packages/agent-runtime/src/plugins/materializers/codex.ts new file mode 100644 index 000000000..73a0034d3 --- /dev/null +++ b/packages/agent-runtime/src/plugins/materializers/codex.ts @@ -0,0 +1,189 @@ +import type { RunnerSandbox, RuntimePlugin } from "../../types.js"; +import { renderSkillMd } from "../skill-md.js"; + +export interface CodexMaterializeResult { + /** + * Inline `-c` CLI overrides the caller should append to the codex + * invocation, e.g. `-c 'mcp_servers.={command="...",args=[...]}'`. + * Each entry is a complete `key=value` string ready for `-c`. + */ + cliConfigOverrides: string[]; + /** + * The `HOME` value the caller should set in the harness invocation + * env. Codex discovers skills at `$HOME/.agents/skills//` + * (NOT `$CODEX_HOME/skills/` — verified empirically), so we pin + * HOME to a per-session directory. + */ + homeOverride: string; + filesWritten: string[]; +} + +/** + * Materialize a RuntimePlugin for Codex. + * + * Skills → files at `/.agents/skills//SKILL.md` + + * optional `agents/openai.yaml` for the OpenAI runtime. + * MCP servers → returned as inline `-c mcp_servers.={...}` CLI + * overrides (no file written). Caller appends them to + * the codex invocation. + * Hooks → deferred. Codex has a full hooks engine (SessionStart / + * PreToolUse / PostToolUse / PermissionRequest / + * UserPromptSubmit / Stop, schema at + * https://developers.openai.com/codex/hooks), but two + * stacked upstream bugs make hooks materialization unusable + * from `codex exec` on every release we can reach. Both are + * open as of codex 0.131.0: + * + * 1. Direct config-layer hooks regression — open as + * https://github.com/openai/codex/issues/21639. + * Hooks declared in `~/.codex/hooks.json` or inline as + * `[[hooks.]]` in `~/.codex/config.toml` stopped + * firing starting in 0.129.0. ≥5 users independently + * confirmed across 0.129.0-alpha.15, 0.130.0, and + * Codex Desktop 26.506.21252; we have also reproduced + * on 0.131.0 with `--dangerously-bypass-hook-trust`, + * `[features].hooks = true`, `[features].codex_hooks + * = true`, valid JSON schema with `matcher` set, and + * both fresh and warmed `CODEX_HOME`s. Was working + * on 0.128.0-alpha.1 per the issue thread. + * + * 2. Plugin manifest `hooks` field silently dropped — + * open as https://github.com/openai/codex/issues/16430. + * `codex-rs/core/src/plugins/manifest.rs` parses + * `skills`, `mcpServers`, and `apps` but does not read + * the `hooks` field at all, and `hooks/src/engine/ + * discovery.rs` only walks config-layer folders, never + * the installed-plugin tree under `/ + * plugins/cache///`. So even with + * a fully-installed enabled plugin (verified via + * `codex plugin list` reporting "(installed, enabled)") + * and `[features].plugin_hooks = true` (now stable in + * 0.131.0), plugin-bundled hooks never register. The + * `plugin_hooks` feature flag toggles a gate whose + * implementation isn't shipped yet — the field is + * discarded before the gate is ever checked. + * + * The `--dangerously-bypass-hook-trust` flag landed in + * 0.131.0 (absent in 0.130.0) but doesn't help on either + * path: #21639 prevents the underlying discovery from + * finding any hook to bypass-trust, and #16430 prevents the + * plugin manifest from contributing any hook to discovery + * in the first place. + * + * Revisit when both issues close. Earliest: ship a learning + * test against the release that closes #21639 — if direct + * hooks fire again, we have a fallback materialization + * strategy (write to a session-local `hooks.json` under a + * per-session CODEX_HOME). Adding plugin-bundled support + * then waits on #16430 closing. Until then the materializer + * silently drops `plugin.hooks` rather than write a config + * tree the runtime will refuse to execute. + * + * `homeOverride` is the value the caller must set as the harness's + * HOME env var. Override HOME (not CODEX_HOME) for skill isolation. + */ +export async function materializePluginForCodex( + plugin: RuntimePlugin, + sandbox: RunnerSandbox, + homeOverride: string, +): Promise { + const filesWritten: string[] = []; + + if (plugin.skills && plugin.skills.length > 0) { + const skillsRoot = joinPath(homeOverride, ".agents", "skills"); + await sandbox.filesystem.mkdir(skillsRoot); + for (const skill of plugin.skills) { + const skillDir = joinPath(skillsRoot, skill.name); + await sandbox.filesystem.mkdir(skillDir); + const skillPath = joinPath(skillDir, "SKILL.md"); + await sandbox.filesystem.writeFile(skillPath, renderSkillMd(skill)); + filesWritten.push(skillPath); + + // Codex's OpenAI runtime expects an `agents/openai.yaml` sibling + // describing the skill at the protocol level. Without this, codex + // will still load the SKILL.md but the surface area in the agent + // directory is incomplete. Emit a minimal one. + const agentsDir = joinPath(skillDir, "agents"); + await sandbox.filesystem.mkdir(agentsDir); + const openaiYamlPath = joinPath(agentsDir, "openai.yaml"); + const yaml = [ + "interface:", + ` display_name: ${skill.name}`, + ` short_description: ${yamlString(skill.description)}`, + ` default_prompt: ${yamlString(skill.description)}`, + ].join("\n"); + await sandbox.filesystem.writeFile(openaiYamlPath, `${yaml}\n`); + filesWritten.push(openaiYamlPath); + + for (const asset of skill.assets ?? []) { + const assetPath = joinPath(skillDir, asset.path); + const assetDir = dirnameOf(assetPath); + if (assetDir) await sandbox.filesystem.mkdir(assetDir); + await sandbox.filesystem.writeFile(assetPath, asset.content); + filesWritten.push(assetPath); + } + } + } + + const cliConfigOverrides: string[] = []; + if (plugin.mcpServers) { + for (const [serverName, cfg] of Object.entries(plugin.mcpServers)) { + // Build inline TOML for codex's `-c key=value` flag. + // Codex parses the value as TOML, so command/args/env become a + // TOML table literal. + const parts: string[] = []; + if (cfg.command) parts.push(`command=${tomlString(cfg.command)}`); + if (cfg.args && cfg.args.length > 0) { + const argsLit = cfg.args.map(tomlString).join(","); + parts.push(`args=[${argsLit}]`); + } + if (cfg.env && Object.keys(cfg.env).length > 0) { + const envEntries = Object.entries(cfg.env) + .map(([k, v]) => `${tomlKey(k)}=${tomlString(v)}`) + .join(","); + parts.push(`env={${envEntries}}`); + } + if (cfg.url) parts.push(`url=${tomlString(cfg.url)}`); + cliConfigOverrides.push( + `mcp_servers.${tomlKey(serverName)}={${parts.join(",")}}`, + ); + } + } + + // Hooks intentionally not materialized — see the top-of-file comment + // for why codex 0.130.0's `codex exec` filters untrusted hooks before + // dispatch and the trust hash is internal to the binary. + + return { cliConfigOverrides, homeOverride, filesWritten }; +} + +function joinPath(...parts: string[]): string { + return parts + .filter((p) => p !== "") + .map((p) => p.replace(/\/+$/, "")) + .join("/") + .replace(/\/{2,}/g, "/"); +} + +function dirnameOf(path: string): string | undefined { + const idx = path.lastIndexOf("/"); + return idx > 0 ? path.slice(0, idx) : undefined; +} + +/** Wrap a TOML string scalar. */ +function tomlString(value: string): string { + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} + +/** Quote a TOML bare key when it contains non-bare characters. */ +function tomlKey(value: string): string { + if (/^[A-Za-z0-9_-]+$/.test(value)) return value; + return tomlString(value); +} + +function yamlString(value: string): string { + if (/[:#\n\\"]/.test(value)) { + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; + } + return value; +} diff --git a/packages/agent-runtime/src/plugins/materializers/cursor.ts b/packages/agent-runtime/src/plugins/materializers/cursor.ts new file mode 100644 index 000000000..6a1506067 --- /dev/null +++ b/packages/agent-runtime/src/plugins/materializers/cursor.ts @@ -0,0 +1,141 @@ +import type { + PluginHookEvent, + RunnerSandbox, + RuntimePlugin, +} from "../../types.js"; +import { renderSkillMd } from "../skill-md.js"; + +export interface CursorMaterializeResult { + /** True when at least one MCP server was declared — caller should pass `--approve-mcps` to cursor-agent. */ + hasMcpServers: boolean; + filesWritten: string[]; +} + +/** + * Map Claude-style hook event names to Cursor-style names. + * Cursor uses lowerCamelCase and has a different set; events without + * a clean translation are silently dropped. + */ +const CURSOR_HOOK_EVENT_MAP: Partial> = { + PreToolUse: "preToolUse", + PostToolUse: "afterFileEdit", // closest Cursor analog + Stop: "stop", + UserPromptSubmit: "beforeSubmitPrompt", + // SessionStart: no Cursor equivalent — dropped. +}; + +/** + * Materialize a RuntimePlugin into Cursor's workspace-level config tree + * at `/.cursor/`: + * + * /.cursor/mcp.json (when mcpServers) + * /.cursor/hooks.json (when hooks) + * /.cursor/skills//SKILL.md (per skill) + * + * Caller adds `--approve-mcps` to the cursor-agent invocation when + * `hasMcpServers` is true so headless runs don't silently skip + * unapproved servers. + * + * If multiple plugins materialize into the same workspace, the + * materializer merges into the existing files (last-wins per key). + */ +export async function materializePluginForCursor( + plugin: RuntimePlugin, + sandbox: RunnerSandbox, + workspaceRoot: string, +): Promise { + const cursorDir = joinPath(workspaceRoot, ".cursor"); + await sandbox.filesystem.mkdir(cursorDir); + const filesWritten: string[] = []; + + let hasMcpServers = false; + if (plugin.mcpServers && Object.keys(plugin.mcpServers).length > 0) { + hasMcpServers = true; + const mcpPath = joinPath(cursorDir, "mcp.json"); + // Merge into any existing mcp.json so multiple plugins don't trample. + const existing = await tryReadJson(sandbox, mcpPath); + const merged = { + mcpServers: { + ...(existing?.mcpServers ?? {}), + ...plugin.mcpServers, + }, + }; + await sandbox.filesystem.writeFile( + mcpPath, + JSON.stringify(merged, null, 2), + ); + filesWritten.push(mcpPath); + } + + if (plugin.hooks && plugin.hooks.length > 0) { + const hooksPath = joinPath(cursorDir, "hooks.json"); + const existing = await tryReadJson(sandbox, hooksPath); + const existingHooks = (existing?.hooks ?? {}) as Record< + string, + Array> + >; + for (const hook of plugin.hooks) { + const cursorEvent = CURSOR_HOOK_EVENT_MAP[hook.event]; + if (!cursorEvent) continue; + const entry: Record = { + command: hook.command, + failClosed: hook.failClosed ?? false, + }; + if (!existingHooks[cursorEvent]) existingHooks[cursorEvent] = []; + existingHooks[cursorEvent]!.push(entry); + } + await sandbox.filesystem.writeFile( + hooksPath, + JSON.stringify({ version: 1, hooks: existingHooks }, null, 2), + ); + filesWritten.push(hooksPath); + } + + if (plugin.skills && plugin.skills.length > 0) { + await sandbox.filesystem.mkdir(joinPath(cursorDir, "skills")); + for (const skill of plugin.skills) { + const skillDir = joinPath(cursorDir, "skills", skill.name); + await sandbox.filesystem.mkdir(skillDir); + const skillPath = joinPath(skillDir, "SKILL.md"); + await sandbox.filesystem.writeFile(skillPath, renderSkillMd(skill)); + filesWritten.push(skillPath); + for (const asset of skill.assets ?? []) { + const assetPath = joinPath(skillDir, asset.path); + const assetDir = dirnameOf(assetPath); + if (assetDir) await sandbox.filesystem.mkdir(assetDir); + await sandbox.filesystem.writeFile(assetPath, asset.content); + filesWritten.push(assetPath); + } + } + } + + return { hasMcpServers, filesWritten }; +} + +async function tryReadJson( + sandbox: RunnerSandbox, + path: string, +): Promise | undefined> { + if (!(await sandbox.filesystem.exists(path))) return undefined; + try { + return JSON.parse(await sandbox.filesystem.readFile(path)) as Record< + string, + unknown + >; + } catch { + return undefined; + } +} + +function joinPath(...parts: string[]): string { + return parts + .filter((p) => p !== "") + .map((p) => p.replace(/\/+$/, "")) + .join("/") + .replace(/\/{2,}/g, "/"); +} + +function dirnameOf(path: string): string | undefined { + const idx = path.lastIndexOf("/"); + return idx > 0 ? path.slice(0, idx) : undefined; +} diff --git a/packages/agent-runtime/src/plugins/materializers/index.ts b/packages/agent-runtime/src/plugins/materializers/index.ts new file mode 100644 index 000000000..ff0b04fbf --- /dev/null +++ b/packages/agent-runtime/src/plugins/materializers/index.ts @@ -0,0 +1,12 @@ +export { + type ClaudeMaterializeResult, + materializePluginForClaude, +} from "./claude.js"; +export { + type CodexMaterializeResult, + materializePluginForCodex, +} from "./codex.js"; +export { + type CursorMaterializeResult, + materializePluginForCursor, +} from "./cursor.js"; diff --git a/packages/agent-runtime/src/plugins/resolver.ts b/packages/agent-runtime/src/plugins/resolver.ts new file mode 100644 index 000000000..63555aee7 --- /dev/null +++ b/packages/agent-runtime/src/plugins/resolver.ts @@ -0,0 +1,20 @@ +import type { PluginInput, RuntimePlugin } from "../types.js"; + +/** + * Resolve a PluginInput to a fully-inline RuntimePlugin. + * + * v1 supports inline only — `{ rootPath }` (reading + * `/cyrus-plugin.json` from disk) throws "not yet implemented". + * The contract is locked so callers can write code against rootPath today; + * we'll flesh out disk reading in a follow-up. + */ +export async function resolvePlugin( + input: PluginInput, +): Promise { + if ("rootPath" in input) { + throw new Error( + `plugins.rootPath resolution is not implemented yet — supply an inline RuntimePlugin instead (rootPath=${input.rootPath}).`, + ); + } + return input; +} diff --git a/packages/agent-runtime/src/plugins/skill-md.ts b/packages/agent-runtime/src/plugins/skill-md.ts new file mode 100644 index 000000000..484bbb3ca --- /dev/null +++ b/packages/agent-runtime/src/plugins/skill-md.ts @@ -0,0 +1,28 @@ +import type { PluginSkill } from "../types.js"; + +/** + * Render a PluginSkill into a SKILL.md file body (YAML frontmatter + + * markdown). The same format works for Claude, Cursor, and Codex. + */ +export function renderSkillMd(skill: PluginSkill): string { + const frontmatter: string[] = [ + `name: ${skill.name}`, + `description: ${yamlString(skill.description)}`, + ]; + if (skill.disableModelInvocation) { + frontmatter.push("disable-model-invocation: true"); + } + return `---\n${frontmatter.join("\n")}\n---\n\n${skill.content}\n`; +} + +/** + * Wrap a YAML scalar — if the description contains special chars (`:`, + * `#`, newlines), use a double-quoted form. Otherwise plain. + */ +function yamlString(value: string): string { + if (/[:#\n\\"]/.test(value)) { + const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + return `"${escaped}"`; + } + return value; +} diff --git a/packages/agent-runtime/src/runtime.ts b/packages/agent-runtime/src/runtime.ts new file mode 100644 index 000000000..752837c4c --- /dev/null +++ b/packages/agent-runtime/src/runtime.ts @@ -0,0 +1,176 @@ +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, + HarnessAdapter, + HarnessKind, + NormalizedAgentSessionConfig, + RuntimeCallbacks, + RuntimeHarnessConfig, + RuntimeSecret, + RuntimeVolumeConfig, + SandboxProvider, +} from "./types.js"; + +/** + * Fixed in-sandbox mount point for harness state when the caller opts in to + * `sandbox.persistentState`. Internal — never reaches the public API surface; + * each adapter's {@link HarnessAdapter.buildStateEnv} joins a stable + * subdirectory underneath, so multiple harnesses can share one binding. + */ +const PERSISTENT_STATE_MOUNT_PATH = "/var/cyrus/harness-state"; + +export interface CreateAgentRuntimeOptions< + H extends HarnessKind = HarnessKind, +> { + callbacks?: RuntimeCallbacks; + sandboxProviders?: Record; +} + +/** + * Variant of `CreateAgentSessionConfig` whose `harness` field is + * narrowed to a single `HarnessKind`, so `createAgentSession` can + * infer `H` from the config the caller wrote. + */ +export type CreateAgentSessionConfigFor = Omit< + CreateAgentSessionConfig, + "harness" +> & { + harness: H | (RuntimeHarnessConfig & { kind: H }); +}; + +export class AgentRuntime { + constructor(private readonly options: CreateAgentRuntimeOptions = {}) {} + + async createSession( + config: CreateAgentSessionConfigFor, + ): Promise> { + const normalized = applyPersistentState( + normalizeConfig(config), + getHarnessAdapter, + ); + 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); + // Internal RuntimeAgentSession is non-generic (it operates on the + // loose union); narrow the public return via cast at this boundary + // so callers get the typed handle without the implementation + // having to thread the generic everywhere. + return new RuntimeAgentSession( + normalized, + adapter, + sandbox, + this.options + .callbacks as RuntimeCallbacks /* widen to default for impl */, + ) as unknown as AgentSession; + } +} + +export function createAgentRuntime( + options?: CreateAgentRuntimeOptions, +): AgentRuntime { + return new AgentRuntime(options); +} + +export async function createAgentSession( + config: CreateAgentSessionConfigFor, + 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, + ]), + ); +} + +/** + * If `sandbox.persistentState` is set, attach the matching volume mount and + * inject the harness-specific state-dir env vars the adapter declares via + * {@link HarnessAdapter.buildStateEnv}. Both happen at this seam so consumers + * don't deal with mount paths, subpaths, or `CLAUDE_CONFIG_DIR` / `CURSOR_DATA_DIR` + * directly — they just hand us a volume + a bindingId. + * + * Caller-provided `volumes` and `env` win on conflict (we append/spread our + * additions after them is wrong — we spread theirs LAST so the caller can + * override the state env if they know what they're doing). No-op when: + * - the binding is unset + * - the adapter doesn't implement `buildStateEnv` (harnesses with no + * redirectable state dir) + */ +export function applyPersistentState( + normalized: NormalizedAgentSessionConfig, + getAdapter: (kind: HarnessKind) => HarnessAdapter, +): NormalizedAgentSessionConfig { + const ps = normalized.sandbox.persistentState; + if (!ps) return normalized; + const adapter = getAdapter(normalized.harness.kind); + if (!adapter.buildStateEnv) return normalized; + + const stateVolume: RuntimeVolumeConfig = { + name: ps.volume.name, + mountPath: PERSISTENT_STATE_MOUNT_PATH, + subpath: ps.bindingId, + source: ps.volume.source, + kind: ps.volume.kind, + readOnly: ps.volume.readOnly, + }; + + const stateEnv = adapter.buildStateEnv(PERSISTENT_STATE_MOUNT_PATH); + + return { + ...normalized, + sandbox: { + ...normalized.sandbox, + volumes: [...(normalized.sandbox.volumes ?? []), stateVolume], + }, + env: { ...stateEnv, ...normalized.env }, + }; +} 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..eb7717e40 --- /dev/null +++ b/packages/agent-runtime/src/sandbox/compute-sdk.ts @@ -0,0 +1,311 @@ +import type { + CommandExecutionResult, + RunnerSandbox, + RunnerSandboxCapabilities, + RuntimeSandboxConfig, + SandboxFileEntry, + 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; + 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>; + /** + * 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; +} + +export interface ComputeSdkLike { + sandbox?: { + create(options?: Record): Promise; + getById?(sandboxId: string): Promise; + }; +} + +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 { + 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, + this.options.nativeStreamAdapters, + ); + } + + 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, + snapshotId: config.snapshot, + metadata: config.metadata, + namespace: config.namespace, + name: config.name, + directory: config.workingDirectory, + volumes: translateVolumes(config.volumes), + networkEgress: config.networkEgress, + }); + } +} + +/** + * Bridge our generic {@link RuntimeVolumeConfig} shape to the field names + * provider-native SDKs expect. ComputeSDK's daytona wrapper destructures + * known fields and spreads the rest straight into `@daytonaio/sdk`'s + * `create({ volumes })`, which expects `volumeId` rather than `name` — + * without translation, Daytona sees `volumeId: undefined` and throws + * "Volume 'undefined' not found". We emit BOTH so other providers that + * read `name` (e.g. fly machines, bind-mount providers) still see it. + */ +function translateVolumes( + volumes: RuntimeSandboxConfig["volumes"], +): unknown[] | undefined { + if (!volumes || volumes.length === 0) return undefined; + return volumes.map((v) => ({ ...v, volumeId: v.name })); +} + +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, + capabilities: RunnerSandboxCapabilities, + config: RuntimeSandboxConfig, + extraStreamAdapters?: readonly NativeStreamAdapter[], + ) { + 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); + + // 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( + 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 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(); + 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..5297f02d5 --- /dev/null +++ b/packages/agent-runtime/src/sandbox/local.ts @@ -0,0 +1,313 @@ +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, + SandboxStreamCommandOptions, +} 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, +}; + +/** + * 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; +} + +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 ?? LOCAL_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 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; + } +} + +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); + } +} + +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( + new Error( + "Background commands are not supported by LocalSandboxProvider.", + ), + ); + } + + 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: [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(() => { + child.kill("SIGTERM"); + }, options.timeout); + } + + 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. + } + } + }); + 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); + } + }); + 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, + 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/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 new file mode 100644 index 000000000..0d217de84 --- /dev/null +++ b/packages/agent-runtime/src/schemas.ts @@ -0,0 +1,229 @@ +import { z } from "zod"; + +export const HarnessKindSchema = z.enum([ + "claude", + "codex", + "cursor", + "gemini", + "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(), + snapshot: 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(), + subpath: z.string().optional(), + }), + ) + .optional(), + persistentState: z + .object({ + volume: z.object({ + name: z.string().min(1), + source: z.string().optional(), + kind: z.enum(["bind", "fuse", "provider"]).optional(), + readOnly: z.boolean().optional(), + }), + // bindingId becomes the volume subpath. Reject anything that + // could escape the mount root or land on a sibling binding. + bindingId: z + .string() + .min(1) + .refine( + (s) => !s.includes("..") && !s.startsWith("/"), + "bindingId must not contain '..' or start with '/'", + ), + }) + .optional(), + networkEgress: RuntimeNetworkEgressConfigSchema.optional(), + destroyWhileInactive: z.boolean().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(), + agentSessionsRoot: z.string().optional(), + 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(), + 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(), + plugins: z + .array( + z.union([ + z.object({ + name: z.string().min(1), + version: z.string().optional(), + description: z.string().optional(), + mcpServers: z + .record( + z.string(), + // MCP server entries are forwarded verbatim to the + // materializer / harness CLI, which interprets the + // full SDK schema (`type`, `tools`, `alwaysLoad`, + // etc.). `.passthrough()` keeps the documented + // fields typed while letting unknown SDK fields + // flow through without being silently stripped. + z + .object({ + type: z.enum(["http", "sse", "stdio"]).optional(), + command: z.string().optional(), + args: z.array(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), + url: z.string().optional(), + httpUrl: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), + }) + .passthrough(), + ) + .optional(), + hooks: z + .array( + z.object({ + event: z.enum([ + "PreToolUse", + "PostToolUse", + "SessionStart", + "Stop", + "UserPromptSubmit", + ]), + command: z.string().min(1), + matcher: z.string().optional(), + timeout: z.number().int().positive().optional(), + failClosed: z.boolean().optional(), + }), + ) + .optional(), + skills: z + .array( + z.object({ + name: z.string().min(1), + description: z.string().min(1), + content: z.string(), + disableModelInvocation: z.boolean().optional(), + assets: z + .array( + z.object({ + path: z.string().min(1), + content: z.string(), + }), + ) + .optional(), + }), + ) + .optional(), + }), + z.object({ rootPath: z.string().min(1) }), + ]), + ) + .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(), + interactiveInput: z.boolean().optional(), + resumeHarnessSessionId: z.string().min(1).optional(), +}); diff --git a/packages/agent-runtime/src/session.ts b/packages/agent-runtime/src/session.ts new file mode 100644 index 000000000..f25b8e7e8 --- /dev/null +++ b/packages/agent-runtime/src/session.ts @@ -0,0 +1,922 @@ +import { EventEmitter } from "node:events"; +import { mkdir } from "node:fs/promises"; +import { homedir } from "node:os"; +import { dirname, isAbsolute, join, resolve } from "node:path"; +import { + materializeFolderIntoSandbox, + materializeRepositoryIntoSandbox, + syncFolderBackToHost, +} from "./materializers/index.js"; +import { + materializePluginForClaude, + materializePluginForCodex, + materializePluginForCursor, + resolvePlugin, +} from "./plugins/index.js"; +// (Daytona stop/start support — see pauseSandboxIfApplicable.) +import type { + AgentSession, + AgentSessionResult, + HarnessAdapter, + McpServerRuntimeConfig, + NormalizedAgentSessionConfig, + RunnerSandbox, + RuntimeCallbacks, + RuntimeFolderConfig, + 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); + }); + }, + }; + } +} + +/** + * 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); + } +} + +const DEFAULT_AGENT_SESSIONS_ROOT = join(homedir(), ".cyrus-agent-sessions"); + +function resolveAgentSessionsRoot(configuredRoot: string | undefined): string { + const root = configuredRoot ?? DEFAULT_AGENT_SESSIONS_ROOT; + return isAbsolute(root) ? root : resolve(process.cwd(), root); +} + +/** + * Try to fetch a native @daytonaio/sdk Sandbox out of a ComputeSDK- + * wrapped sandbox via the `getInstance()` escape hatch. Used by the + * destroyWhileInactive code path to call `.start()` / `.stop()` on the + * native sandbox (ComputeSDK doesn't expose lifecycle control). + * + * Returns `undefined` for sandboxes that aren't Daytona-shaped (e.g. + * local provider, or a Daytona sandbox wrapped without `getInstance`). + */ +function tryNativeDaytonaSandbox( + sandbox: RunnerSandbox, +): { start(): Promise; stop(): Promise } | undefined { + const candidate = ( + sandbox as unknown as { sandbox?: { getInstance?: () => unknown } } + ).sandbox; + const instance = candidate?.getInstance?.(); + if (!instance || typeof instance !== "object") return undefined; + const obj = instance as { start?: unknown; stop?: unknown }; + if (typeof obj.start === "function" && typeof obj.stop === "function") { + return obj as { start(): Promise; stop(): Promise }; + } + return undefined; +} + +// Does NOT formally `implements AgentSession` — the public interface is +// generic over the harness kind, but internally we work with the loose +// `TranscriptEvent` form. The factory in `runtime.ts` casts at the +// boundary (`as unknown as AgentSession`), which is the type-safe place +// to thread the generic through. +export class RuntimeAgentSession extends EventEmitter { + 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 readonly sessionStateDir: string; + /** + * Per-readwrite-folder ledger of files we materialized in, so sync-back + * (at session.destroy()) can re-read them even if the agent didn't + * touch them. + */ + private readonly folderLedger = new Map< + RuntimeFolderConfig, + readonly string[] + >(); + + private materializationDone = false; + private turnCount = 0; + private sandboxDestroyed = false; + private sandboxDestroyPromise?: Promise; + /** + * Outputs from plugin materialization on the first turn, persisted + * for re-use on subsequent turns (the adapter's `buildCommand` needs + * them every turn to wire CLI flags consistently). + */ + private pluginOutputs: { + claudePluginDirs: string[]; + claudeMcpConfigPath?: string; + cursorHasMcpServers: boolean; + codexConfigOverrides: string[]; + codexHomeOverride?: string; + } = { + claudePluginDirs: [], + cursorHasMcpServers: false, + codexConfigOverrides: [], + }; + + private readonly sandbox: RunnerSandbox; + /** + * When `true`, the session pauses the underlying sandbox between + * runs (Daytona: `stop()`) and resumes it on the next `run()`. The + * sandbox itself is the same object across runs — only its + * running/stopped state toggles. State on disk inside the sandbox + * (including `~/.claude/`) is preserved by Daytona during stop. + */ + private readonly destroyWhileInactive: boolean; + private sandboxIsPaused = false; + + // Per-run state — created fresh in run(), cleared in finally. + private currentRunAbort?: AbortController; + private currentInputBuffer?: AsyncEventBuffer; + private currentRunStreaming = false; + + constructor( + private readonly config: NormalizedAgentSessionConfig, + private readonly adapter: HarnessAdapter, + sandbox: RunnerSandbox, + private readonly callbacks: RuntimeCallbacks = {}, + ) { + super(); + this.sessionId = config.sessionId; + this.harness = adapter.kind; + this.events = this.eventBuffer; + this.sandbox = sandbox; + this.sessionStateDir = join( + resolveAgentSessionsRoot(config.agentSessionsRoot), + this.sessionId, + ); + this.destroyWhileInactive = + Boolean(config.sandbox.destroyWhileInactive) && + config.sandbox.provider === "daytona"; + } + + /** + * Resume the sandbox if it was paused after a previous run. + * No-op on first run (sandbox is freshly created and running) and + * when destroyWhileInactive is off. + */ + private async resumeSandboxIfApplicable(): Promise { + if (!this.destroyWhileInactive) return; + if (!this.sandboxIsPaused) return; + const native = tryNativeDaytonaSandbox(this.sandbox); + if (!native) { + await this.emitEvent( + this.createEvent("sandbox.resume.skipped", { + reason: "no native start/stop on sandbox (provider not Daytona?)", + }), + ); + return; + } + await this.emitEvent(this.createEvent("sandbox.resume.started", {})); + const t0 = Date.now(); + await native.start(); + this.sandboxIsPaused = false; + await this.emitEvent( + this.createEvent("sandbox.resume.completed", { + durationMs: Date.now() - t0, + }), + ); + } + + /** + * Pause the sandbox between runs so the operator stops paying for + * idle compute. State on disk inside the sandbox is preserved. + */ + private async pauseSandboxIfApplicable(): Promise { + if (!this.destroyWhileInactive) return; + if (this.sandboxIsPaused) return; + const native = tryNativeDaytonaSandbox(this.sandbox); + if (!native) return; + await this.emitEvent(this.createEvent("sandbox.pause.started", {})); + const t0 = Date.now(); + try { + await native.stop(); + this.sandboxIsPaused = true; + await this.emitEvent( + this.createEvent("sandbox.pause.completed", { + durationMs: Date.now() - t0, + }), + ); + } catch (err) { + await this.emitEvent( + this.createEvent("sandbox.pause.failed", { + error: err instanceof Error ? err.message : String(err), + }), + ); + } + } + + /** + * Run one turn of the harness. First call materializes files/folders/ + * repos and runs setup commands; subsequent calls skip all of that and + * invoke the harness with its resume flag so it continues the prior + * conversation from the session's persistent state backing. + */ + async run(userPrompt: string): Promise { + const turnIndex = this.turnCount; + const continueSession = turnIndex > 0; + + const abortCtrl = new AbortController(); + const inputBuffer = new AsyncEventBuffer(); + this.currentRunAbort = abortCtrl; + this.currentInputBuffer = inputBuffer; + + const eventStartIndex = this.observedEvents.length; + const startedAt = Date.now(); + let runStopped = false; + + try { + // destroyWhileInactive mode: resume the sandbox if we paused it + // after a previous run. State on disk (incl. `~/.claude/`) + // persists across stop/start so this is cheap and Claude's + // `--continue` still finds the prior session. + await this.resumeSandboxIfApplicable(); + + if (!this.materializationDone) { + await this.ensureSessionStateDir(); + await this.materializeFiles(); + await this.materializeFolders(); + await this.materializeRepositories(); + await this.materializePlugins(); + await this.runSetupCommands(); + this.materializationDone = true; + } + + const command = this.adapter.buildCommand(this.config, { + userPrompt, + continueSession, + pluginOutputs: { + claudePluginDirs: this.pluginOutputs.claudePluginDirs, + claudeMcpConfigPath: this.pluginOutputs.claudeMcpConfigPath, + cursorHasMcpServers: this.pluginOutputs.cursorHasMcpServers, + codexConfigOverrides: this.pluginOutputs.codexConfigOverrides, + codexHomeOverride: this.pluginOutputs.codexHomeOverride, + }, + }); + const fullCommand = [ + command.command, + ...command.args.map(shellQuote), + ].join(" "); + // HOME defaults to the sandbox's natural HOME (host's real one + // for local; /home/daytona inside Daytona), so Claude's + // `~/.claude/` is naturally visible / survives stop/start. + // Codex is the exception: skill discovery is rooted at + // `$HOME/.agents/skills/` (verified empirically), so when the + // codex materializer wrote skills to a per-session HOME root, + // we need to override HOME in the harness env for codex to + // see them. + const codexHomeOverride: Record = + this.harness === "codex" && this.pluginOutputs.codexHomeOverride + ? { HOME: this.pluginOutputs.codexHomeOverride } + : {}; + const env: Record = { + ...codexHomeOverride, + ...this.config.env, + ...(command.env ?? {}), + ...this.materializeSecrets(), + }; + const cwd = this.config.sandbox.workingDirectory; + + const canStream = + typeof this.sandbox.streamCommand === "function" && + this.sandbox.capabilities.streamingProcess === true; + + let exitCode: number; + if (canStream) { + this.currentRunStreaming = true; + const stdoutSplitter = new LineSplitter(); + const stderrSplitter = new LineSplitter(); + const inputIterable = this.config.interactiveInput + ? inputBuffer + : undefined; + const result = await this.sandbox.streamCommand!(fullCommand, { + cwd, + env, + signal: abortCtrl.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); + }); + }, + }); + 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; + } + + runStopped = abortCtrl.signal.aborted; + this.turnCount += 1; + + const turnEvents = this.observedEvents.slice( + eventStartIndex, + ) as AgentSessionResult["events"]; + return { + sessionId: this.sessionId, + harness: this.harness, + success: exitCode === 0 && !runStopped, + exitCode, + // extractResult/extractSessionId run over the FULL transcript + // (not just turnEvents) — the harness session id is usually + // emitted in a `system.init` event on turn 1 and stays + // referenced by every subsequent turn, so per-turn-slice + // would miss it on continuation turns. + result: this.adapter.extractResult?.(this.observedEvents), + harnessSessionId: this.adapter.extractSessionId?.(this.observedEvents), + events: turnEvents, + destroy: () => this.destroySandboxOnce(), + }; + } 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); + const turnEvents = this.observedEvents.slice( + eventStartIndex, + ) as AgentSessionResult["events"]; + return { + sessionId: this.sessionId, + harness: this.harness, + success: false, + error: err, + harnessSessionId: this.adapter.extractSessionId?.(this.observedEvents), + events: turnEvents, + destroy: () => this.destroySandboxOnce(), + }; + } finally { + this.currentRunStreaming = false; + inputBuffer.close(); + this.currentInputBuffer = undefined; + this.currentRunAbort = undefined; + // destroyWhileInactive: pause the sandbox so the operator stops + // paying for idle compute. State on disk is preserved by Daytona + // during stop, so the next run()'s resumeSandboxIfApplicable() + // brings it back instantly with `--continue`-friendly state. + await this.pauseSandboxIfApplicable(); + } + } + + async addMessage(message: string): Promise { + this.queuedMessages.push(message); + await this.emitEvent(this.createEvent("message.queued", { message })); + // Route into the current run's stdin only when interactive input is on + // AND a run is actively streaming. Outside a run, messages stay in + // the queue (observable via getQueuedMessages()) — callers can drain + // them or feed them to the next run() themselves. + if ( + this.currentRunStreaming && + this.config.interactiveInput && + this.currentInputBuffer + ) { + const wire = message.endsWith("\n") ? message : `${message}\n`; + this.currentInputBuffer.push(wire); + } + } + + async interrupt(reason?: string): Promise { + await this.emitEvent(this.createEvent("interrupt.requested", { reason })); + } + + async stop(reason?: string): Promise { + // Per-run cancel only. Does NOT destroy the sandbox or close the + // session-wide event stream — those live until destroy(). + if (!this.currentRunAbort) return; + await this.emitEvent(this.createEvent("stop.requested", { reason })); + this.currentRunAbort.abort(); + this.currentInputBuffer?.close(); + } + + async destroy(): Promise { + // If a run is still in flight, cancel it first so the harness exits. + if (this.currentRunAbort) { + await this.stop("destroy"); + } + // If we paused the sandbox after the last run, resume it briefly + // so the syncFoldersBack walk has something to read from. + await this.resumeSandboxIfApplicable(); + await this.syncFoldersBack(); + await this.destroySandboxOnce(); + this.eventBuffer.close(); + } + + /** + * Idempotent sandbox teardown. Backs both `AgentSession.destroy()` and + * `AgentSessionResult.destroy()`, so callers can call either or both + * without double-destroying the underlying 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; + } + + /** + * Snapshot of every event observed so far on this session, in + * insertion order. See {@link AgentSession.transcript} for usage. + * + * Returns a fresh copy of the internal buffer so callers can't + * accidentally mutate session state. + */ + transcript(): readonly TranscriptEvent[] { + return [...this.observedEvents]; + } + + private async parseBufferedOutput( + 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); + // `callbacks` is typed for the public boundary — its callback + // expects `TranscriptEvent` where H is + // chosen by the caller of `createAgentSession`. Internally we + // hold events as `TranscriptEvent` because the runtime + // is harness-agnostic. The cast at this single boundary is the + // type-safe equivalent of the cast already happening at the + // `createAgentSession` factory. + await ( + this.callbacks.onTranscriptEvent as + | ((e: TranscriptEvent) => Promise | void) + | undefined + )?.(event); + } + + /** + * Ensure the per-session state-backing directory exists on the host. + * The harness process's HOME is set to this directory so that, for + * Claude / Codex / Gemini, the per-session `.claude` / `.codex` / + * `.gemini` subdir is isolated and resumable. + */ + private async ensureSessionStateDir(): Promise { + await mkdir(this.sessionStateDir, { recursive: true }); + // For each state directory the harness declares, pre-create it so + // the harness CLI doesn't fail on first write to a missing parent. + for (const rel of this.adapter.stateDirectories) { + await mkdir(join(this.sessionStateDir, rel), { recursive: true }); + } + } + + 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 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 materializePlugins(): Promise { + const plugins = this.config.plugins ?? []; + if (plugins.length === 0) return; + + // Per-harness root paths inside the sandbox. + const workspaceRoot = this.config.sandbox.workingDirectory ?? "/"; + const claudePluginsRoot = `${workspaceRoot.replace(/\/$/, "")}/.cyrus-plugins`; + // Codex skills live at $HOME/.agents/skills/. For local provider use + // a per-session tmp HOME so we don't trample the user's real one; + // for remote sandboxes use the sandbox's natural home (/home/daytona) + // which is isolated by being a fresh container. + const codexHomeRoot = + this.config.sandbox.provider === "local" + ? this.sessionStateDir + : workspaceRoot; + + // Claude's `--mcp-config` flag is a single path — feeding it the + // last plugin's per-plugin `.mcp.json` would silently drop earlier + // plugins' servers (especially harmful with `--strict-mcp-config`). + // Accumulate every plugin's mcpServers map and, after the loop, + // write one combined config that `--mcp-config` points at. The + // per-plugin `.mcp.json` files inside each plugin dir are still + // written by the materializer because they're part of the documented + // Claude plugin layout (used by `--plugin-dir` consumers); the + // aggregated file is purely the handoff target for `--mcp-config`. + const combinedClaudeMcpServers: Record = {}; + + for (const input of plugins) { + const plugin = await resolvePlugin(input); + await this.emitEvent( + this.createEvent("plugin.materialize.started", { + name: plugin.name, + harness: this.harness, + }), + ); + try { + if (this.harness === "claude") { + const out = await materializePluginForClaude( + plugin, + this.sandbox, + claudePluginsRoot, + ); + this.pluginOutputs.claudePluginDirs.push(out.pluginDir); + if (plugin.mcpServers) { + // Later-wins on duplicate server names — same precedence + // you'd get if a user hand-merged two `.mcp.json` files + // by spreading them in order. Plugin order is caller- + // supplied via `config.plugins`, so the caller can + // reorder if a specific shadow is desired. + for (const [name, server] of Object.entries(plugin.mcpServers)) { + combinedClaudeMcpServers[name] = server; + } + } + await this.emitEvent( + this.createEvent("plugin.materialize.completed", { + name: plugin.name, + harness: "claude", + pluginDir: out.pluginDir, + filesWritten: out.filesWritten.length, + }), + ); + } else if (this.harness === "cursor") { + const out = await materializePluginForCursor( + plugin, + this.sandbox, + workspaceRoot, + ); + this.pluginOutputs.cursorHasMcpServers = + this.pluginOutputs.cursorHasMcpServers || out.hasMcpServers; + await this.emitEvent( + this.createEvent("plugin.materialize.completed", { + name: plugin.name, + harness: "cursor", + filesWritten: out.filesWritten.length, + }), + ); + } else if (this.harness === "codex") { + const out = await materializePluginForCodex( + plugin, + this.sandbox, + codexHomeRoot, + ); + this.pluginOutputs.codexConfigOverrides.push( + ...out.cliConfigOverrides, + ); + this.pluginOutputs.codexHomeOverride = out.homeOverride; + await this.emitEvent( + this.createEvent("plugin.materialize.completed", { + name: plugin.name, + harness: "codex", + configOverrides: out.cliConfigOverrides.length, + filesWritten: out.filesWritten.length, + }), + ); + } else { + await this.emitEvent( + this.createEvent("plugin.materialize.skipped", { + name: plugin.name, + harness: this.harness, + reason: "no materializer for this harness", + }), + ); + } + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + await this.emitEvent( + this.createEvent("plugin.materialize.failed", { + name: plugin.name, + harness: this.harness, + error: err.message, + }), + ); + throw err; + } + } + + // Emit the combined Claude MCP config (one file holding every + // plugin's mcpServers) and use it as the single `--mcp-config` + // value. Skip when no plugin contributed any servers. + if ( + this.harness === "claude" && + Object.keys(combinedClaudeMcpServers).length > 0 + ) { + const combinedPath = `${claudePluginsRoot}/.mcp.combined.json`; + await this.sandbox.filesystem.writeFile( + combinedPath, + JSON.stringify({ mcpServers: combinedClaudeMcpServers }, null, 2), + ); + this.pluginOutputs.claudeMcpConfigPath = combinedPath; + } + } + + 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. + } + } + this.folderLedger.clear(); + } + + 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..173b666bb --- /dev/null +++ b/packages/agent-runtime/src/types.ts @@ -0,0 +1,762 @@ +export type HarnessKind = "claude" | "codex" | "cursor" | "gemini" | "opencode"; + +// Type-only imports — for Claude / Codex / Gemini / OpenCode these are +// devDependencies and never reach the bundle. `@cursor/sdk` is a real +// runtime dep because our vendored cursor driver (cursor-driver.ts) +// imports it as a value; the *type* used here lives in the same package +// as the value imports there, so we have a single source of truth and +// the wire format is the SDK union by construction. +// +// Each row is empirically verified to match what reaches the runtime as +// a JSONL line on the corresponding harness's stdout: +// - claude / codex / gemini: the harness CLI emits these directly +// - opencode: CLI envelope wraps SDK `Part`; envelope declared locally +// (OpenCodeStreamEvent below) +// - cursor: our vendored driver emits SDKMessage directly — no drift +// possible because we own the producer +import type { SDKMessage as ClaudeSDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import type { SDKMessage as CursorSDKMessage } from "@cursor/sdk"; +import type { JsonStreamEvent as GeminiJsonStreamEvent } from "@google/gemini-cli-core"; +import type { ThreadEvent as CodexThreadEvent } from "@openai/codex-sdk"; +import type { Part as OpenCodePart } from "@opencode-ai/sdk"; + +/** + * The JSONL envelope `opencode run --format json` writes to stdout. + * + * The CLI wraps each `Part` (from `@opencode-ai/sdk`) in a thin + * `{ type, timestamp, sessionID, part }` envelope. The envelope itself + * is not in the SDK's `Event` union — that's a separate + * server-protocol surface — so we declare the envelope locally. + */ +export interface OpenCodeStreamEvent { + type: "step_start" | "step_finish" | "tool_use" | "text"; + timestamp: number; + sessionID: string; + part: OpenCodePart; +} + +/** + * Type-level lookup from a `HarnessKind` to the upstream-typed shape of + * `TranscriptEvent.raw` for that harness. + * + * `AgentSession` indexes into this so callers who say + * `createAgentSession({ harness: "claude", … })` automatically get + * `event.raw: ClaudeSDKMessage` without any cast. Adding a new harness + * means: add it to `HarnessKind`, add its raw event union here, fill in + * the adapter — the type system enforces the rest. + */ +export type HarnessRawByKind = { + claude: ClaudeSDKMessage; + codex: CodexThreadEvent; + cursor: CursorSDKMessage; + gemini: GeminiJsonStreamEvent; + opencode: OpenCodeStreamEvent; +}; + +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 { + /** + * Optional MCP transport tag. Materialized verbatim into `.mcp.json` + * so it reaches the harness CLI (Claude Code uses this to pick + * between HTTP/SSE/stdio transports). + */ + type?: "http" | "sse" | "stdio"; + command?: string; + args?: string[]; + env?: Record; + url?: string; + /** + * Legacy alias for `url` retained for older callers. Prefer `url` + * with an appropriate `type` for new code. + */ + httpUrl?: string; + headers?: Record; + /** + * Catch-all for additional SDK-defined fields (`tools`, `alwaysLoad`, + * etc.). The runtime forwards every key under each server entry to + * the materializer unchanged — these fields are interpreted by the + * harness, not by us — so the schema is intentionally permissive. + */ + [extraField: string]: unknown; +} + +/** + * Hook event names — a deliberate universal subset that maps cleanly to + * Claude (`PreToolUse`, `PostToolUse`, `SessionStart`, `Stop`, + * `UserPromptSubmit`) and Cursor (`preToolUse`, etc.). Events that exist + * on one harness but not the others are silently dropped by the + * materializer for harnesses that can't translate them. + * + * Codex hooks are deferred for v1 — its hook schema is version-pinned + * and unstable. + */ +export type PluginHookEvent = + | "PreToolUse" + | "PostToolUse" + | "SessionStart" + | "Stop" + | "UserPromptSubmit"; + +export interface PluginHook { + event: PluginHookEvent; + /** Shell command to run when the event fires. */ + command: string; + /** Optional regex matcher over the tool name (PreToolUse/PostToolUse). */ + matcher?: string; + /** Optional per-hook timeout in seconds. Honored by Claude. */ + timeout?: number; + /** Fail-closed semantics for Cursor (true => deny on hook crash). Ignored by Claude. */ + failClosed?: boolean; +} + +export interface PluginSkill { + /** Skill name, used as the directory name and slash-command suffix. */ + name: string; + /** SKILL.md frontmatter `description` — drives auto-invocation. Required. */ + description: string; + /** SKILL.md markdown body (without frontmatter). */ + content: string; + /** If true, the skill is slash-command-only (no model auto-invoke). */ + disableModelInvocation?: boolean; + /** + * Sibling files placed under the skill's directory at + * `/`. Used for `scripts/`, `references/`, etc. + */ + assets?: Array<{ path: string; content: string }>; +} + +/** + * Provider-agnostic plugin shape — bundles MCP servers, hooks, and + * skills. The runtime translates this into harness-native filesystem + * state or CLI flags per the target harness. + * + * Callers can either supply a fully-resolved `RuntimePlugin` inline, or + * point at a directory via `{ rootPath }` (resolver reads + * `/cyrus-plugin.json` and slurps referenced files). v1 + * implements inline only; rootPath is a follow-up. + */ +export interface RuntimePlugin { + name: string; + version?: string; + description?: string; + mcpServers?: Record; + hooks?: PluginHook[]; + skills?: PluginSkill[]; +} + +export type PluginInput = RuntimePlugin | { rootPath: string }; + +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; +} + +/** + * 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; + source?: string; + kind?: "bind" | "fuse" | "provider"; + readOnly?: boolean; + /** + * Provider-defined prefix within the volume to expose at `mountPath`. + * The sandbox sees only data under this subpath; sibling subpaths are + * invisible. Used for per-binding / per-tenant isolation when many + * sandboxes share one provider volume (the Daytona Volumes pattern). + */ + subpath?: string; +} + +export interface RuntimeNetworkEgressConfig { + mode: NetworkEgressMode; + proxyUrl?: string; + allowedHosts?: string[]; + deniedHosts?: string[]; +} + +/** + * Declarative way to make a harness's persistent state survive across + * sandbox lifecycles. The caller picks a backing storage and a stable + * binding identifier; the runtime mounts the backing at a fixed internal + * path and asks the harness adapter (via {@link HarnessAdapter.buildStateEnv}) + * which env vars to set so the harness writes its state there instead + * of under `$HOME`. + * + * Concrete example (Daytona + Claude): + * ```ts + * sandbox: { + * provider: "daytona", + * persistentState: { + * volume: { name: "cyrus-prod-vol", kind: "fuse" }, + * bindingId: threadKey, + * }, + * } + * ``` + * The runtime mounts the volume, uses `bindingId` as the subpath, and the + * Claude adapter contributes `CLAUDE_CONFIG_DIR=/.claude`. A future + * session with the same `bindingId` + same `volume.name` sees the prior + * state on disk and `--resume ` works across brand-new sandboxes. + * + * Caller-facing surface is intentionally minimal: no env-var names, no + * mount paths, no subpath math. + */ +export interface RuntimePersistentState { + /** + * Provider-backed storage for the harness's state directory. Required + * today (only the Daytona path is wired). For providers without volume + * support this field will move to a discriminated shape in a future + * release; for now, set the volume name + kind and the runtime fills + * in the rest. + */ + volume: Omit; + /** + * Stable identifier for this state binding. Same `bindingId` + same + * `volume.name` = the same on-disk state visible across sessions + * (and across brand-new sandboxes for remote providers). Used by the + * runtime as the volume's `subpath`, so it must be a non-empty + * filesystem-safe string (a thread id, conversation id, etc. all + * work). Path traversal attempts (`..`, leading `/`) are rejected. + */ + bindingId: string; +} + +export interface RuntimeSandboxConfig { + provider: "local" | string; + id?: string; + name?: string; + namespace?: string; + workingDirectory?: string; + templateId?: string; + /** + * Optional snapshot identifier to seed the sandbox from. For the + * Daytona provider this maps to the Daytona snapshot name — when set, + * the sandbox boots from that pre-built snapshot instead of the + * default base image. Ignored by providers that do not have a + * snapshot concept. + */ + snapshot?: string; + timeoutMs?: number; + metadata?: Record; + volumes?: RuntimeVolumeConfig[]; + /** + * Make the harness's persistent state (e.g. `~/.claude/`, `~/.cursor/`) + * survive across sandbox lifecycles by mounting a backing store at a + * runtime-internal path and asking the harness adapter which env vars + * to set so the harness writes there. See {@link RuntimePersistentState}. + */ + persistentState?: RuntimePersistentState; + networkEgress?: RuntimeNetworkEgressConfig; + /** + * When `true`, the runtime "pauses" the underlying sandbox while no + * `session.run()` is in flight and resumes it on the next `run()`. + * + * For Daytona this maps to `sandbox.stop()` / `sandbox.start()` — + * stopped sandboxes preserve all on-disk state (so Claude's + * `~/.claude/` survives) and free up compute. Restart is a few + * seconds, far cheaper than a from-scratch sandbox create + setup + * commands. + * + * For the local sandbox the flag is a no-op (local sessions are + * always free). + * + * Trade-off: compute cost vs. resume latency. You stop paying for + * an idle warm sandbox between turns at the cost of a few-second + * resume on the next run. + */ + destroyWhileInactive?: boolean; +} + +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; + env?: Record; + secrets?: Record; + packages?: RuntimePackageConfig; + files?: RuntimeFileConfig[]; + folders?: RuntimeFolderConfig[]; + repositories?: RuntimeRepositoryConfig[]; + /** + * Bundles of MCP servers + hooks + skills materialized into the + * sandbox in a harness-native form. The unified plugin shape is the + * only way to deliver MCP servers to a session — there is no + * standalone `mcps` field. A plugin with `mcpServers` populated and + * `hooks`/`skills` omitted is the standard "MCP-only" carrier. + */ + plugins?: PluginInput[]; + permissions?: RuntimePermissionConfig; + memory?: RuntimeMemoryConfig; + sandbox?: RuntimeSandboxConfig; + networkEgress?: RuntimeNetworkEgressConfig; + metadata?: Record; + /** + * Root host directory under which each session's state backing lives. + * Defaults to `~/.cyrus-agent-sessions/`. Each session gets a + * subdirectory `//`. For the local sandbox the + * subdirectory becomes the harness process's `HOME`, so per-session + * `.claude` / `.codex` / `.gemini` state is naturally isolated and + * resumable across `session.run()` calls. + */ + agentSessionsRoot?: string; + /** + * 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; + /** + * Harness-native session id to resume. Caller-supplied — typically the + * `harnessSessionId` captured from a prior {@link AgentSessionResult}. + * The harness adapter translates it into the appropriate CLI flag + * (e.g. `--resume ` for Claude). Pair with a persistent + * {@link RuntimeVolumeConfig} (or another mechanism that exposes the + * harness's transcript across sandbox lifetimes) so the resumed run + * can actually read its prior conversation. + */ + resumeHarnessSessionId?: string; +} + +/** + * A single event emitted by the runtime — either a lifecycle event + * generated by `RuntimeAgentSession` itself + * (e.g. `"sandbox.resume.started"`, `"file.write.completed"`) or a + * harness-streamed event parsed from the underlying CLI's stdout. + * + * The `TRaw` type parameter lets `AgentSession` thread the + * harness-specific event union through to `raw`, so a consumer that + * created the session with `harness: "claude"` reads + * `event.raw: SDKMessage` with no cast. Lifecycle events still appear + * in the same stream — for those, `raw` is the lifecycle payload + * (a plain object), which doesn't match the harness union; consumers + * that strictly type-check `event.raw` should branch on `event.kind` + * (lifecycle kinds use a `.` convention like + * `"sandbox.pause.completed"`, harness-streamed kinds use the + * upstream's `type` value). + */ +export interface TranscriptEvent { + sessionId: string; + harness: HarnessKind; + timestamp: string; + kind: string; + raw: TRaw; + normalized?: unknown; + metadata?: Record; +} + +export interface HarnessCommand { + command: string; + args: string[]; + env?: Record; + stdin?: string; +} + +/** + * Options passed by `RuntimeAgentSession` to the harness adapter on each + * `run()`. The adapter uses these to construct the per-turn invocation — + * for instance, Claude maps `continueSession: true` to `--continue`. + */ +export interface HarnessRunOptions { + userPrompt: string; + /** + * `true` on every `run()` after the first, signalling the harness + * should resume the prior conversation in the session's backing + * (e.g. Claude `--continue`). `false` on the first run. + */ + continueSession: boolean; + /** + * Outputs from per-harness plugin materializers, surfaced so the + * adapter's `buildCommand` can wire them into the CLI invocation. + * The session populates this on first turn after running the right + * materializer for the harness. + */ + pluginOutputs?: { + /** Claude: directories to pass as `--plugin-dir ` (one per plugin). */ + claudePluginDirs?: string[]; + /** Claude: optional combined mcp config path for `--mcp-config` + `--strict-mcp-config`. */ + claudeMcpConfigPath?: string; + /** Cursor: true when any plugin declared MCP servers — caller appends `--approve-mcps`. */ + cursorHasMcpServers?: boolean; + /** Codex: inline `-c` CLI overrides (e.g. `mcp_servers.={...}`). */ + codexConfigOverrides?: string[]; + /** Codex: HOME override required for skills discovery (`$HOME/.agents/skills/`). */ + codexHomeOverride?: string; + }; +} + +export interface HarnessAdapter { + readonly kind: HarnessKind; + /** + * Relative paths (under `HOME` inside the compute) where the harness + * keeps its session state. The runtime ensures these survive between + * `run()` calls by making the parent (`HOME`) per-session persistent. + * + * - Claude: `[".claude"]` + * - Codex: `[".codex"]` + * - Gemini: `[".gemini"]` + * + * Adapters without a resumable state model leave this empty. + */ + readonly stateDirectories: readonly string[]; + buildCommand( + config: NormalizedAgentSessionConfig, + options: HarnessRunOptions, + ): HarnessCommand; + parseStdoutLine( + line: string, + context: TranscriptParseContext, + ): TranscriptEvent | undefined; + parseStderrLine?( + line: string, + context: TranscriptParseContext, + ): TranscriptEvent | undefined; + extractResult?(events: TranscriptEvent[]): string | undefined; + /** + * Pull the harness-native session id out of the observed transcript + * (e.g. Claude's `system.init.session_id`). Returned on + * {@link AgentSessionResult.harnessSessionId} so callers can persist it + * for the next reply in the same binding. + */ + extractSessionId?(events: TranscriptEvent[]): string | undefined; + /** + * Given the path where the runtime has mounted persistent storage for + * this session, return env vars that point the harness's state + * directory (e.g. `~/.claude/`, `~/.cursor/`) into that mount. + * + * Called by the runtime when {@link RuntimeSandboxConfig.persistentState} + * is set, so the consumer's session config can stay harness-agnostic. + * Adapters that don't support persistent-state redirection can omit + * this; the runtime then leaves the session env unchanged (and the + * persistent-state binding becomes a no-op for that harness). + * + * Shape of the env var depends on which override the harness exposes + * upstream — verified per harness: + * - Claude: `{ CLAUDE_CONFIG_DIR: `${mountPath}/.claude` }` — points + * at the dir itself. + * - Cursor: `{ CURSOR_DATA_DIR: `${mountPath}/.cursor` }` — points at + * the dir itself. + * - Codex: `{ CODEX_HOME: `${mountPath}/.codex` }` — points at the dir + * itself; codex aborts if the path doesn't exist, so the mount must + * already be writable. + * - Gemini: `{ GEMINI_CLI_HOME: mountPath }` — overrides the value of + * `homedir()` inside the CLI; `.gemini` is hardcoded as the suffix, + * so the harness will write under `${mountPath}/.gemini/`. + * - OpenCode: no single override env var upstream; the runtime sets all + * four XDG dirs (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, + * `XDG_CACHE_HOME`) under a scoped subdir so opencode's + * `xdg-basedir`-derived paths all land under the mount. + */ + buildStateEnv?(mountPath: string): Record; +} + +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; +} + +/** + * 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; + 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; + /** + * 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; +} + +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: H; + success: boolean; + exitCode?: number; + result?: string; + error?: Error; + events: TranscriptEvent[]; + /** + * Harness-native session id observed in the transcript (e.g. Claude's + * `system.init.session_id`). Undefined when the harness does not + * surface one or the run failed before emitting it. Callers persist + * this per-binding and pass it back as + * {@link CreateAgentSessionConfig.resumeHarnessSessionId} on the next + * reply so the harness can resume context from the prior turn. + */ + harnessSessionId?: string; + /** + * 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 + extends Omit { + sessionId: string; + harness: RuntimeHarnessConfig; + model?: string; + env: Record; + secrets: Record; + sandbox: RuntimeSandboxConfig; +} + +export interface AgentSession { + readonly sessionId: string; + readonly harness: H; + readonly events: AsyncIterable>; + /** + * Run one turn of the harness against this session. + * + * On the first call, the runtime materializes files/folders/repos, + * runs setup commands, then invokes the harness with the supplied + * prompt. On subsequent calls, materialization and setup are + * skipped and the harness is invoked with its resume flag (Claude: + * `--continue`) so it picks up the prior conversation from the + * session's persistent state backing. + */ + run(userPrompt: string): Promise>; + /** + * Return a snapshot of every event observed so far on this session — + * both lifecycle events (`sandbox.*`, `setup.*`, etc.) and + * harness-streamed events, in the order the runtime saw them. + * + * The returned array is a fresh copy of the internal buffer; mutating + * it has no effect on the session. Subsequent events appended after + * the snapshot was taken will not appear — call again to refresh. + * + * Use cases: cross-turn replay, post-hoc inspection, building a UI + * timeline without consuming the live `events` async iterable, or + * resuming consumption from a known index across reconnects. + */ + transcript(): readonly TranscriptEvent[]; + 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-scripts/plugin-proof.mjs b/packages/agent-runtime/test-scripts/plugin-proof.mjs new file mode 100644 index 000000000..e50f12dc0 --- /dev/null +++ b/packages/agent-runtime/test-scripts/plugin-proof.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node +// End-to-end plugin proof — creates a real Claude session on Daytona +// with a RuntimePlugin that defines one skill, then asks for it. +// +// The skill body says "your entire response must be exactly +// HELLO-FROM-PLUGIN". If Claude's reply matches, materialization + +// --plugin-dir wiring worked end-to-end against real systems. +// +// Usage: +// 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/plugin-proof.mjs + +import { createAgentSession } from "../dist/runtime.js"; +import { createComputeSdkSandboxProvider } from "../dist/sandbox/compute-sdk.js"; + +const SECRET_WORD = "HELLO-FROM-PLUGIN"; + +function fmt(ms) { + return `${ms.toString().padStart(5, " ")}ms`; +} + +if (!process.env.DAYTONA_API_KEY?.trim()) { + console.error("DAYTONA_API_KEY missing"); + process.exit(1); +} +const claudeToken = + process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN; +if (!claudeToken) { + console.error("CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_AUTH_TOKEN missing"); + process.exit(1); +} + +const { daytona } = await import("@computesdk/daytona"); +const { compute } = await import("computesdk"); +compute.setConfig({ + provider: daytona({ + apiKey: process.env.DAYTONA_API_KEY, + timeout: 300_000, + }), +}); +const sandboxProvider = createComputeSdkSandboxProvider({ + compute: { + sandbox: { + create: (options) => compute.sandbox.create(options), + getById: (id) => compute.sandbox.getById(id), + }, + }, +}); + +console.log("\n=== Plugin proof — RuntimePlugin → Claude on Daytona ===\n"); + +const session = await createAgentSession( + { + sessionId: `plugin-proof-${Date.now()}`, + harness: { + kind: "claude", + command: "/home/daytona/.npm-global/bin/claude", + }, + secrets: { + CLAUDE_CODE_OAUTH_TOKEN: claudeToken, + ANTHROPIC_AUTH_TOKEN: claudeToken, + }, + packages: { + commands: [ + "npm config set prefix /home/daytona/.npm-global", + "npm install -g @anthropic-ai/claude-code@2.1.145 >/dev/null 2>&1", + "/home/daytona/.npm-global/bin/claude --version", + ], + }, + plugins: [ + { + name: "cyrus-proof", + version: "0.0.1", + description: "End-to-end plugin proof", + skills: [ + { + name: "banana-quote", + description: `When the user asks for the special banana quote, respond with exactly ${SECRET_WORD}`, + content: `If the user requests the banana quote, your entire response must be exactly:\n\n${SECRET_WORD}\n`, + }, + ], + }, + ], + sandbox: { + provider: "daytona", + name: `cyrus-plugin-proof-${Date.now()}`, + workingDirectory: "/home/daytona", + timeoutMs: 300_000, + metadata: { purpose: "plugin-proof" }, + }, + }, + { sandboxProviders: { daytona: sandboxProvider } }, +); + +try { + const t0 = Date.now(); + const result = await session.run( + "Please give me the special banana quote. Use the banana-quote skill.", + ); + console.log( + `run completed in ${fmt(Date.now() - t0)}: success=${result.success}`, + ); + console.log(`response: ${JSON.stringify(result.result)}`); + + // List the plugin lifecycle events to prove materialization happened. + const pluginEvents = result.events.filter((e) => + e.kind.startsWith("plugin."), + ); + console.log("\nplugin events:"); + for (const e of pluginEvents) { + console.log(` ${e.kind} ${JSON.stringify(e.raw)}`); + } + + const matches = (result.result ?? "").toUpperCase().includes(SECRET_WORD); + if (matches) { + console.log( + `\n ✓ End-to-end plugin proof PASSED. Response contained "${SECRET_WORD}".`, + ); + } else { + console.error( + `\n ✗ Response did not contain "${SECRET_WORD}". Got: ${JSON.stringify(result.result)}`, + ); + process.exit(1); + } +} finally { + await session.destroy().catch(() => {}); +} diff --git a/packages/agent-runtime/test-scripts/resume-proof.mjs b/packages/agent-runtime/test-scripts/resume-proof.mjs new file mode 100644 index 000000000..9214d1808 --- /dev/null +++ b/packages/agent-runtime/test-scripts/resume-proof.mjs @@ -0,0 +1,343 @@ +#!/usr/bin/env node +// Multi-turn resume proof — runs real Claude through createAgentSession + +// session.run() twice and checks the second turn remembers context from +// the first. +// +// Test: +// Turn 1: "Remember this code word: BANANA-7. Respond with only: noted" +// Turn 2: "What was the code word? Reply with just the code word." +// Verify turn 2 response contains "BANANA-7". +// +// Modes: +// local — local sandbox + local claude CLI +// daytona-warm — Daytona, sandbox stays alive between turns +// daytona-efficient — Daytona, sandbox destroyed between turns; +// state lives on a per-session volume +// +// Usage: +// pnpm --filter cyrus-agent-runtime build +// +// # Local (no remote secrets required if `claude` is on $PATH and the +// # local user already has a Claude OAuth login): +// node packages/agent-runtime/test-scripts/resume-proof.mjs local +// +// # Daytona modes (need secrets): +// set -a; source ~/.cyrus/secrets/daytona.env; source ~/.cyrus/secrets/claude.env; set +a +// node packages/agent-runtime/test-scripts/resume-proof.mjs daytona-warm +// node packages/agent-runtime/test-scripts/resume-proof.mjs daytona-efficient + +import { mkdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createAgentSession } from "../dist/runtime.js"; +import { createComputeSdkSandboxProvider } from "../dist/sandbox/compute-sdk.js"; + +const mode = process.argv[2] ?? "local"; +const CODE_WORD = "BANANA-7"; + +function fmt(ms) { + return `${ms.toString().padStart(5, " ")}ms`; +} + +function verifyResume(turn2Result) { + const text = (turn2Result.result ?? "").toUpperCase(); + if (text.includes(CODE_WORD)) { + console.log( + `\n ✓ Resume confirmed: turn-2 response contains "${CODE_WORD}".`, + ); + console.log(` Full response: ${JSON.stringify(turn2Result.result)}`); + return true; + } + console.error( + `\n ✗ Resume FAILED: turn-2 response did not contain "${CODE_WORD}".`, + ); + console.error(` Got: ${JSON.stringify(turn2Result.result)}`); + return false; +} + +const TURN_1 = `Remember this code word for me: ${CODE_WORD}. Respond with exactly one word: noted`; +const TURN_2 = `What was the code word I asked you to remember? Reply with only the code word, nothing else.`; + +async function runLocalMode() { + console.log( + "\n=== Multi-turn resume — LOCAL sandbox, local claude CLI ===\n", + ); + 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) must be set in the environment. " + + "The local claude CLI uses this for headless `-p` mode.", + ); + } + const root = await mkdir(join(tmpdir(), `resume-proof-local-${Date.now()}`), { + recursive: true, + }).then((d) => d ?? join(tmpdir(), `resume-proof-local-${Date.now()}`)); + const agentSessionsRoot = join(tmpdir(), `resume-proof-state-${Date.now()}`); + await mkdir(root, { recursive: true }); + await mkdir(agentSessionsRoot, { recursive: true }); + + try { + const session = await createAgentSession({ + sessionId: `resume-local-${Date.now()}`, + harness: { kind: "claude" }, + sandbox: { provider: "local", workingDirectory: root }, + agentSessionsRoot, + secrets: { + CLAUDE_CODE_OAUTH_TOKEN: claudeToken, + ANTHROPIC_AUTH_TOKEN: claudeToken, + }, + }); + + console.log("turn 1: send code word…"); + const t0 = Date.now(); + const r1 = await session.run(TURN_1); + console.log( + ` turn 1 complete: success=${r1.success} duration=${fmt(Date.now() - t0)}`, + ); + console.log(` response: ${JSON.stringify(r1.result)}`); + if (!r1.success) { + console.error(` turn 1 error: ${r1.error?.message}`); + throw new Error("turn 1 failed"); + } + + console.log("\nturn 2: ask for the code word…"); + const t1 = Date.now(); + const r2 = await session.run(TURN_2); + console.log( + ` turn 2 complete: success=${r2.success} duration=${fmt(Date.now() - t1)}`, + ); + + const passed = verifyResume(r2); + await session.destroy(); + if (!passed) process.exit(1); + } finally { + await rm(root, { recursive: true, force: true }).catch(() => {}); + await rm(agentSessionsRoot, { recursive: true, force: true }).catch( + () => {}, + ); + } +} + +async function runDaytonaWarmMode() { + console.log( + "\n=== Multi-turn resume — DAYTONA, sandbox WARM between turns ===\n", + ); + if (!process.env.DAYTONA_API_KEY) { + throw new Error("DAYTONA_API_KEY is not set."); + } + const claudeToken = + process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN; + if (!claudeToken) { + throw new Error("CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_AUTH_TOKEN not set."); + } + + const { daytona } = await import("@computesdk/daytona"); + const { compute } = await import("computesdk"); + compute.setConfig({ + provider: daytona({ + apiKey: process.env.DAYTONA_API_KEY, + timeout: 300_000, + }), + }); + const sandboxProvider = createComputeSdkSandboxProvider({ + compute: { + sandbox: { + create: (options) => compute.sandbox.create(options), + getById: (id) => compute.sandbox.getById(id), + }, + }, + }); + + const sessionId = `resume-daytona-warm-${Date.now()}`; + const session = await createAgentSession( + { + sessionId, + harness: { + kind: "claude", + command: "/home/daytona/.npm-global/bin/claude", + }, + secrets: { + CLAUDE_CODE_OAUTH_TOKEN: claudeToken, + ANTHROPIC_AUTH_TOKEN: claudeToken, + }, + packages: { + commands: [ + "npm config set prefix /home/daytona/.npm-global", + "npm install -g @anthropic-ai/claude-code@2.1.145 >/dev/null 2>&1", + "/home/daytona/.npm-global/bin/claude --version", + ], + }, + sandbox: { + provider: "daytona", + name: `cyrus-resume-warm-${Date.now()}`, + workingDirectory: "/home/daytona", + timeoutMs: 300_000, + metadata: { purpose: "resume-proof-warm" }, + }, + }, + { sandboxProviders: { daytona: sandboxProvider } }, + ); + + try { + console.log("turn 1: send code word (cold start with install)…"); + const t0 = Date.now(); + const r1 = await session.run(TURN_1); + console.log( + ` turn 1: success=${r1.success} duration=${fmt(Date.now() - t0)} response=${JSON.stringify(r1.result)}`, + ); + console.log( + " turn 1 event kinds:", + r1.events.map((e) => e.kind), + ); + // Dump the LAST event of each kind to see its shape — usually the + // `result` envelope is the one extractResult cares about. + const lastResult = [...r1.events] + .reverse() + .find((e) => e.kind === "result"); + if (lastResult) { + console.log( + " turn 1 last result.raw =", + JSON.stringify(lastResult.raw).slice(0, 400), + ); + } + if (!r1.success) { + console.error(` turn 1 error: ${r1.error?.message}`); + throw new Error("turn 1 failed"); + } + + console.log("\nturn 2: ask for code word (warm sandbox, --continue)…"); + const t1 = Date.now(); + const r2 = await session.run(TURN_2); + console.log( + ` turn 2: success=${r2.success} duration=${fmt(Date.now() - t1)}`, + ); + console.log(` response: ${JSON.stringify(r2.result)}`); + + const passed = verifyResume(r2); + await session.destroy(); + if (!passed) process.exit(1); + } catch (err) { + await session.destroy().catch(() => {}); + throw err; + } +} + +async function runDaytonaEfficientMode() { + console.log( + "\n=== Multi-turn resume — DAYTONA, sandbox DESTROYED between turns (efficiencies) ===\n", + ); + if (!process.env.DAYTONA_API_KEY) { + throw new Error("DAYTONA_API_KEY is not set."); + } + const claudeToken = + process.env.CLAUDE_CODE_OAUTH_TOKEN ?? process.env.ANTHROPIC_AUTH_TOKEN; + if (!claudeToken) { + throw new Error("CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_AUTH_TOKEN not set."); + } + + const { daytona } = await import("@computesdk/daytona"); + const { compute } = await import("computesdk"); + compute.setConfig({ + provider: daytona({ + apiKey: process.env.DAYTONA_API_KEY, + timeout: 300_000, + }), + }); + const sandboxProvider = createComputeSdkSandboxProvider({ + compute: { + sandbox: { + create: (options) => compute.sandbox.create(options), + getById: (id) => compute.sandbox.getById(id), + }, + }, + }); + + // destroyWhileInactive mode pauses the sandbox (Daytona stop()) after + // each run and resumes (Daytona start()) on the next. State on disk + // is preserved by Daytona during stop, so `/home/daytona/.claude/` + // and the installed `claude` binary both survive. + const sessionId = `resume-daytona-eff-${Date.now()}`; + const session = await createAgentSession( + { + sessionId, + harness: { + kind: "claude", + command: "/home/daytona/.npm-global/bin/claude", + }, + secrets: { + CLAUDE_CODE_OAUTH_TOKEN: claudeToken, + ANTHROPIC_AUTH_TOKEN: claudeToken, + }, + packages: { + commands: [ + "npm config set prefix /home/daytona/.npm-global", + "npm install -g @anthropic-ai/claude-code@2.1.145 >/dev/null 2>&1", + "/home/daytona/.npm-global/bin/claude --version", + ], + }, + sandbox: { + provider: "daytona", + name: `cyrus-resume-eff-${Date.now()}`, + workingDirectory: "/home/daytona", + timeoutMs: 300_000, + metadata: { purpose: "resume-proof-efficient" }, + destroyWhileInactive: true, + }, + }, + { sandboxProviders: { daytona: sandboxProvider } }, + ); + + try { + console.log("turn 1: send code word (cold sandbox + install)…"); + const t0 = Date.now(); + const r1 = await session.run(TURN_1); + console.log( + ` turn 1: success=${r1.success} duration=${fmt(Date.now() - t0)} response=${JSON.stringify(r1.result)}`, + ); + if (!r1.success) { + console.error(` turn 1 error: ${r1.error?.message}`); + throw new Error("turn 1 failed"); + } + + // In efficiencies mode the runtime tears down the sandbox after + // run() returns. We pause briefly so any in-flight destroy completes + // and so the operator can verify the sandbox is gone if they want. + console.log("\n (sandbox should be destroyed now — pausing 3s)"); + await new Promise((r) => setTimeout(r, 3000)); + + console.log( + "\nturn 2: ask for code word (cold sandbox, mount volume, --continue)…", + ); + const t1 = Date.now(); + const r2 = await session.run(TURN_2); + console.log( + ` turn 2: success=${r2.success} duration=${fmt(Date.now() - t1)}`, + ); + console.log(` response: ${JSON.stringify(r2.result)}`); + + const passed = verifyResume(r2); + await session.destroy(); + if (!passed) process.exit(1); + } catch (err) { + await session.destroy().catch(() => {}); + throw err; + } +} + +(async () => { + try { + if (mode === "local") await runLocalMode(); + else if (mode === "daytona-warm") await runDaytonaWarmMode(); + else if (mode === "daytona-efficient") await runDaytonaEfficientMode(); + else { + console.error( + `unknown mode: ${mode} (expected 'local', 'daytona-warm', or 'daytona-efficient')`, + ); + process.exit(1); + } + } catch (err) { + console.error("\nProof FAILED:", err); + process.exit(1); + } +})(); diff --git a/packages/agent-runtime/test-scripts/resume-smoke.mjs b/packages/agent-runtime/test-scripts/resume-smoke.mjs new file mode 100644 index 000000000..110c00b0e --- /dev/null +++ b/packages/agent-runtime/test-scripts/resume-smoke.mjs @@ -0,0 +1,103 @@ +// Real-Daytona two-turn resume smoke. Proves Claude remembers turn 1's +// context when turn 2 runs in a brand-new sandbox that mounts the same +// Daytona Volume at CLAUDE_CONFIG_DIR. +// +// Prereqs (env): +// DAYTONA_API_KEY — your Daytona key +// CLAUDE_CODE_OAUTH_TOKEN — portable Claude Code token +// CYRUS_TEST_VOLUME_ID — id of a pre-created Daytona volume +// +// Build first: pnpm --filter cyrus-agent-runtime build +// Run from packages/agent-runtime: node test-scripts/resume-smoke.mjs + +import { daytona } from "@computesdk/daytona"; +import { createAgentSession } from "../dist/index.js"; +import { createComputeSdkSandboxProvider } from "../dist/sandbox/compute-sdk.js"; + +const VOLUME_ID = process.env.CYRUS_TEST_VOLUME_ID; +if (!VOLUME_ID) throw new Error("Set CYRUS_TEST_VOLUME_ID"); + +// Pick a per-run subpath so reruns don't see each other's state. In real +// Cyrus, AgentSessionManager would derive this from its own session id. +const SUBPATH = `smoke/${Date.now()}`; +const MOUNT = "/var/cyrus/context"; + +const provider = createComputeSdkSandboxProvider({ + compute: daytona({ apiKey: process.env.DAYTONA_API_KEY, timeout: 300000 }), +}); +const sandboxProviders = { daytona: provider }; + +function commonConfig(userPrompt, resumeHarnessSessionId) { + return { + harness: { kind: "claude" }, + userPrompt, + env: { + PATH: "/home/daytona/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + CLAUDE_CONFIG_DIR: `${MOUNT}/.claude`, + }, + secrets: { + CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN, + }, + packages: { + commands: [ + "npm config set prefix /home/daytona/.npm-global", + "npm install -g @anthropic-ai/claude-code", + ], + }, + sandbox: { + provider: "daytona", + name: `agent-runtime-resume-${Date.now()}`, + workingDirectory: "/home/daytona", + timeoutMs: 300000, + // Same volume + same subpath on both turns. Sandbox is fresh each + // turn; the volume's contents survive sandbox.destroy(). + volumes: [ + { name: VOLUME_ID, mountPath: MOUNT, subpath: SUBPATH, kind: "fuse" }, + ], + }, + resumeHarnessSessionId, + }; +} + +// ---- Turn 1 ---------------------------------------------------------------- +console.log("[turn 1] starting"); +const session1 = await createAgentSession( + commonConfig("Remember this token: BANANA-7. Reply 'noted'."), + { sandboxProviders }, +); +const result1 = await session1.start(); +await result1.destroy(); + +console.log("[turn 1]", { + success: result1.success, + result: result1.result, + harnessSessionId: result1.harnessSessionId, +}); + +if (!result1.harnessSessionId) { + throw new Error("No harnessSessionId captured on turn 1"); +} + +// ---- Turn 2 — brand-new sandbox, same volume + subpath, resume id --------- +console.log("[turn 2] starting with resume id", result1.harnessSessionId); +const session2 = await createAgentSession( + commonConfig( + "What token did I ask you to remember? Reply with just the token, no extra words.", + result1.harnessSessionId, + ), + { sandboxProviders }, +); +const result2 = await session2.start(); +await result2.destroy(); + +console.log("[turn 2]", { + success: result2.success, + result: result2.result, + harnessSessionId: result2.harnessSessionId, +}); + +const remembered = result2.result?.includes("BANANA-7"); +console.log( + remembered ? "PASS — resume works" : "FAIL — turn 2 did not recall turn 1", +); +process.exit(remembered ? 0 : 1); 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..dcf65558f --- /dev/null +++ b/packages/agent-runtime/test-scripts/streaming-spike.mjs @@ -0,0 +1,369 @@ +#!/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 { createAgentSession } from "../dist/runtime.js"; +import { createComputeSdkSandboxProvider } from "../dist/sandbox/compute-sdk.js"; +import { createLocalSandboxProvider } from "../dist/sandbox/local.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@2.1.145 >/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" }, + 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.run( + "Reply exactly: runtime stream spike ok. Do not call any tools.", + ); + 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/harnesses.test.ts b/packages/agent-runtime/test/harnesses.test.ts new file mode 100644 index 000000000..199d87aa2 --- /dev/null +++ b/packages/agent-runtime/test/harnesses.test.ts @@ -0,0 +1,441 @@ +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" }, + 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", + ]); + }); + + 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"], + }, + }, + { userPrompt: "Fix the failing test" }, + ); + + 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", + // "ask" maps to Claude's "default" — Claude's CLI does not + // accept "ask" verbatim. + "--permission-mode", + "default", + "--allowedTools", + "Read(**),Edit(**)", + "--disallowedTools", + "Bash", + ]); + }); + + it("maps Cyrus's PermissionMode 'bypass' to Claude's 'bypassPermissions'", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + permissions: { mode: "bypass" }, + }, + { userPrompt: "do it" }, + ); + expect(command.args).toContain("--permission-mode"); + const idx = command.args.indexOf("--permission-mode"); + expect(command.args[idx + 1]).toBe("bypassPermissions"); + }); + + it("appends --resume when resumeHarnessSessionId is set", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + resumeHarnessSessionId: "abc-uuid", + }, + { userPrompt: "next turn" }, + ); + expect(command.args).toContain("--resume"); + const resumeAt = command.args.indexOf("--resume"); + expect(command.args[resumeAt + 1]).toBe("abc-uuid"); + }); + + it("extracts the harness-native session id from Claude's init event", () => { + const adapter = getHarnessAdapter("claude"); + const sessionId = adapter.extractSessionId?.([ + { + sessionId: "cy-1", + harness: "claude", + timestamp: new Date().toISOString(), + kind: "system", + raw: { + type: "system", + subtype: "init", + session_id: "claude-uuid-42", + }, + }, + ]); + expect(sessionId).toBe("claude-uuid-42"); + }); + + it("returns undefined when no event carries a session id", () => { + const adapter = getHarnessAdapter("claude"); + const sessionId = adapter.extractSessionId?.([ + { + sessionId: "cy-1", + harness: "claude", + timestamp: new Date().toISOString(), + kind: "text", + raw: "no session id here", + }, + ]); + expect(sessionId).toBeUndefined(); + }); + + it("builds a Codex JSON command", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + harness: { kind: "codex" }, + model: "gpt-5.3-codex", + systemPrompt: "Use the repo style", + permissions: { mode: "auto" }, + }, + { userPrompt: "Implement the feature" }, + ); + + 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 via the host-resolved @cyrus-ai/cursor-runner when harness.command is unset", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + harness: { kind: "cursor" }, + model: "composer-2", + permissions: { mode: "ask" }, + }, + { userPrompt: "Patch the bug" }, + ); + + // Local-provider path: no `harness.command` override, so the + // adapter resolves `@cyrus-ai/cursor-runner` from the host's + // node_modules and spawns `node `. The exact + // filesystem location depends on pnpm/npm linking, so we + // assert the entry filename instead of pinning the whole path. + expect(command.command).toBe("node"); + const runnerPath = command.args[0]!; + expect(runnerPath).toMatch(/cursor-(sdk-)?runner[/\\]dist[/\\]index\.js$/); + expect(command.args.slice(1)).toEqual([ + "--prompt", + "Patch the bug", + "--model", + "composer-2", + "--cwd", + "/tmp/worktree", + ]); + }); + + it("uses harness.command directly as the cursor-runner binary when supplied (Daytona-snapshot mode)", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + harness: { kind: "cursor", command: "cursor-runner" }, + model: "composer-2", + permissions: { mode: "ask" }, + }, + { userPrompt: "Patch the bug" }, + ); + + // Snapshot path: caller supplies `harness.command`, the adapter + // spawns it directly (the runner's `#!/usr/bin/env node` shebang + // makes it executable). The command is whatever the caller + // passed — `"cursor-runner"` for PATH resolution inside the + // sandbox, or an absolute path to pin a specific copy. + expect(command.command).toBe("cursor-runner"); + expect(command.args).toEqual([ + "--prompt", + "Patch the bug", + "--model", + "composer-2", + "--cwd", + "/tmp/worktree", + ]); + }); + + it("threads resumeHarnessSessionId into the cursor runner as --agent-id", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + harness: { kind: "cursor", command: "cursor-runner" }, + resumeHarnessSessionId: "agent-74f4af34", + }, + { userPrompt: "carry on" }, + ); + + // Spawning shape is unchanged — the resume flag is just + // appended to args. The runner reads it and calls + // Agent.resume(agent-74f4af34) instead of Agent.create(). + expect(command.command).toBe("cursor-runner"); + expect(command.args).toContain("--agent-id"); + const idx = command.args.indexOf("--agent-id"); + expect(command.args[idx + 1]).toBe("agent-74f4af34"); + }); + + it("omits --agent-id when resumeHarnessSessionId is not set", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + harness: { kind: "cursor", command: "cursor-runner" }, + }, + { userPrompt: "fresh start" }, + ); + expect(command.args).not.toContain("--agent-id"); + }); + + it("extracts the harness-native agent id from the first cursor SDKMessage", () => { + const adapter = getHarnessAdapter("cursor"); + // Cursor's first stream event is typically a `status` message + // with `agent_id` set — every SDKMessage variant carries it, + // so any of them work. Use the realistic shape from a captured + // run. + const sessionId = adapter.extractSessionId?.([ + { + sessionId: "cy-1", + harness: "cursor", + timestamp: new Date().toISOString(), + kind: "status", + raw: { + type: "status", + agent_id: "agent-74f4af34-9d01-4b98-b271-21ea87c68ca6", + run_id: "run-dc52f12e-1269-49d1-907a-b6c399501c8d", + status: "RUNNING", + }, + }, + { + sessionId: "cy-1", + harness: "cursor", + timestamp: new Date().toISOString(), + kind: "assistant", + raw: { + type: "assistant", + agent_id: "agent-74f4af34-9d01-4b98-b271-21ea87c68ca6", + run_id: "run-dc52f12e-1269-49d1-907a-b6c399501c8d", + message: { + role: "assistant", + content: [{ type: "text", text: "hi" }], + }, + }, + }, + ]); + expect(sessionId).toBe("agent-74f4af34-9d01-4b98-b271-21ea87c68ca6"); + }); + + it("returns undefined when no cursor event carries an agent_id", () => { + const adapter = getHarnessAdapter("cursor"); + const sessionId = adapter.extractSessionId?.([ + { + sessionId: "cy-1", + harness: "cursor", + timestamp: new Date().toISOString(), + kind: "text", + raw: "no agent_id in a plain string event", + }, + ]); + expect(sessionId).toBeUndefined(); + }); + + it("builds a Gemini command with env-backed system prompt", () => { + const command = buildHarnessInvocation( + { + ...baseConfig, + harness: { kind: "gemini" }, + systemPrompt: "System text", + permissions: { mode: "bypass" }, + }, + { userPrompt: "Analyze this" }, + ); + + 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("claude.buildStateEnv joins .claude under the mount path", () => { + const adapter = getHarnessAdapter("claude"); + // `applyPersistentState` always passes the runtime-internal mount + // point — adapters are expected to namespace under a fixed subdir + // so two harnesses can share one binding without colliding. + expect(adapter.buildStateEnv?.("/var/cyrus/harness-state")).toEqual({ + CLAUDE_CONFIG_DIR: "/var/cyrus/harness-state/.claude", + }); + }); + + it("cursor.buildStateEnv joins .cursor under the mount path", () => { + const adapter = getHarnessAdapter("cursor"); + expect(adapter.buildStateEnv?.("/var/cyrus/harness-state")).toEqual({ + CURSOR_DATA_DIR: "/var/cyrus/harness-state/.cursor", + }); + }); + + it("codex.buildStateEnv sets CODEX_HOME to mount/.codex", () => { + // Verified against `codex-rs/utils/home-dir/src/lib.rs::find_codex_home` + // in openai/codex — `CODEX_HOME` is the only env var that + // redirects the dir, no XDG fallback. + const adapter = getHarnessAdapter("codex"); + expect(adapter.buildStateEnv?.("/var/cyrus/harness-state")).toEqual({ + CODEX_HOME: "/var/cyrus/harness-state/.codex", + }); + }); + + it("gemini.buildStateEnv sets GEMINI_CLI_HOME to the mount path itself", () => { + // Verified against `@google/gemini-cli-core` → + // `dist/src/utils/paths.js::homedir`: the env var replaces the + // homedir, and `.gemini` is appended by the CLI itself. So we + // hand it the mount path, not `mount/.gemini`. + const adapter = getHarnessAdapter("gemini"); + expect(adapter.buildStateEnv?.("/var/cyrus/harness-state")).toEqual({ + GEMINI_CLI_HOME: "/var/cyrus/harness-state", + }); + }); + + it("opencode.buildStateEnv sets all four XDG dirs under .opencode-xdg", () => { + // opencode has no single state-dir env var — it derives all four + // storage roots from `xdg-basedir` and appends `/opencode` to each + // (see `Global.make()` in sst/opencode). We scope under + // `.opencode-xdg/` so we don't claim the XDG hierarchy for + // unrelated tools that happen to run in the sandbox. + const adapter = getHarnessAdapter("opencode"); + expect(adapter.buildStateEnv?.("/var/cyrus/harness-state")).toEqual({ + XDG_CONFIG_HOME: "/var/cyrus/harness-state/.opencode-xdg/config", + XDG_DATA_HOME: "/var/cyrus/harness-state/.opencode-xdg/data", + XDG_STATE_HOME: "/var/cyrus/harness-state/.opencode-xdg/state", + XDG_CACHE_HOME: "/var/cyrus/harness-state/.opencode-xdg/cache", + }); + }); + + 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/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 new file mode 100644 index 000000000..fa694d8cf --- /dev/null +++ b/packages/agent-runtime/test/runtime.test.ts @@ -0,0 +1,1078 @@ +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 { getHarnessAdapter } from "../src/harnesses/index.js"; +import { + applyPersistentState, + createAgentSession, + normalizeConfig, +} from "../src/runtime.js"; +import type { + CommandExecutionResult, + RunnerSandbox, + RunnerSandboxCapabilities, + SandboxFilesystem, + SandboxProvider, + SandboxStreamCommandOptions, +} from "../src/types.js"; + +describe("AgentRuntime", () => { + it("normalizes minimal session config", () => { + const config = normalizeConfig({ + harness: "codex", + 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("preserves sandbox.snapshot through normalization", () => { + const config = normalizeConfig({ + harness: "claude", + sandbox: { + provider: "daytona", + snapshot: "cyrus-base-v3", + }, + }); + expect(config.sandbox.snapshot).toBe("cyrus-base-v3"); + }); + + it("applyPersistentState attaches the volume and env when set", () => { + // Caller-facing surface: pick a backing volume + a stable bindingId. + // No knowledge of mount paths, subpath math, or CLAUDE_CONFIG_DIR. + const normalized = normalizeConfig({ + harness: "claude", + sandbox: { + provider: "daytona", + persistentState: { + volume: { name: "cyrus-prod-vol", kind: "fuse" }, + bindingId: "thread-abc", + }, + }, + }); + const result = applyPersistentState(normalized, getHarnessAdapter); + + // Volume gets mounted at the runtime-internal path with bindingId + // as subpath — the same name+bindingId across sandbox lifetimes + // re-exposes the prior state on disk. + expect(result.sandbox.volumes).toEqual([ + { + name: "cyrus-prod-vol", + mountPath: "/var/cyrus/harness-state", + subpath: "thread-abc", + source: undefined, + kind: "fuse", + readOnly: undefined, + }, + ]); + // Claude adapter contributes CLAUDE_CONFIG_DIR pointing into the + // mount — `claude --resume ` now finds the prior transcript. + expect(result.env.CLAUDE_CONFIG_DIR).toBe( + "/var/cyrus/harness-state/.claude", + ); + }); + + it("applyPersistentState is a no-op when persistentState is unset", () => { + const normalized = normalizeConfig({ + harness: "claude", + env: { EXISTING: "1" }, + }); + const result = applyPersistentState(normalized, getHarnessAdapter); + expect(result).toBe(normalized); + }); + + it("applyPersistentState is a no-op when the adapter omits buildStateEnv", () => { + // Defensive: if a future harness adapter doesn't implement + // `buildStateEnv` (no upstream env var for redirecting state), + // declaring persistentState should silently no-op rather than + // mount a volume nobody will read from. Inject a stub adapter + // to simulate that, since today all five real adapters declare + // the method. + const normalized = normalizeConfig({ + harness: "claude", + sandbox: { + provider: "daytona", + persistentState: { + volume: { name: "shared-vol" }, + bindingId: "thread-xyz", + }, + }, + }); + const stubAdapter = { + ...getHarnessAdapter("claude"), + buildStateEnv: undefined, + }; + const result = applyPersistentState(normalized, () => stubAdapter); + expect(result.sandbox.volumes).toBeUndefined(); + expect(result.env.CLAUDE_CONFIG_DIR).toBeUndefined(); + }); + + it("applyPersistentState preserves caller env and existing volumes", () => { + const normalized = normalizeConfig({ + harness: "cursor", + env: { CALLER_VAR: "keep-me" }, + sandbox: { + provider: "daytona", + volumes: [{ name: "logs-vol", mountPath: "/var/log/agent" }], + persistentState: { + volume: { name: "cyrus-prod-vol" }, + bindingId: "thread-def", + }, + }, + }); + const result = applyPersistentState(normalized, getHarnessAdapter); + + expect(result.env.CALLER_VAR).toBe("keep-me"); + expect(result.env.CURSOR_DATA_DIR).toBe("/var/cyrus/harness-state/.cursor"); + expect(result.sandbox.volumes).toHaveLength(2); + expect(result.sandbox.volumes?.[0]).toMatchObject({ name: "logs-vol" }); + expect(result.sandbox.volumes?.[1]).toMatchObject({ + name: "cyrus-prod-vol", + subpath: "thread-def", + }); + }); + + 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", + 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.run("Do it"); + + 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", + packages: { + npm: ["example-cli"], + commands: ["example-cli --version"], + }, + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + }, + ); + + const result = await session.run("Run after setup"); + + 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("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", + }, + { + sandboxProviders: { local: new FakeSandboxProvider(streamingSandbox) }, + callbacks: { + onTranscriptEvent(event) { + arrivals.push({ + kind: event.kind, + elapsedMs: Date.now() - startedAt, + }); + }, + }, + }, + ); + + const result = await session.run("Do it"); + + 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", + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + }, + ); + const result = await session.run("fallback"); + 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", + }, + { + sandboxProviders: { local: new FakeSandboxProvider(streamingSandbox) }, + }, + ); + // Push messages before run — 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.run("no stdin please"); + 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", + 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.run("open a stream"); + // 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 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" }, + 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.run("edit files please"); + // Sync-back happens on session.destroy() now, not at the end of + // run() — call it so the test can assert the host file deltas. + await session.destroy(); + expect(result.success).toBe(true); + + // Materialize events fire inside run(); syncback fires inside destroy() + // — both are in result.events because run()'s event slice happens + // from eventStartIndex through call-time, and destroy ran after. + // Materialize events are guaranteed in result.events. + const kinds = result.events.map((e) => e.kind); + expect(kinds).toContain("folder.materialize.started"); + expect(kinds).toContain("folder.materialize.completed"); + + // Host file deltas prove sync-back ran via destroy(). + 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", + repositories: [ + { + source: "/tmp/upstream", + mountPath: "/work/repo", + branch: "feature", + access: "read", + }, + ], + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const result = await session.run("clone please"); + 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("supports multi-turn run() — first turn fresh, second turn continues", async () => { + // First run is a fresh harness invocation (materializes setup, no + // --continue). Second run skips materialization and passes --continue. + // We verify both by inspecting the recorded sandbox commands. + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "ok" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-multi-turn", + harness: "claude", // claude has stateDirectories: [".claude"] + packages: { commands: ["echo install"] }, + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const r1 = await session.run("first message"); + expect(r1.success).toBe(true); + + const r2 = await session.run("second message"); + expect(r2.success).toBe(true); + + // Setup commands ran only once (first turn). + const setupRuns = sandbox.commands.filter( + (c) => c.command === "echo install", + ); + expect(setupRuns).toHaveLength(1); + + // First harness invocation: no --continue. + // Second: --continue present. + const harnessRuns = sandbox.commands.filter((c) => + c.command.startsWith("claude "), + ); + expect(harnessRuns).toHaveLength(2); + expect(harnessRuns[0]!.command).not.toContain("--continue"); + expect(harnessRuns[1]!.command).toContain("--continue"); + + await session.destroy(); + }); + + 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", + item: { type: "agent_message", text: "done" }, + }), + ); + const session = await createAgentSession( + { + sessionId: "session-destroy", + harness: "codex", + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const result = await session.run("anything"); + expect(result.success).toBe(true); + expect(typeof result.destroy).toBe("function"); + expect(typeof session.destroy).toBe("function"); + expect(sandbox.destroyed).toBe(0); + + // 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 result.destroy() again is a no-op. + await result.destroy(); + expect(sandbox.destroyed).toBe(1); + + // 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", + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const startPromise = session.run("anything"); + 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); + }); + + it("materializes a Claude plugin and wires --plugin-dir into the harness invocation", async () => { + // Verifies: session calls the right materializer, writes plugin + // files into the fake sandbox, and passes the resulting plugin + // dir as `--plugin-dir` on the harness CLI. + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "result", + subtype: "success", + result: "ok", + }), + ); + const session = await createAgentSession( + { + sessionId: "session-plugin-claude", + harness: { kind: "claude" }, + sandbox: { provider: "local", workingDirectory: "/work" }, + plugins: [ + { + name: "demo", + version: "0.0.1", + mcpServers: { foo: { command: "echo", args: ["x"] } }, + hooks: [ + { event: "PreToolUse", command: "echo pre", matcher: "Bash" }, + ], + skills: [ + { + name: "hi", + description: "Greet the user.", + content: "Say hi.", + }, + ], + }, + ], + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + const result = await session.run("hello"); + expect(result.success).toBe(true); + + // Plugin files landed at the expected paths. + const paths = sandbox.files.map((f) => f.path).sort(); + expect(paths).toContain( + "/work/.cyrus-plugins/demo/.claude-plugin/plugin.json", + ); + // The per-plugin `.mcp.json` is still written — it's part of the + // documented Claude plugin layout that `--plugin-dir` consumers + // expect. The canonical handoff target for `--mcp-config` is the + // session-level combined file (see next assertion). + expect(paths).toContain("/work/.cyrus-plugins/demo/.mcp.json"); + expect(paths).toContain("/work/.cyrus-plugins/.mcp.combined.json"); + expect(paths).toContain("/work/.cyrus-plugins/demo/hooks/hooks.json"); + expect(paths).toContain("/work/.cyrus-plugins/demo/skills/hi/SKILL.md"); + + // Harness command got --plugin-dir + --mcp-config + --strict-mcp-config. + // --mcp-config points at the combined file (a single scalar that + // aggregates every plugin's mcpServers), not the per-plugin file. + const harnessCmd = sandbox.commands.at(-1)!.command; + expect(harnessCmd).toContain("--plugin-dir /work/.cyrus-plugins/demo"); + expect(harnessCmd).toContain( + "--mcp-config /work/.cyrus-plugins/.mcp.combined.json", + ); + expect(harnessCmd).toContain("--strict-mcp-config"); + + // Plugin lifecycle events present. + const kinds = result.events.map((e) => e.kind); + expect(kinds).toContain("plugin.materialize.started"); + expect(kinds).toContain("plugin.materialize.completed"); + + // SKILL.md content has the right frontmatter. + const skillFile = sandbox.files.find( + (f) => f.path === "/work/.cyrus-plugins/demo/skills/hi/SKILL.md", + )!; + expect(skillFile.content).toContain("name: hi"); + expect(skillFile.content).toContain("description: Greet the user."); + expect(skillFile.content).toContain("Say hi."); + }); + + it("merges MCP servers across multiple Claude plugins into one --mcp-config", async () => { + // Regression guard. The Claude `--mcp-config` flag is a single + // scalar path. Earlier this code overwrote `claudeMcpConfigPath` + // per plugin, so multi-plugin sessions silently dropped every + // plugin's MCP servers except the last (and `--strict-mcp-config` + // made that fatal for tool calls into the dropped servers). + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "result", + subtype: "success", + result: "ok", + }), + ); + const session = await createAgentSession( + { + sessionId: "session-plugin-claude-merge", + harness: { kind: "claude" }, + sandbox: { provider: "local", workingDirectory: "/work" }, + plugins: [ + { + name: "alpha", + mcpServers: { + alphaTool: { command: "alpha-bin", args: ["--port=1"] }, + }, + }, + { + name: "beta", + mcpServers: { + betaTool: { command: "beta-bin", args: ["--port=2"] }, + }, + }, + { + name: "gamma", + mcpServers: { + gammaTool: { url: "https://gamma.example/sse", type: "sse" }, + }, + }, + ], + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + const result = await session.run("hello"); + expect(result.success).toBe(true); + + // Every plugin's per-plugin `.mcp.json` was still written (the + // documented Claude plugin layout), AND a session-level combined + // file exists. + const paths = sandbox.files.map((f) => f.path); + expect(paths).toContain("/work/.cyrus-plugins/alpha/.mcp.json"); + expect(paths).toContain("/work/.cyrus-plugins/beta/.mcp.json"); + expect(paths).toContain("/work/.cyrus-plugins/gamma/.mcp.json"); + expect(paths).toContain("/work/.cyrus-plugins/.mcp.combined.json"); + + // Combined file has every plugin's servers under one `mcpServers` + // map. Order doesn't matter; presence does. + const combined = JSON.parse( + sandbox.files.find( + (f) => f.path === "/work/.cyrus-plugins/.mcp.combined.json", + )!.content, + ); + expect(Object.keys(combined.mcpServers).sort()).toEqual([ + "alphaTool", + "betaTool", + "gammaTool", + ]); + expect(combined.mcpServers.alphaTool).toEqual({ + command: "alpha-bin", + args: ["--port=1"], + }); + expect(combined.mcpServers.gammaTool).toEqual({ + url: "https://gamma.example/sse", + type: "sse", + }); + + // Harness command points `--mcp-config` at the combined file + // (one scalar, every plugin's servers reachable) — not the + // last plugin's per-plugin file. + const harnessCmd = sandbox.commands.at(-1)!.command; + expect(harnessCmd).toContain( + "--mcp-config /work/.cyrus-plugins/.mcp.combined.json", + ); + expect(harnessCmd).not.toContain( + "--mcp-config /work/.cyrus-plugins/gamma/.mcp.json", + ); + // All three plugin dirs reach the CLI as `--plugin-dir`. + expect(harnessCmd).toContain("--plugin-dir /work/.cyrus-plugins/alpha"); + expect(harnessCmd).toContain("--plugin-dir /work/.cyrus-plugins/beta"); + expect(harnessCmd).toContain("--plugin-dir /work/.cyrus-plugins/gamma"); + }); + + it("prefers later-listed Claude plugin's server on duplicate names", async () => { + // Plugin order is caller-supplied, so the caller can deliberately + // shadow an earlier server by listing the replacement plugin later. + // We document and lock in that precedence here. + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "result", + subtype: "success", + result: "ok", + }), + ); + const session = await createAgentSession( + { + sessionId: "session-plugin-claude-shadow", + harness: { kind: "claude" }, + sandbox: { provider: "local", workingDirectory: "/work" }, + plugins: [ + { + name: "base", + mcpServers: { + shared: { command: "old-bin" }, + }, + }, + { + name: "override", + mcpServers: { + shared: { command: "new-bin" }, + }, + }, + ], + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + await session.run("hello"); + + const combined = JSON.parse( + sandbox.files.find( + (f) => f.path === "/work/.cyrus-plugins/.mcp.combined.json", + )!.content, + ); + expect(combined.mcpServers.shared).toEqual({ command: "new-bin" }); + }); + + 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", + files: [ + { + path: "/home/daytona/.codex/auth.json", + content: "secret-auth-json", + sensitive: true, + }, + ], + }, + { + sandboxProviders: { local: new FakeSandboxProvider(sandbox) }, + }, + ); + + const result = await session.run("Run after files"); + + 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]", + }, + }, + ]); + }); + + it("threads resumeHarnessSessionId into claude --resume and surfaces harnessSessionId on the result", async () => { + // End-to-end through createAgentSession: caller hands in the prior + // harness session id (the AgentSessionManager owns this mapping in + // real Cyrus), the Claude adapter adds --resume, and the new id + // emitted by the run lands on the result for the caller to persist. + const sandbox = new FakeSandbox( + JSON.stringify({ + type: "system", + subtype: "init", + session_id: "claude-new-uuid", + }), + ); + const session = await createAgentSession( + { + sessionId: "session-resume", + harness: "claude", + resumeHarnessSessionId: "claude-prior-uuid", + }, + { sandboxProviders: { local: new FakeSandboxProvider(sandbox) } }, + ); + + const result = await session.run("follow-up"); + + expect(result.success).toBe(true); + expect(result.harnessSessionId).toBe("claude-new-uuid"); + // `commands[0].args` includes --resume + the prior session id; + // joining with spaces lets us assert without depending on exact + // argument order. + const fullCmdline = [ + sandbox.commands[0]?.command, + ...(sandbox.commands[0]?.args ?? []), + ].join(" "); + expect(fullCmdline).toContain("--resume claude-prior-uuid"); + }); +}); + +class FakeSandboxProvider implements SandboxProvider { + readonly provider = "local"; + + constructor(private readonly sandbox: RunnerSandbox) {} + + async create(): Promise { + return this.sandbox; + } +} + +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; + destroyed = 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 = ""; + let exitCode = 0; + for (const event of this.schedule) { + // 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); + } + 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, + durationMs: Date.now() - startedAt, + }; + } + + async destroy(): Promise { + this.destroyed += 1; + } +} + +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; + }> = []; + destroyed = 0; + + 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 { + this.destroyed += 1; + } +} diff --git a/packages/agent-runtime/test/sandbox.test.ts b/packages/agent-runtime/test/sandbox.test.ts new file mode 100644 index 000000000..1170d8966 --- /dev/null +++ b/packages/agent-runtime/test/sandbox.test.ts @@ -0,0 +1,433 @@ +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(true); + + 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 }); + } + }); + + 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", () => { + 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", + snapshotId: undefined, + 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"], + ]); + }); + + 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("forwards RuntimeSandboxConfig.snapshot as snapshotId on create", async () => { + let received: Record | undefined; + const fakeSandbox: ComputeSdkSandboxLike = { + sandboxId: "sbx_snap", + provider: "daytona", + 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 }; + }, + async destroy() {}, + }; + const compute = { + sandbox: { + async create(options: Record) { + received = options; + return fakeSandbox; + }, + }, + }; + const provider = createComputeSdkSandboxProvider({ compute }); + await provider.create({ + provider: "daytona", + snapshot: "cyrus-base-v3", + }); + expect(received?.snapshotId).toBe("cyrus-base-v3"); + }); + + 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, + ); + }); +}); diff --git a/packages/agent-runtime/test/typed-events.test.ts b/packages/agent-runtime/test/typed-events.test.ts new file mode 100644 index 000000000..150295bcb --- /dev/null +++ b/packages/agent-runtime/test/typed-events.test.ts @@ -0,0 +1,100 @@ +/** + * Type-narrowing smoke tests. These are intentionally compile-time + * assertions — if the generics don't propagate correctly the file + * fails `tsc`. The runtime `expect`s are belt-and-suspenders. + */ +import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import type { SDKMessage as CursorSDKMessage } from "@cursor/sdk"; +import type { JsonStreamEvent } from "@google/gemini-cli-core"; +import type { ThreadEvent } from "@openai/codex-sdk"; +import { describe, expect, it } from "vitest"; +import type { + AgentSession, + AgentSessionResult, + HarnessRawByKind, + OpenCodeStreamEvent, + TranscriptEvent, +} from "../src/types.js"; + +// Helper: assert two types are equal at the type level. +type Equals = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? true + : false; +type ExpectTrue = T; + +describe("typed events — compile-time narrowing", () => { + it("AgentSession<'claude'> events carry SDKMessage as raw", () => { + // If the generic narrowing is broken this file fails tsc, not vitest. + type _Claude = ExpectTrue>; + type _Codex = ExpectTrue>; + type _Gemini = ExpectTrue< + Equals + >; + type _OpenCode = ExpectTrue< + Equals + >; + type _Cursor = ExpectTrue< + Equals + >; + + // Use a fake to anchor the test in the runtime too. + type ClaudeSession = AgentSession<"claude">; + const fakeEvent = { + sessionId: "s", + harness: "claude" as const, + timestamp: "t", + kind: "assistant", + raw: { type: "assistant" } as unknown as SDKMessage, + } satisfies TranscriptEvent; + expect(fakeEvent.harness).toBe("claude"); + }); + + it("AgentSessionResult<'codex'>.events are typed to ThreadEvent", () => { + type CodexResult = AgentSessionResult<"codex">; + type _Check = ExpectTrue< + Equals[]> + >; + type _HarnessTag = ExpectTrue>; + expect(true).toBe(true); + }); + + it("Defaulted AgentSession (no H) keeps a usable union for raw", () => { + // Consumers that don't supply H see `unknown` (the union over all + // HarnessRawByKind values is widened to unknown via the default). + // This is the back-compat path — current code reading `event.raw` + // without narrowing is unchanged. + type Default = AgentSession; + type _Harness = ExpectTrue>; + expect(true).toBe(true); + }); +}); + +// Helper alias used in the back-compat test above. Keeping it local so +// the test file declares its own expectations rather than importing +// internals. +type HarnessKindLoose = "claude" | "codex" | "cursor" | "gemini" | "opencode"; + +describe("AgentSession.transcript() shape", () => { + it("is typed to TranscriptEvent[] for a typed session", () => { + type ClaudeTranscript = ReturnType["transcript"]>; + type _Claude = ExpectTrue< + Equals[]> + >; + + type CodexTranscript = ReturnType["transcript"]>; + type _Codex = ExpectTrue< + Equals[]> + >; + + expect(true).toBe(true); + }); + + it("defaults to readonly TranscriptEvent[] when H is not specified", () => { + type DefaultTranscript = ReturnType; + type _Default = ExpectTrue< + Equals[]> + >; + expect(true).toBe(true); + }); +}); 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/claude-runner/package.json b/packages/claude-runner/package.json index 1d706913f..80e51ceb7 100644 --- a/packages/claude-runner/package.json +++ b/packages/claude-runner/package.json @@ -16,7 +16,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.123", + "@anthropic-ai/claude-agent-sdk": "0.2.141", "@anthropic-ai/sdk": "^0.91.0", "@linear/sdk": "^64.0.0", "cyrus-core": "workspace:*", diff --git a/packages/core/package.json b/packages/core/package.json index c213357b7..1b6f025fe 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,7 +18,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.123", + "@anthropic-ai/claude-agent-sdk": "0.2.141", "@linear/sdk": "^64.0.0", "zod": "^4.3.6" }, diff --git a/packages/core/schemas/EdgeConfig.json b/packages/core/schemas/EdgeConfig.json index f906a6f7c..785567b1d 100644 --- a/packages/core/schemas/EdgeConfig.json +++ b/packages/core/schemas/EdgeConfig.json @@ -543,6 +543,10 @@ "type": "string", "enum": ["claude", "gemini", "codex", "cursor"] }, + "defaultProvider": { + "type": "string", + "enum": ["local", "daytona"] + }, "defaultModel": { "type": "string" }, diff --git a/packages/core/schemas/EdgeConfigPayload.json b/packages/core/schemas/EdgeConfigPayload.json index fd7a5b5c5..e408c1b95 100644 --- a/packages/core/schemas/EdgeConfigPayload.json +++ b/packages/core/schemas/EdgeConfigPayload.json @@ -537,6 +537,10 @@ "type": "string", "enum": ["claude", "gemini", "codex", "cursor"] }, + "defaultProvider": { + "type": "string", + "enum": ["local", "daytona"] + }, "defaultModel": { "type": "string" }, diff --git a/packages/core/src/config-schemas.ts b/packages/core/src/config-schemas.ts index e09bbe49e..7e3377d7b 100644 --- a/packages/core/src/config-schemas.ts +++ b/packages/core/src/config-schemas.ts @@ -6,6 +6,18 @@ import { z } from "zod"; export const RunnerTypeSchema = z.enum(["claude", "gemini", "codex", "cursor"]); export type RunnerType = z.infer; +/** + * Supported sandbox provider types for agent execution. + * + * - "local": Run the agent harness directly on the host (no sandbox provider). + * - "daytona": Run the agent harness inside a Daytona-managed cloud sandbox. + * + * Additional providers (e.g. other ComputeSDK backends) may be added here as + * they're wired through the runtime. + */ +export const ProviderTypeSchema = z.enum(["local", "daytona"]); +export type ProviderType = z.infer; + /** * User identifier for access control matching. * Supports multiple formats for flexibility: @@ -366,6 +378,13 @@ export const EdgeConfigSchema = z.object({ */ defaultRunner: RunnerTypeSchema.optional(), + /** + * Default sandbox provider to use when no provider is specified for a session. + * Accepts "local" (run on host) or "daytona" (run inside a Daytona sandbox). + * If omitted, the runtime falls back to "local". + */ + defaultProvider: ProviderTypeSchema.optional(), + /** * @deprecated Use claudeDefaultModel instead. * Legacy field retained for backwards compatibility and migrated on load. diff --git a/packages/core/src/config-types.ts b/packages/core/src/config-types.ts index a805c7c6c..b0d5626d1 100644 --- a/packages/core/src/config-types.ts +++ b/packages/core/src/config-types.ts @@ -17,6 +17,8 @@ export { migrateEdgeConfig, type NetworkPolicy, NetworkPolicySchema, + type ProviderType, + ProviderTypeSchema, type RepositoryConfig, type RepositoryConfigPayload, RepositoryConfigPayloadSchema, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 623d7bd54..cb7d235ae 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -68,6 +68,7 @@ export type { LinearWorkspaceConfig, NetworkPolicy, OAuthCallbackHandler, + ProviderType, RepositoryConfig, RepositoryConfigPayload, RunnerType, @@ -82,6 +83,7 @@ export { LinearWorkspaceConfigSchema, migrateEdgeConfig, NetworkPolicySchema, + ProviderTypeSchema, RepositoryConfigPayloadSchema, RepositoryConfigSchema, RunnerTypeSchema, diff --git a/packages/cursor-sdk-runner/README.md b/packages/cursor-sdk-runner/README.md new file mode 100644 index 000000000..8fd451a1a --- /dev/null +++ b/packages/cursor-sdk-runner/README.md @@ -0,0 +1,90 @@ +# @cyrus-ai/cursor-runner + +A thin CLI wrapper around [`@cursor/sdk`](https://www.npmjs.com/package/@cursor/sdk). +Spawns a Cursor agent, runs one prompt, and emits each `SDKMessage` the SDK +produces as a JSONL line on stdout. + +The point: **type-safe streaming across a process boundary**. Every line on +stdout is an `SDKMessage` from `@cursor/sdk`, so the consumer can import the +same SDK types and narrow `JSON.parse(line)` with zero drift risk. Compare +to invoking `cursor-agent --output-format stream-json` directly — that +CLI's JSONL schema is different from the SDK's union and is not +version-pinned to anything you can `import` against. + +This package exists primarily to be spawned by +[`cyrus-agent-runtime`](https://www.npmjs.com/package/cyrus-agent-runtime)'s +cursor harness, but it's a perfectly usable standalone tool if you want +typed Cursor streaming from any process. + +## Install + +```sh +npm install -g @cyrus-ai/cursor-runner +``` + +## Use + +```sh +export CURSOR_API_KEY=cursor_… # required + +cursor-runner \ + --prompt "Patch the bug in src/utils.ts" \ + --model composer-2 \ + --cwd /path/to/repo +``` + +### Options + +| Flag | Required | Description | +|---|---|---| +| `--prompt ` | yes | The user prompt sent to the agent. | +| `--model ` | no | Model ID. Run `Cursor.models.list()` for valid IDs (`composer-2`, `gpt-5`, etc.). | +| `--cwd ` | no | Working directory the local agent operates against. Defaults to `process.cwd()`. | +| `--system-prompt ` | no | Prepended to `--prompt`. Cursor's local-agent surface doesn't have a separate system-instructions field at this layer. | +| `--agent-id ` | no | Resume an existing agent (cross-turn continuation). | +| `--agent-id-file ` | no | After `Agent.create()`, writes the new agentId to this file so a follow-up turn can pass it back via `--agent-id`. | + +### Exit codes + +- `0` — completed successfully +- `1` — runtime error (stream failure, agent error) +- `2` — misuse (missing `--prompt`, missing `CURSOR_API_KEY`, etc.) + +### Wire format + +Each line on stdout is a JSON-serialized `SDKMessage` from `@cursor/sdk`. +The union includes (variant of `type`): + +- `system` — `subtype: "init"`, agent + model info +- `user` — your prompt as the SDK saw it +- `assistant` — streamed assistant content blocks +- `tool_call` — `status: "running" | "completed" | "error"` lifecycle +- `thinking` — extended-thinking blocks +- `status` — agent state changes (`RUNNING`, `FINISHED`, etc.) +- `request`, `task` — other lifecycle signals + +Consumers should import the SDK type and narrow: + +```ts +import type { SDKMessage } from "@cursor/sdk"; + +for await (const line of readlines(child.stdout)) { + const msg = JSON.parse(line) as SDKMessage; + switch (msg.type) { + case "assistant": /* msg.message.content is BetaContentBlock[] */ break; + case "tool_call": /* msg.status, msg.name, msg.args, msg.result */ break; + // … + } +} +``` + +## Why a separate package? + +`@cursor/sdk` is a TypeScript library; using it from another process requires +either bundling it into every consumer or hosting it behind a stable +CLI. This is that CLI. Keeping it small and dedicated means consumers don't +need a heavyweight runtime to get typed Cursor streaming. + +## License + +MIT diff --git a/packages/cursor-sdk-runner/package.json b/packages/cursor-sdk-runner/package.json new file mode 100644 index 000000000..5ca21378e --- /dev/null +++ b/packages/cursor-sdk-runner/package.json @@ -0,0 +1,46 @@ +{ + "name": "@cyrus-ai/cursor-runner", + "version": "0.2.51", + "description": "Cursor SDK runner — spawned by Cyrus's agent-runtime as a process boundary around @cursor/sdk, emitting SDKMessage events as JSONL on stdout so consumers get typed events with no schema drift.", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "cursor-runner": "dist/index.js" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "vitest", + "test:run": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cursor/sdk": "1.0.13" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.3", + "vitest": "^3.1.4" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "cursor", + "cursor-sdk", + "cli", + "jsonl", + "agent", + "streaming" + ], + "repository": { + "type": "git", + "url": "https://github.com/cyrusagents/cyrus.git", + "directory": "packages/cursor-sdk-runner" + } +} diff --git a/packages/cursor-sdk-runner/src/index.ts b/packages/cursor-sdk-runner/src/index.ts new file mode 100644 index 000000000..14671e551 --- /dev/null +++ b/packages/cursor-sdk-runner/src/index.ts @@ -0,0 +1,137 @@ +#!/usr/bin/env node +/** + * @cyrus-ai/cursor-runner — Cursor SDK runner. + * + * A thin process boundary around `@cursor/sdk`'s `Agent.create()` + + * `run.stream()`. Reads a prompt and options from argv, emits each + * `SDKMessage` the SDK produces as a JSONL line on stdout, exits 0 + * when the run completes (1 on stream error, 2 on misuse). + * + * The point: spawning this CLI from another process is type-safe by + * construction — every line on stdout IS an `SDKMessage` from the + * Cursor SDK, so the spawner can import the same `@cursor/sdk` types + * and narrow `JSON.parse(line)` to `SDKMessage` with no drift risk. + * Compare to invoking `cursor-agent --output-format stream-json`, + * whose schema is different from the SDK's and is not version-pinned + * to anything you can import. + * + * **Usage** (after `npm install -g @cyrus-ai/cursor-runner`): + * + * cursor-runner \ + * --prompt # required + * [--model ] # e.g. composer-2 (`Cursor.models.list()` for valid IDs) + * [--cwd ] # working directory for the local agent + * [--system-prompt ] # prepended to --prompt + * [--agent-id ] # resume an existing agent (cross-turn) + * [--agent-id-file ] # writes the agentId here after Agent.create() + * + * **Auth:** reads `CURSOR_API_KEY` from the environment. Exits 2 if missing. + * + * **Stdout:** one JSON `SDKMessage` per line, nothing else. + * **Stderr:** human-readable error text only when something goes wrong. + */ + +import { writeFile } from "node:fs/promises"; +import { parseArgs } from "node:util"; +import { Agent } from "@cursor/sdk"; + +interface Argv { + prompt: string; + model?: string; + cwd?: string; + systemPrompt?: string; + agentId?: string; + agentIdFile?: string; +} + +function parseArgv(): Argv { + const { values } = parseArgs({ + options: { + prompt: { type: "string" }, + model: { type: "string" }, + cwd: { type: "string" }, + "system-prompt": { type: "string" }, + "agent-id": { type: "string" }, + "agent-id-file": { type: "string" }, + }, + strict: true, + allowPositionals: false, + }); + + if (!values.prompt) { + process.stderr.write("cursor-runner: --prompt is required\n"); + process.exit(2); + } + + return { + prompt: values.prompt, + model: values.model, + cwd: values.cwd, + systemPrompt: values["system-prompt"], + agentId: values["agent-id"], + agentIdFile: values["agent-id-file"], + }; +} + +async function main(): Promise { + const argv = parseArgv(); + + const apiKey = process.env.CURSOR_API_KEY?.trim(); + if (!apiKey) { + process.stderr.write( + "cursor-runner: CURSOR_API_KEY is not set in the environment\n", + ); + process.exit(2); + } + + const agent = argv.agentId + ? await Agent.resume(argv.agentId, { apiKey }) + : await Agent.create({ + apiKey, + model: argv.model ? { id: argv.model } : undefined, + local: { cwd: argv.cwd ?? process.cwd() }, + }); + + // Persist the agentId so the spawner can pass it back as + // `--agent-id` on the next turn (mirrors Claude's --continue / codex's + // thread-id resume). Best-effort: a write failure logs to stderr but + // doesn't kill the run. + if (argv.agentIdFile) { + await writeFile(argv.agentIdFile, agent.agentId, "utf8").catch( + (err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write( + `cursor-runner: failed to persist agent-id-file: ${msg}\n`, + ); + }, + ); + } + + const promptText = argv.systemPrompt + ? `${argv.systemPrompt}\n\n${argv.prompt}` + : argv.prompt; + + try { + const run = await agent.send(promptText); + try { + for await (const message of run.stream()) { + // One SDKMessage per line. JSON.stringify is safe — the SDK + // union is plain serializable data, no live references. + process.stdout.write(`${JSON.stringify(message)}\n`); + } + await run.wait(); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write(`cursor-runner: stream error: ${msg}\n`); + process.exitCode = 1; + } + } finally { + agent.close(); + } +} + +main().catch((err: unknown) => { + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write(`cursor-runner: fatal: ${msg}\n`); + process.exit(1); +}); diff --git a/packages/cursor-sdk-runner/tsconfig.json b/packages/cursor-sdk-runner/tsconfig.json new file mode 100644 index 000000000..73b4fb869 --- /dev/null +++ b/packages/cursor-sdk-runner/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/edge-worker/package.json b/packages/edge-worker/package.json index 09e76a1f0..9cb1ba730 100644 --- a/packages/edge-worker/package.json +++ b/packages/edge-worker/package.json @@ -23,10 +23,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.123", + "@anthropic-ai/claude-agent-sdk": "0.2.141", + "@computesdk/daytona": "^1.7.26", "@linear/sdk": "^64.0.0", "@ngrok/ngrok": "^1.5.1", "chokidar": "^4.0.3", + "computesdk": "^4.0.0", + "cyrus-agent-runtime": "workspace:*", "cyrus-claude-runner": "workspace:*", "cyrus-cloudflare-tunnel-client": "workspace:*", "cyrus-codex-runner": "workspace:*", diff --git a/packages/edge-worker/src/AgentChatSessionHandler.ts b/packages/edge-worker/src/AgentChatSessionHandler.ts new file mode 100644 index 000000000..a861eff81 --- /dev/null +++ b/packages/edge-worker/src/AgentChatSessionHandler.ts @@ -0,0 +1,756 @@ +import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +import type { + AgentSession, + CreateAgentSessionConfigFor, + McpServerRuntimeConfig, + TranscriptEvent, +} from "cyrus-agent-runtime"; +import { createAgentSession } from "cyrus-agent-runtime"; +import type { McpServerConfig } from "cyrus-claude-runner"; +import type { ILogger, ProviderType } from "cyrus-core"; +import { createLogger } from "cyrus-core"; + +/** + * Generic chat platform adapter for the agent-runtime-backed handler. + * + * NOTE: `postReply` here takes a plain string (the final assistant text + * extracted by the harness adapter), not an `IAgentRunner`. This decouples + * platform adapters from the runner machinery — they only need to know how + * to convert agent output back into a platform message. + */ +export type ChatPlatformName = "slack" | "linear" | "github"; + +export interface ChatPlatformAdapter { + readonly platformName: ChatPlatformName; + extractTaskInstructions(event: TEvent): string; + getThreadKey(event: TEvent): string; + getEventId(event: TEvent): string; + buildSystemPrompt(event: TEvent): string; + fetchThreadContext(event: TEvent): Promise; + postReply(event: TEvent, finalText: string): Promise; + acknowledgeReceipt(event: TEvent): Promise; + notifyBusy(event: TEvent, threadKey: string): Promise; +} + +export interface AgentChatSessionHandlerDeps { + onWebhookStart: () => void; + onWebhookEnd: () => void; + onError: (error: Error) => void; + /** + * How long a thread's warm session can sit idle before the handler + * destroys it (sandbox torn down, slot freed). Default 15 minutes. + * Next mention after eviction starts a fresh sandbox + fresh Claude + * session. + */ + idleTtlMs?: number; + /** + * Sandbox provider to run sessions in. Defaults to `"local"`. + * + * - `"local"`: harness runs directly on the host. The host must have + * `claude` on `PATH` (or `claudeCliPath` set). No `DAYTONA_API_KEY` + * required. `destroyWhileInactive` is a no-op for this provider. + * - `"daytona"`: each thread gets a Daytona sandbox. Requires + * `DAYTONA_API_KEY` in the environment; by default Claude CLI is + * installed inside the sandbox via `npm install -g`. Idle sandboxes + * are paused between turns (preserving on-disk state for + * `--continue`) and destroyed after the idle TTL. + * + * Optional env vars: + * - `DAYTONA_SNAPSHOT`: pre-built Daytona snapshot to seed the + * sandbox from. When set, the npm-install setup is skipped (the + * snapshot is expected to have Claude preinstalled) and the CLI + * path defaults to `claude` on `PATH`. + * - `DAYTONA_WORKING_DIR`: home/working directory inside the + * sandbox (default `/home/daytona`). Set this when your snapshot + * uses a different user, e.g. `/home/cyrus`. + * - `DAYTONA_CLAUDE_CLI_PATH`: absolute path to the `claude` + * binary inside the sandbox. Defaults to + * `/.npm-global/bin/claude` without a snapshot, or + * `claude` (PATH-resolved) with a snapshot. + */ + provider?: ProviderType; + /** + * Override the `claude` binary path for local sessions. When unset, + * the harness resolves `claude` via the host's `PATH`. Ignored for + * the daytona provider, which always uses the in-sandbox install + * path. + */ + claudeCliPath?: string; + /** + * Build the MCP servers to attach to a thread's session, called once + * per thread on first creation. Return `undefined` or an empty + * record to run with no MCP servers. The returned config is wrapped + * into a `RuntimePlugin` named `"chat"` and passed to the runtime + * via `plugins[]`, which the materializer fans out into the + * harness's native MCP wiring (Claude reads `.mcp.json`; other + * harnesses get their equivalent). + * + * Per-server transports supported: `type: "http"` and `type: "sse"` + * (forwarded verbatim with `url` + `headers`) and stdio (`command` + + * `args` + `env`, with `type` omitted or set to `"stdio"`). SDK-only + * `type: "sdk"` configs require an in-process server instance and + * are NOT supported across the runtime's subprocess boundary — + * those callers should expose the same server as an HTTP endpoint + * instead (which `McpConfigService` already does for cyrus-tools). + */ + buildMcpServers?: ( + event: TEvent, + ) => Promise | undefined>; +} + +const DEFAULT_IDLE_TTL_MS = 15 * 60 * 1000; // 15 minutes + +// Default Daytona working directory — matches the directory used in the +// streaming spike that validated this end-to-end. Daytona's default base +// image puts the user at /home/daytona; custom snapshots may use a +// different layout (overridable via DAYTONA_WORKING_DIR). +const DEFAULT_DAYTONA_WORKING_DIR = "/home/daytona"; + +// Default claude binary path for the default base image — where `claude` +// lands after `npm install -g` with our custom npm prefix. When a custom +// snapshot is in use, the default switches to `claude` (resolved via the +// sandbox PATH) since the snapshot author owns the install layout. +function defaultClaudeCliPath(workingDir: string): string { + return `${workingDir}/.npm-global/bin/claude`; +} + +// Setup commands that run inside a fresh Daytona sandbox before the +// harness invocation. Used only when no custom snapshot is supplied — +// a custom snapshot is expected to have Claude preinstalled. +// +// Claude CLI is pinned to a specific version so the stream-json shape +// the harness emits matches what `@anthropic-ai/claude-agent-sdk@0.2.141` +// (the SDK we type `HarnessRawByKind["claude"]` against) describes. +// Using `@latest` here would let a future CLI release silently introduce +// fields/variants the SDK pin doesn't know about — exactly the kind of +// drift the SDK-typed events were meant to eliminate. +const PINNED_CLAUDE_CLI_VERSION = "2.1.145"; +function buildDefaultClaudeSetupCommands( + workingDir: string, + cliPath: string, +): string[] { + return [ + `npm config set prefix ${workingDir}/.npm-global`, + `npm install -g @anthropic-ai/claude-code@${PINNED_CLAUDE_CLI_VERSION} >/dev/null 2>&1`, + `${cliPath} --version`, + ]; +} + +/** + * Discriminated union of the three Claude auth modes the handler accepts. + * They are NOT interchangeable — Claude Code treats them as different + * sources with different billing / proxy semantics, so we preserve which + * one came in and forward only that env var to the harness. + * + * - `oauth`: token from `CLAUDE_CODE_OAUTH_TOKEN` (Claude Code Pro/Max + * subscription). + * - `apiKey`: key from `ANTHROPIC_API_KEY` (direct Anthropic API + * access). + * - `authToken`: token from `ANTHROPIC_AUTH_TOKEN` (used by deployments + * that point Claude Code at an Anthropic-compatible proxy / gateway; + * the existing claude-runner forwards this env var alongside the + * other two — see `packages/claude-runner/src/session-env.ts`'s + * `AUTH_ENV_KEYS`). + */ +export type ClaudeCredential = + | { kind: "oauth"; token: string } + | { kind: "apiKey"; token: string } + | { kind: "authToken"; token: string }; + +export function readClaudeCredential(): ClaudeCredential | undefined { + // OAuth takes precedence: subscription users running Claude Code + // against their plan generally want that to be the active path even + // if one of the Anthropic-key env vars happens to be set. Then + // ANTHROPIC_API_KEY, then ANTHROPIC_AUTH_TOKEN — matches the + // AUTH_ENV_KEYS order in claude-runner/src/session-env.ts so the + // chat handler and the legacy runner pick the same one on hosts + // that have multiple set. + const oauth = process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim(); + if (oauth) return { kind: "oauth", token: oauth }; + const apiKey = process.env.ANTHROPIC_API_KEY?.trim(); + if (apiKey) return { kind: "apiKey", token: apiKey }; + const authToken = process.env.ANTHROPIC_AUTH_TOKEN?.trim(); + if (authToken) return { kind: "authToken", token: authToken }; + return undefined; +} + +/** + * Translate the Claude SDK's `McpServerConfig` union into the runtime's + * permissive `McpServerRuntimeConfig` shape. + * + * The runtime forwards entries verbatim to the materializer, which + * writes them straight into `.mcp.json` (Claude) or the equivalent + * harness-native config file. That means the SDK fields (`type`, + * `url`, `headers`, `command`, `args`, `env`, `alwaysLoad`, `tools`, + * ...) are exactly what the harness needs at runtime — we just need a + * type-safe bridge to drop them into a record the runtime accepts. + * + * SDK-instance servers (`type: "sdk"`, which carry a live `McpServer` + * object) are NOT representable across the runtime's subprocess + * boundary and are silently dropped here. Callers that want those + * tools available to a subprocess harness should expose the same + * server over HTTP/SSE instead (which `McpConfigService` already does + * for `cyrus-tools`). + */ +function toRuntimeMcpServers( + servers: Record, +): Record { + const out: Record = {}; + for (const [name, entry] of Object.entries(servers)) { + // `type: "sdk"` entries can't be carried across IPC; skip. + if ((entry as { type?: string }).type === "sdk") continue; + // Spread the SDK entry directly. `McpServerRuntimeConfig` has a + // `[k: string]: unknown` index signature so unknown SDK keys + // (e.g. `alwaysLoad`, `tools`) flow through unchanged. + out[name] = { ...(entry as Record) }; + } + return out; +} + +// Guard against multiple compute.setConfig() calls — ComputeSDK uses a +// module-global config so we only need to set it once per process. +let computeConfigured = false; + +async function configureDaytonaCompute(apiKey: string): Promise { + if (computeConfigured) return; + const { daytona } = await import("@computesdk/daytona"); + const { compute } = await import("computesdk"); + compute.setConfig({ + provider: daytona({ apiKey, timeout: 300_000 }), + }); + computeConfigured = true; +} + +interface ThreadState { + // Chat sessions are Claude-only today (see "Claude harness only" in + // the class docstring). Narrowing here surfaces `SDKMessage` typing + // on `state.session.transcript()` / `result.events` so the assistant- + // text extractor doesn't need manual casts on `event.raw`. + session: AgentSession<"claude">; + lastActivityAt: number; + /** + * In-flight run promise, if any. Used so a second webhook for the + * same thread can detect "busy" without racing on session.run(). + */ + inFlight?: Promise; + /** Last event the handler answered for; used as the busy-notify target. */ + lastEvent: TEvent; +} + +/** + * Chat-session handler built on top of `cyrus-agent-runtime`'s + * `createAgentSession` + multi-turn `session.run()`. Replaces the old + * `ChatSessionHandler` + `IAgentRunner` + `AgentSessionManager` stack. + * + * Provider-aware: each handler instance is configured for either the + * `local` or `daytona` sandbox provider via `deps.provider` (defaulting + * to `local`). The first message in a thread creates a fresh agent + * session against that provider; follow-up messages reuse it via + * Claude's `--continue` flag. After `idleTtlMs` of inactivity the + * handler destroys the session and frees the slot. + * + * **Provider details** + * + * - **local** — harness runs directly on the host. The host must have + * `claude` reachable via `PATH` (or pass `deps.claudeCliPath`). + * `destroyWhileInactive` is a no-op; eviction still calls + * `session.destroy()` to clean up the per-session HOME under + * `~/.cyrus-agent-sessions/`. + * + * - **daytona** — each thread gets a Daytona sandbox seeded with + * `@anthropic-ai/claude-code` via `npm install -g`. Requires + * `DAYTONA_API_KEY`. Idle sandboxes are paused between turns + * (`destroyWhileInactive: true`) so on-disk state survives but + * compute is freed; eviction destroys them outright. + * + * **Common requirements** + * + * - One of `CLAUDE_CODE_OAUTH_TOKEN` (Claude Code subscription OAuth) + * or `ANTHROPIC_API_KEY` (direct Anthropic API access). The handler + * forwards exactly the env var that was set — these are distinct + * auth modes in Claude Code with different billing semantics, so + * we never set both. `CLAUDE_CODE_OAUTH_TOKEN` wins if both are + * present. + * + * **MCP servers** + * + * Wired via `deps.buildMcpServers(event)`, invoked once per thread on + * first session creation. The handler wraps the returned config into a + * single anonymous `RuntimePlugin` (`name: "chat"`) which the runtime + * materializer fans out into the harness's native MCP surface (for + * Claude that's `/.mcp.json`). HTTP, SSE, and stdio + * transports are supported; in-process SDK-instance configs are + * dropped (they can't cross the subprocess boundary — expose them + * over HTTP/SSE instead, which `McpConfigService` already does for + * `cyrus-tools`). + * + * **Known limitations** + * + * - **No mid-flight stream injection.** A second message while the + * thread's session is still answering the first triggers + * `notifyBusy()` rather than injecting into stdin. Future work: + * route through `AgentSession.addMessage()` with + * `interactiveInput: true`. + * - **Claude harness only.** No runner-selection layer for chat yet. + * - **No cross-process recovery.** EdgeWorker restart drops the + * warm-thread map; next mention is a cold start. Daytona's own + * `autoStopInterval` eventually reclaims any orphaned sandboxes. + */ +export class AgentChatSessionHandler { + private readonly adapter: ChatPlatformAdapter; + private readonly deps: AgentChatSessionHandlerDeps; + private readonly logger: ILogger; + private readonly threadSessions = new Map>(); + private readonly provider: ProviderType; + private readonly daytonaApiKey: string | undefined; + private readonly daytonaSnapshot: string | undefined; + private readonly daytonaWorkingDir: string; + private readonly daytonaClaudeCliPath: string; + private readonly daytonaSetupCommands: readonly string[]; + private readonly claudeCliPath: string | undefined; + private readonly idleTtlMs: number; + private idleSweepTimer?: NodeJS.Timeout; + private shuttingDown = false; + + constructor( + adapter: ChatPlatformAdapter, + deps: AgentChatSessionHandlerDeps, + logger?: ILogger, + ) { + this.adapter = adapter; + this.deps = deps; + this.logger = + logger ?? createLogger({ component: "AgentChatSessionHandler" }); + + this.provider = deps.provider ?? "local"; + this.claudeCliPath = deps.claudeCliPath?.trim() || undefined; + + if (this.provider === "daytona") { + const apiKey = process.env.DAYTONA_API_KEY?.trim(); + if (!apiKey) { + throw new Error( + "AgentChatSessionHandler with provider='daytona' requires DAYTONA_API_KEY " + + "in the environment. Set it before starting Cyrus, switch to " + + "provider='local', or disable the chat integration.", + ); + } + this.daytonaApiKey = apiKey; + this.daytonaSnapshot = process.env.DAYTONA_SNAPSHOT?.trim() || undefined; + this.daytonaWorkingDir = + process.env.DAYTONA_WORKING_DIR?.trim() || DEFAULT_DAYTONA_WORKING_DIR; + // With a custom snapshot the install layout belongs to the + // snapshot author, so default to `claude` on PATH; without one + // we drive the install ourselves under the working dir. + const defaultCliPath = this.daytonaSnapshot + ? "claude" + : defaultClaudeCliPath(this.daytonaWorkingDir); + this.daytonaClaudeCliPath = + process.env.DAYTONA_CLAUDE_CLI_PATH?.trim() || defaultCliPath; + // A custom snapshot is expected to have Claude preinstalled, + // so the npm-install setup is skipped. Without a snapshot we + // install Claude into the working dir on every cold start. + this.daytonaSetupCommands = this.daytonaSnapshot + ? [] + : buildDefaultClaudeSetupCommands( + this.daytonaWorkingDir, + this.daytonaClaudeCliPath, + ); + } else { + this.daytonaApiKey = undefined; + this.daytonaSnapshot = undefined; + this.daytonaWorkingDir = DEFAULT_DAYTONA_WORKING_DIR; + this.daytonaClaudeCliPath = defaultClaudeCliPath( + DEFAULT_DAYTONA_WORKING_DIR, + ); + this.daytonaSetupCommands = []; + } + + this.idleTtlMs = deps.idleTtlMs ?? DEFAULT_IDLE_TTL_MS; + + // Sweep every minute; sweep work is cheap (just a map iteration + maybe + // a destroy() per expired entry). + this.idleSweepTimer = setInterval(() => { + void this.sweepIdle(); + }, 60_000); + this.idleSweepTimer.unref?.(); + } + + /** Returns true if any thread on this handler has an in-flight session. */ + isAnyRunnerBusy(): boolean { + for (const state of this.threadSessions.values()) { + if (state.inFlight) return true; + } + return false; + } + + /** Test/inspection: enumerate active threads. */ + listThreads(): Array<{ threadKey: string; sessionId: string }> { + return Array.from(this.threadSessions.entries()).map( + ([threadKey, state]) => ({ + threadKey, + sessionId: state.session.sessionId, + }), + ); + } + + async handleEvent(event: TEvent): Promise { + this.deps.onWebhookStart(); + try { + const eventId = this.adapter.getEventId(event); + const threadKey = this.adapter.getThreadKey(event); + this.logger.info( + `Processing ${this.adapter.platformName} webhook: ${eventId} (thread ${threadKey})`, + ); + + // Fire-and-forget acknowledgement (e.g. emoji reaction). + this.adapter.acknowledgeReceipt(event).catch((err: unknown) => { + this.logger.warn( + `Failed to acknowledge ${this.adapter.platformName} event: ${err instanceof Error ? err.message : err}`, + ); + }); + + // Busy thread → notify and bail. No stdin injection today. + const existing = this.threadSessions.get(threadKey); + if (existing?.inFlight) { + this.logger.info( + `Thread ${threadKey} has an in-flight session; notifying user.`, + ); + await this.adapter.notifyBusy(event, threadKey); + return; + } + + const credential = readClaudeCredential(); + if (!credential) { + this.logger.error( + "Cannot run chat session: no CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN in environment", + ); + await this.adapter.postReply( + event, + "I'm not configured with a Claude credential, so I can't respond. " + + "Ask your admin to set one of CLAUDE_CODE_OAUTH_TOKEN (Claude Code subscription), " + + "ANTHROPIC_API_KEY (direct API access), or ANTHROPIC_AUTH_TOKEN (proxy / gateway).", + ); + return; + } + + if (this.provider === "daytona") { + // Lazy provider init — `daytonaApiKey` is set in the + // constructor when the provider is "daytona", so the + // non-null assertion is safe here. + await configureDaytonaCompute(this.daytonaApiKey as string); + } + + const taskInstructions = this.adapter.extractTaskInstructions(event); + const isFirstTurn = !existing; + + // Thread context is injected only on the first turn — subsequent + // turns are continuations of the same Claude session, which already + // knows the prior conversation. + const userPrompt = isFirstTurn + ? await this.buildFirstTurnPrompt(event, taskInstructions) + : taskInstructions; + + let state: ThreadState; + if (existing) { + state = existing; + } else { + const systemPrompt = this.adapter.buildSystemPrompt(event); + const sessionId = `${this.adapter.platformName}-${eventId}`; + this.logger.info( + `Creating ${this.provider} AgentSession ${sessionId} for thread ${threadKey}`, + ); + const mcpServers = this.deps.buildMcpServers + ? await this.deps.buildMcpServers(event).catch((err: unknown) => { + this.logger.warn( + `buildMcpServers threw for ${threadKey}; running session with no MCP servers: ${ + err instanceof Error ? err.message : err + }`, + ); + return undefined; + }) + : undefined; + const sessionConfig = this.buildSessionConfig({ + sessionId, + threadKey, + systemPrompt, + credential, + mcpServers, + }); + // Explicit `<"claude">` so the returned session is + // `AgentSession<"claude">`, threading SDKMessage typing + // through to state.session, result.events, and the + // extractAssistantFallback walker. Chat is Claude-only + // today; if that changes, swap to a discriminated config + // + per-harness narrowing. + const session = await createAgentSession<"claude">(sessionConfig, { + callbacks: { + onTranscriptEvent: (te) => { + this.logger.debug(`[${sessionId}] transcript event: ${te.kind}`); + }, + }, + }); + state = { + session, + lastActivityAt: Date.now(), + lastEvent: event, + }; + this.threadSessions.set(threadKey, state); + } + + // Mark the run as in-flight so concurrent webhooks see "busy". + const runPromise = state.session.run(userPrompt); + state.inFlight = runPromise; + state.lastEvent = event; + + try { + const result = await runPromise; + state.lastActivityAt = Date.now(); + + if (!result.success) { + this.logger.error( + `Session ${state.session.sessionId} turn did not succeed (exitCode=${result.exitCode})`, + result.error, + ); + if (result.error) this.deps.onError(result.error); + try { + await this.adapter.postReply( + event, + result.error + ? `I hit an error: ${result.error.message}` + : `I couldn't complete the request (exit code ${result.exitCode}).`, + ); + } catch (postErr) { + this.logger.error( + `Failed to post failure notice for session ${state.session.sessionId}`, + postErr instanceof Error ? postErr : new Error(String(postErr)), + ); + } + // A failed run kills the thread — destroy and free the slot + // so the next mention starts fresh. + await this.destroyThread(threadKey); + return; + } + + const finalText = + result.result ?? this.extractAssistantFallback(result.events); + if (!finalText) { + this.logger.warn( + `Session ${state.session.sessionId} completed but produced no result text`, + ); + return; + } + + try { + await this.adapter.postReply(event, finalText); + this.logger.info( + `Posted reply for session ${state.session.sessionId}`, + ); + } catch (postErr) { + this.logger.error( + `Failed to post reply for session ${state.session.sessionId}`, + postErr instanceof Error ? postErr : new Error(String(postErr)), + ); + } + } finally { + state.inFlight = undefined; + } + } catch (error) { + this.logger.error( + `Failed to process ${this.adapter.platformName} webhook`, + error instanceof Error ? error : new Error(String(error)), + ); + this.deps.onError( + error instanceof Error ? error : new Error(String(error)), + ); + } finally { + this.deps.onWebhookEnd(); + } + } + + /** + * Stop the idle sweeper and destroy every warm thread session. + */ + async shutdown(): Promise { + this.shuttingDown = true; + if (this.idleSweepTimer) { + clearInterval(this.idleSweepTimer); + this.idleSweepTimer = undefined; + } + const states = Array.from(this.threadSessions.values()); + this.threadSessions.clear(); + await Promise.all( + states.map(async (state) => { + try { + await state.session.destroy(); + } catch (err) { + this.logger.warn( + `Failed to destroy session ${state.session.sessionId} during shutdown: ${err instanceof Error ? err.message : err}`, + ); + } + }), + ); + } + + private async buildFirstTurnPrompt( + event: TEvent, + taskInstructions: string, + ): Promise { + const threadContext = await this.adapter.fetchThreadContext(event); + return threadContext + ? `${threadContext}\n\n${taskInstructions}` + : taskInstructions; + } + + /** + * Assemble the `CreateAgentSessionConfig` for a fresh thread session. + * Provider-specific config (harness path, packages, sandbox shape) + * lives here so the rest of `handleEvent` stays provider-agnostic. + */ + private buildSessionConfig(args: { + sessionId: string; + threadKey: string; + systemPrompt: string; + credential: ClaudeCredential; + mcpServers?: Record; + }): CreateAgentSessionConfigFor<"claude"> { + const { sessionId, threadKey, systemPrompt, credential, mcpServers } = args; + // Forward exactly the env var the operator actually set. Setting + // more than one would be a bug: Claude Code treats + // `CLAUDE_CODE_OAUTH_TOKEN` (subscription OAuth), `ANTHROPIC_API_KEY` + // (direct API access), and `ANTHROPIC_AUTH_TOKEN` (proxy-style + // auth) as distinct auth modes with different billing / routing, + // so they must not be conflated. + const secrets: Record = + credential.kind === "oauth" + ? { CLAUDE_CODE_OAUTH_TOKEN: credential.token } + : credential.kind === "apiKey" + ? { ANTHROPIC_API_KEY: credential.token } + : { ANTHROPIC_AUTH_TOKEN: credential.token }; + // Wrap the chat-session MCP servers into a single anonymous + // RuntimePlugin so the runtime materializer fans them out into + // the harness's native MCP config surface. Omitted entirely when + // the caller returned undefined / no entries. + const plugins = + mcpServers && Object.keys(mcpServers).length > 0 + ? [{ name: "chat", mcpServers: toRuntimeMcpServers(mcpServers) }] + : undefined; + + if (this.provider === "daytona") { + return { + sessionId, + harness: { + kind: "claude", + command: this.daytonaClaudeCliPath, + }, + systemPrompt, + secrets, + // The Daytona sandbox is the isolation boundary, so + // permission prompts inside it are noise — bypass them + // so the agent can use Bash/Read/Write etc. without + // blocking on prompts that no user will ever answer. + permissions: { mode: "bypass" }, + ...(this.daytonaSetupCommands.length > 0 + ? { packages: { commands: [...this.daytonaSetupCommands] } } + : {}), + ...(plugins ? { plugins } : {}), + sandbox: { + provider: "daytona", + name: `cyrus-${this.adapter.platformName}-${sessionId}`, + workingDirectory: this.daytonaWorkingDir, + timeoutMs: 300_000, + // Pause the sandbox between turns so we stop paying for + // idle compute. Daytona preserves on-disk state during + // stop, so the next turn's `--continue` finds the prior + // `.claude/` intact. + destroyWhileInactive: true, + ...(this.daytonaSnapshot ? { snapshot: this.daytonaSnapshot } : {}), + metadata: { + purpose: `cyrus-${this.adapter.platformName}-chat`, + threadKey, + }, + }, + }; + } + + // Local provider: harness runs on the host. `claude` must be on + // PATH or `claudeCliPath` must be set. The runtime gives each + // session its own HOME under `~/.cyrus-agent-sessions//` so + // per-thread `.claude/` state persists across `--continue` turns + // without colliding with the operator's real `~/.claude/`. + // Narrow `kind: "claude"` as a literal (not HarnessKind) so the + // containing return value satisfies CreateAgentSessionConfigFor<"claude">. + const harnessConfig = { + kind: "claude" as const, + ...(this.claudeCliPath ? { command: this.claudeCliPath } : {}), + }; + return { + sessionId, + harness: harnessConfig, + systemPrompt, + secrets, + ...(plugins ? { plugins } : {}), + sandbox: { + provider: "local", + }, + metadata: { + purpose: `cyrus-${this.adapter.platformName}-chat`, + threadKey, + }, + }; + } + + private async destroyThread(threadKey: string): Promise { + const state = this.threadSessions.get(threadKey); + if (!state) return; + this.threadSessions.delete(threadKey); + try { + await state.session.destroy(); + } catch (err) { + this.logger.warn( + `Failed to destroy thread ${threadKey} session ${state.session.sessionId}: ${err instanceof Error ? err.message : err}`, + ); + } + } + + private async sweepIdle(): Promise { + if (this.shuttingDown) return; + const now = Date.now(); + const expired: string[] = []; + for (const [threadKey, state] of this.threadSessions) { + if (state.inFlight) continue; + if (now - state.lastActivityAt >= this.idleTtlMs) { + expired.push(threadKey); + } + } + for (const threadKey of expired) { + this.logger.info( + `Evicting idle thread ${threadKey} after ${Math.round(this.idleTtlMs / 1000)}s of inactivity`, + ); + await this.destroyThread(threadKey); + } + } + + /** + * Walk the transcript backwards looking for the last assistant text + * block. Used when the harness adapter's `extractResult()` returns + * undefined. + * + * Now type-safe: `state.session` is `AgentSession<"claude">`, so + * `events` is `TranscriptEvent[]` and `event.raw` + * narrows directly via the Claude SDK's discriminated union — no + * hand-written guards or casts. + */ + private extractAssistantFallback( + events: readonly TranscriptEvent[], + ): string | undefined { + for (let i = events.length - 1; i >= 0; i -= 1) { + const e = events[i]; + if (!e) continue; + if (e.raw.type !== "assistant") continue; + for (const block of e.raw.message.content) { + if (block.type === "text" && block.text) return block.text; + } + } + return undefined; + } +} diff --git a/packages/edge-worker/src/ChatSessionHandler.ts b/packages/edge-worker/src/ChatSessionHandler.ts deleted file mode 100644 index c9ce7a2fc..000000000 --- a/packages/edge-worker/src/ChatSessionHandler.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { mkdir } from "node:fs/promises"; -import { join } from "node:path"; -import type { SDKMessage } from "cyrus-claude-runner"; -import type { - AgentRunnerConfig, - AgentSessionInfo, - CyrusAgentSession, - IAgentRunner, - ILogger, -} from "cyrus-core"; -import { createLogger } from "cyrus-core"; -import { AgentSessionManager } from "./AgentSessionManager.js"; -import type { ChatRepositoryProvider } from "./ChatRepositoryProvider.js"; -import type { RunnerConfigBuilder } from "./RunnerConfigBuilder.js"; - -/** - * Defines what each chat platform must provide for the generic session lifecycle. - * - * Implementations are stateless data mappers — they translate platform-specific - * events into the common operations the ChatSessionHandler needs. - */ -/** Platform identifiers supported by the session manager */ -export type ChatPlatformName = "slack" | "linear" | "github"; - -export interface ChatPlatformAdapter { - readonly platformName: ChatPlatformName; - - /** Extract the user's task text from the raw event */ - extractTaskInstructions(event: TEvent): string; - - /** Derive a unique thread key for session tracking (e.g., "C123:1704110400.000100") */ - getThreadKey(event: TEvent): string; - - /** Get the unique event ID */ - getEventId(event: TEvent): string; - - /** Build a platform-specific system prompt */ - buildSystemPrompt(event: TEvent): string; - - /** Fetch thread context as formatted string. Returns "" if not applicable */ - fetchThreadContext(event: TEvent): Promise; - - /** Post the agent's final response back to the platform */ - postReply(event: TEvent, runner: IAgentRunner): Promise; - - /** Acknowledge receipt of the event (e.g., emoji reaction). Fire-and-forget */ - acknowledgeReceipt(event: TEvent): Promise; - - /** Notify the user that a previous request is still processing */ - notifyBusy(event: TEvent, threadKey: string): Promise; -} - -/** - * Callbacks for EdgeWorker integration (same pattern as RepositoryRouterDeps). - */ -export interface ChatSessionHandlerDeps { - cyrusHome: string; - /** Provider for live repository paths, default repo, and workspace ID */ - chatRepositoryProvider: ChatRepositoryProvider; - /** Shared RunnerConfigBuilder for constructing runner configs */ - runnerConfigBuilder: RunnerConfigBuilder; - /** Factory function that creates the appropriate runner based on config.defaultRunner */ - createRunner: (config: AgentRunnerConfig) => IAgentRunner; - onWebhookStart: () => void; - onWebhookEnd: () => void; - onStateChange: () => Promise; - onClaudeError: (error: Error) => void; -} - -/** - * Generic session lifecycle engine for chat platform integrations. - * - * Manages the create/resume/inject/reply session lifecycle independent of any - * specific chat platform. Platform-specific behavior is provided via a - * ChatPlatformAdapter. - */ -export class ChatSessionHandler { - private adapter: ChatPlatformAdapter; - private sessionManager: AgentSessionManager; - private threadSessions: Map = new Map(); - private deps: ChatSessionHandlerDeps; - private logger: ILogger; - // FIFO queue of events awaiting a reply, keyed by sessionId. Each entry is - // enqueued when a new prompt (initial/resume/follow-up-inject) is sent to - // the runner, and consumed when the corresponding `result` message arrives - // on the runner's message stream. This decouples reply posting from - // `startStreaming()` resolution, which never resolves when warm sessions - // hold the streaming prompt open across turns. - private pendingReplyEvents: Map = new Map(); - - constructor( - adapter: ChatPlatformAdapter, - deps: ChatSessionHandlerDeps, - logger?: ILogger, - ) { - this.adapter = adapter; - this.deps = deps; - this.logger = logger ?? createLogger({ component: "ChatSessionHandler" }); - - // Initialize a dedicated AgentSessionManager (not tied to any repository) - this.sessionManager = new AgentSessionManager( - undefined, // No parent session lookup - undefined, // No resume parent session - ); - } - - /** - * Main entry point — handles a single chat platform event. - * - * Replaces the per-platform handleXxxWebhook method in EdgeWorker. - */ - async handleEvent(event: TEvent): Promise { - this.deps.onWebhookStart(); - - try { - this.logger.info( - `Processing ${this.adapter.platformName} webhook: ${this.adapter.getEventId(event)}`, - ); - - // Fire-and-forget acknowledgement (e.g., emoji reaction) - this.adapter.acknowledgeReceipt(event).catch((err: unknown) => { - this.logger.warn( - `Failed to acknowledge ${this.adapter.platformName} event: ${err instanceof Error ? err.message : err}`, - ); - }); - - const taskInstructions = this.adapter.extractTaskInstructions(event); - const threadKey = this.adapter.getThreadKey(event); - - // Check if there's already an active session for this thread - const existingSessionId = this.threadSessions.get(threadKey); - if (existingSessionId) { - const existingSession = - this.sessionManager.getSession(existingSessionId); - const existingRunner = - this.sessionManager.getAgentRunner(existingSessionId); - - if (existingSession && existingRunner?.isRunning()) { - // Session is actively running — inject the follow-up via streaming input - if ( - existingRunner.addStreamMessage && - existingRunner.isStreaming?.() - ) { - this.logger.info( - `Injecting follow-up prompt into running session ${existingSessionId} (thread ${threadKey})`, - ); - this.enqueueReply(existingSessionId, event); - existingRunner.addStreamMessage(taskInstructions); - } else { - // Runner doesn't support streaming input or isn't in streaming mode — notify user - this.logger.info( - `Session ${existingSessionId} is still running, notifying user (thread ${threadKey})`, - ); - await this.adapter.notifyBusy(event, threadKey); - } - return; - } - - if (existingSession && existingRunner) { - // Session exists but is not running — resume with --continue - this.logger.info( - `Resuming completed ${this.adapter.platformName} session ${existingSessionId} (thread ${threadKey})`, - ); - - const resumeSessionId = - existingSession.claudeSessionId || existingSession.geminiSessionId; - - if (resumeSessionId) { - try { - await this.resumeSession( - event, - existingSession, - existingSessionId, - resumeSessionId, - taskInstructions, - ); - } catch (error) { - this.logger.error( - `Failed to resume ${this.adapter.platformName} session ${existingSessionId}`, - error instanceof Error ? error : new Error(String(error)), - ); - } - return; - } - } - - // Session exists but runner was lost — fall through to create a new session - this.logger.info( - `Previous session ${existingSessionId} for thread ${threadKey} has no runner, creating new session`, - ); - } - - // Create an empty workspace directory for this thread - const workspace = await this.createWorkspace(threadKey); - if (!workspace) { - this.logger.error( - `Failed to create workspace for ${this.adapter.platformName} thread ${threadKey}`, - ); - return; - } - - this.logger.info( - `${this.adapter.platformName} workspace created at: ${workspace.path}`, - ); - - // Create a chat session (not tied to any issue or repository) - const eventId = this.adapter.getEventId(event); - const sessionId = `${this.adapter.platformName}-${eventId}`; - this.sessionManager.createChatSession( - sessionId, - workspace, - this.adapter.platformName, - ); - - const session = this.sessionManager.getSession(sessionId); - if (!session) { - this.logger.error( - `Failed to create session for ${this.adapter.platformName} webhook ${eventId}`, - ); - return; - } - - // Track this thread → session mapping for follow-up messages - this.threadSessions.set(threadKey, sessionId); - - // Initialize session metadata - if (!session.metadata) { - session.metadata = {}; - } - - // Build the system prompt - const systemPrompt = this.adapter.buildSystemPrompt(event); - - // Build runner config - const runnerConfig = this.buildRunnerConfig( - session.workspace.path, - sessionId, - systemPrompt, - sessionId, - ); - - const runner = this.deps.createRunner(runnerConfig); - - // Store the runner in the session manager - this.sessionManager.addAgentRunner(sessionId, runner); - - // Save persisted state - await this.deps.onStateChange(); - - // Fetch thread context for threaded mentions - const threadContext = await this.adapter.fetchThreadContext(event); - const userPrompt = threadContext - ? `${threadContext}\n\n${taskInstructions}` - : taskInstructions; - - this.logger.info( - `Starting runner for ${this.adapter.platformName} event ${eventId}`, - ); - - // Start in streaming mode if supported (allows follow-up message injection), - // otherwise fall back to non-streaming start. - // - // Reply posting happens from handleAgentMessage() when a `result` - // message arrives on the runner's stream — we do NOT await turn - // completion here, because with warm sessions the streaming prompt - // stays open and the start() promise doesn't resolve until the - // whole session ends. - this.enqueueReply(sessionId, event); - const startPromise = - runner.supportsStreamingInput && runner.startStreaming - ? runner.startStreaming(userPrompt) - : runner.start(userPrompt); - startPromise - .then((sessionInfo: AgentSessionInfo) => { - this.logger.info( - `${this.adapter.platformName} session started: ${sessionInfo.sessionId}`, - ); - }) - .catch((error: unknown) => { - this.logger.error( - `${this.adapter.platformName} session error for event ${eventId}`, - error instanceof Error ? error : new Error(String(error)), - ); - // Runner died before emitting a final `result`. Drop any - // still-queued reply events for this session so a later - // resumeSession() doesn't pair them with a future turn. - this.clearPendingReplies(sessionId); - }) - .finally(() => { - this.deps.onStateChange().catch((error: unknown) => { - this.logger.error( - `onStateChange failed after ${this.adapter.platformName} session ${sessionId}`, - error instanceof Error ? error : new Error(String(error)), - ); - }); - }); - } catch (error) { - this.logger.error( - `Failed to process ${this.adapter.platformName} webhook`, - error instanceof Error ? error : new Error(String(error)), - ); - } finally { - this.deps.onWebhookEnd(); - } - } - - /** Returns true if any runner managed by this handler is currently busy */ - isAnyRunnerBusy(): boolean { - for (const runner of this.sessionManager.getAllAgentRunners()) { - if (runner.isRunning()) { - return true; - } - } - return false; - } - - /** Returns all runners managed by this handler (for shutdown) */ - getAllRunners(): IAgentRunner[] { - return this.sessionManager.getAllAgentRunners(); - } - - /** - * Test/inspection: list all known thread keys and their session IDs. - * Used by F1 to discover chat sessions for follow-up prompts and replay. - */ - listThreads(): Array<{ threadKey: string; sessionId: string }> { - return Array.from(this.threadSessions.entries()).map( - ([threadKey, sessionId]) => ({ threadKey, sessionId }), - ); - } - - /** - * Test/inspection: resolve a chat thread to its runner. Returns undefined - * when the thread is unknown or the runner has been disposed. - */ - getRunnerForThread(threadKey: string): IAgentRunner | undefined { - const sessionId = this.threadSessions.get(threadKey); - if (!sessionId) return undefined; - return this.sessionManager.getAgentRunner(sessionId); - } - - /** - * Resume an existing session with a new prompt (--continue behavior). - */ - private async resumeSession( - event: TEvent, - existingSession: CyrusAgentSession, - sessionId: string, - resumeSessionId: string, - taskInstructions: string, - ): Promise { - const systemPrompt = this.adapter.buildSystemPrompt(event); - - const runnerConfig = this.buildRunnerConfig( - existingSession.workspace.path, - sessionId, - systemPrompt, - sessionId, - resumeSessionId, - ); - - const runner = this.deps.createRunner(runnerConfig); - this.sessionManager.addAgentRunner(sessionId, runner); - - // Reply posting is driven by `result` messages on the runner's stream - // (see handleAgentMessage). We must not await turn completion here — - // warm sessions hold the streaming prompt open across turns so the - // start() promise only resolves when the whole session ends. - this.enqueueReply(sessionId, event); - const startPromise = - runner.supportsStreamingInput && runner.startStreaming - ? runner.startStreaming(taskInstructions) - : runner.start(taskInstructions); - startPromise - .then((sessionInfo: AgentSessionInfo) => { - this.logger.info( - `${this.adapter.platformName} session resumed: ${sessionInfo.sessionId} (was ${resumeSessionId})`, - ); - }) - .catch((error: unknown) => { - this.logger.error( - `${this.adapter.platformName} resume session error for ${sessionId}`, - error instanceof Error ? error : new Error(String(error)), - ); - this.clearPendingReplies(sessionId); - }); - } - - /** - * Handle agent messages for chat sessions. - * Routes to the dedicated AgentSessionManager, and posts a reply when the - * SDK emits a `result` message (signalling turn completion). - */ - private async handleAgentMessage( - sessionId: string, - message: SDKMessage, - ): Promise { - await this.sessionManager.handleClaudeMessage(sessionId, message); - - if (message.type === "result") { - const event = this.dequeueReply(sessionId); - const runner = this.sessionManager.getAgentRunner(sessionId); - if (event && runner) { - try { - await this.adapter.postReply(event, runner); - } catch (error) { - this.logger.error( - `Failed to post ${this.adapter.platformName} reply for session ${sessionId}`, - error instanceof Error ? error : new Error(String(error)), - ); - } - } else if (!event) { - this.logger.warn( - `Received result for session ${sessionId} with no pending reply event — nothing to post`, - ); - } - } - } - - private enqueueReply(sessionId: string, event: TEvent): void { - const queue = this.pendingReplyEvents.get(sessionId) ?? []; - queue.push(event); - this.pendingReplyEvents.set(sessionId, queue); - } - - private dequeueReply(sessionId: string): TEvent | undefined { - const queue = this.pendingReplyEvents.get(sessionId); - if (!queue || queue.length === 0) return undefined; - const event = queue.shift(); - if (queue.length === 0) { - this.pendingReplyEvents.delete(sessionId); - } - return event; - } - - /** - * Discard all queued reply events for a session. Called when the runner - * rejects before emitting a final `result` — without this, a later - * resumeSession() on the same sessionId would pair the stale event with - * the first `result` of the new runner and shift all subsequent replies - * by one turn. - */ - private clearPendingReplies(sessionId: string): void { - const queue = this.pendingReplyEvents.get(sessionId); - if (!queue || queue.length === 0) return; - this.logger.warn( - `Discarding ${queue.length} pending ${this.adapter.platformName} reply event(s) for session ${sessionId} after runner error`, - ); - this.pendingReplyEvents.delete(sessionId); - } - - /** - * Create an empty workspace directory for a chat thread. - * Unlike repository-associated sessions, chat sessions use plain directories (not git worktrees). - */ - private async createWorkspace( - threadKey: string, - ): Promise<{ path: string; isGitWorktree: boolean } | null> { - try { - const sanitizedKey = threadKey.replace(/[^a-zA-Z0-9.-]/g, "_"); - const workspacePath = join( - this.deps.cyrusHome, - `${this.adapter.platformName}-workspaces`, - sanitizedKey, - ); - - await mkdir(workspacePath, { recursive: true }); - - return { path: workspacePath, isGitWorktree: false }; - } catch (error) { - this.logger.error( - `Failed to create ${this.adapter.platformName} workspace for thread ${threadKey}`, - error instanceof Error ? error : new Error(String(error)), - ); - return null; - } - } - - /** - * Build a runner config for a chat session. - * Delegates to RunnerConfigBuilder for config assembly. - */ - private buildRunnerConfig( - workspacePath: string, - workspaceName: string | undefined, - systemPrompt: string, - sessionId: string, - resumeSessionId?: string, - ): AgentRunnerConfig { - const sessionLogger = this.logger.withContext({ - sessionId, - platform: this.adapter.platformName, - }); - - // Read live values from the provider at session-build time - const provider = this.deps.chatRepositoryProvider; - - return this.deps.runnerConfigBuilder.buildChatConfig({ - workspacePath, - workspaceName, - systemPrompt, - sessionId, - resumeSessionId, - cyrusHome: this.deps.cyrusHome, - platformName: this.adapter.platformName, - linearWorkspaceId: provider.getDefaultLinearWorkspaceId(), - repository: provider.getDefaultRepository(), - repositoryPaths: provider.getRepositoryPaths(), - logger: sessionLogger, - onMessage: (message: SDKMessage) => - this.handleAgentMessage(sessionId, message), - onError: (error: Error) => this.deps.onClaudeError(error), - }); - } -} diff --git a/packages/edge-worker/src/EdgeWorker.ts b/packages/edge-worker/src/EdgeWorker.ts index 9a0a7afe1..70007da63 100644 --- a/packages/edge-worker/src/EdgeWorker.ts +++ b/packages/edge-worker/src/EdgeWorker.ts @@ -137,11 +137,11 @@ import { } from "cyrus-slack-event-transport"; import { Sessions, streamableHttp } from "fastify-mcp"; import { ActivityPoster } from "./ActivityPoster.js"; +import { AgentChatSessionHandler } from "./AgentChatSessionHandler.js"; import { AgentSessionManager } from "./AgentSessionManager.js"; import { AskUserQuestionHandler } from "./AskUserQuestionHandler.js"; import { AttachmentService } from "./AttachmentService.js"; import { LiveChatRepositoryProvider } from "./ChatRepositoryProvider.js"; -import { ChatSessionHandler } from "./ChatSessionHandler.js"; import { ConfigManager, type RepositoryChanges } from "./ConfigManager.js"; import { DefaultSkillsDeployer } from "./DefaultSkillsDeployer.js"; import { EgressProxy } from "./EgressProxy.js"; @@ -209,7 +209,7 @@ export class EdgeWorker extends EventEmitter { private gitHubAppTokenProvider: GitHubAppTokenProvider | null = null; // Self-hosted GitHub App token minting private gitLabEventTransport: GitLabEventTransport | null = null; // GitLab event transport for forwarded GitLab webhooks private slackEventTransport: SlackEventTransport | null = null; - private chatSessionHandler: ChatSessionHandler | null = + private chatSessionHandler: AgentChatSessionHandler | null = null; private gitHubCommentService: GitHubCommentService; // Service for posting comments back to GitHub PRs private gitLabCommentService: GitLabCommentService; // Service for posting comments back to GitLab MRs @@ -788,7 +788,7 @@ export class EdgeWorker extends EventEmitter { // for webhook URL verification to succeed. this.registerGitHubEventTransport(); this.registerGitLabEventTransport(); - this.registerSlackEventTransport(); + await this.registerSlackEventTransport(); // 3. Create and register ConfigUpdater (both platforms) this.configUpdater = new ConfigUpdater( @@ -994,7 +994,7 @@ export class EdgeWorker extends EventEmitter { * Register the Slack event transport for receiving forwarded Slack webhooks from CYHOST. * This creates a /slack-webhook endpoint that handles @mention events from Slack. */ - private registerSlackEventTransport(): void { + private async registerSlackEventTransport(): Promise { // Live provider reads from the repository map on demand — no snapshot needed const chatRepositoryProvider = new LiveChatRepositoryProvider( this.repositories, @@ -1018,28 +1018,18 @@ export class EdgeWorker extends EventEmitter { ); } - this.chatSessionHandler = new ChatSessionHandler( + this.chatSessionHandler = new AgentChatSessionHandler( slackAdapter, { - cyrusHome: this.cyrusHome, - chatRepositoryProvider, - runnerConfigBuilder: this.runnerConfigBuilder, - createRunner: (config) => { - const runnerType = this.runnerSelectionService.getDefaultRunner(); - return this.createRunnerForType(runnerType, { - ...config, - model: this.getDefaultModelForRunner(runnerType), - fallbackModel: this.getDefaultFallbackModelForRunner(runnerType), - }); - }, onWebhookStart: () => { this.activeWebhookCount++; }, onWebhookEnd: () => { this.activeWebhookCount--; }, - onStateChange: () => this.savePersistedState(), - onClaudeError: (error) => this.handleClaudeError(error), + onError: (error) => this.handleClaudeError(error), + provider: this.config.defaultProvider, + buildMcpServers: (event) => this.buildChatMcpServers(event), }, this.logger, ); @@ -2365,6 +2355,43 @@ ${taskSection}`; return "idle"; } + /** + * Build the MCP servers attached to a Slack chat session. + * + * Chat sessions aren't tied to a specific repository, but they still + * benefit from the workspace-level MCP servers — Linear, cyrus-tools, + * cyrus-docs, and (optionally) Slack. We delegate to + * `McpConfigService.buildMcpConfig` with a synthetic repo ID and + * the first configured Linear workspace, since that's the same set + * of servers a repo-bound session would see. Returns undefined when + * no Linear workspace is configured, in which case the chat session + * runs with no MCP servers (the Claude CLI default toolset only). + * + * Re-invoked on each new thread; idempotent for the same context. + */ + private async buildChatMcpServers( + event: SlackWebhookEvent, + ): Promise | undefined> { + const workspaces = this.config.linearWorkspaces ?? {}; + const linearWorkspaceId = Object.keys(workspaces)[0]; + if (!linearWorkspaceId) { + this.logger.debug( + "buildChatMcpServers: no Linear workspace configured; session will run with no MCP servers", + ); + return undefined; + } + // Tag the context with the Slack thread so cyrus-tools can scope + // requests for this chat. The repoId here is synthetic — chat + // sessions don't have a real repo, but McpConfigService uses + // `:` only as a cache key. + const threadKey = `${event.payload.channel}:${event.payload.thread_ts || event.payload.ts}`; + return this.mcpConfigService.buildMcpConfig( + `chat-${event.teamId}`, + linearWorkspaceId, + `chat-${threadKey}`, + ); + } + /** * Test-only: dispatch a synthetic Slack webhook event through the chat * session handler. Used by the F1 test harness to exercise the Slack → @@ -2395,40 +2422,18 @@ ${taskSection}`; /** * Test-only: fetch the last assistant text reply for a chat thread. - * Returns null when the thread or runner is unknown, or no assistant - * message has been produced yet. + * + * NOTE: deliberately disabled on this branch (agent-runtime-backed + * Slack chat). The previous implementation reached into the + * IAgentRunner's message list, which `AgentChatSessionHandler` does + * not expose. F1 tests that depended on this need to be reworked. */ - getChatThreadLastReply(threadKey: string): { + getChatThreadLastReply(_threadKey: string): { text: string; isRunning: boolean; messageCount: number; } | null { - if (!this.chatSessionHandler) return null; - const runner = this.chatSessionHandler.getRunnerForThread(threadKey); - if (!runner) return null; - const messages = runner.getMessages(); - const lastAssistant = [...messages] - .reverse() - .find((m) => m.type === "assistant"); - let text = ""; - if ( - lastAssistant && - lastAssistant.type === "assistant" && - "message" in lastAssistant - ) { - const msg = lastAssistant as { - message: { content: Array<{ type: string; text?: string }> }; - }; - const block = msg.message.content?.find( - (b) => b.type === "text" && b.text, - ); - if (block?.text) text = block.text; - } - return { - text, - isRunning: runner.isRunning(), - messageCount: messages.length, - }; + return null; } /** @@ -2448,15 +2453,10 @@ ${taskSection}`; ); } - // get all agent runners (including chat platform sessions) + // Stop all issue-session agent runners. const agentRunners: IAgentRunner[] = [ ...this.agentSessionManager.getAllAgentRunners(), ]; - if (this.chatSessionHandler) { - agentRunners.push(...this.chatSessionHandler.getAllRunners()); - } - - // Kill all agent processes with null checking for (const runner of agentRunners) { if (runner) { try { @@ -2467,6 +2467,15 @@ ${taskSection}`; } } + // Tear down chat platform sessions (agent-runtime-backed). + if (this.chatSessionHandler) { + try { + await this.chatSessionHandler.shutdown(); + } catch (error) { + this.logger.error("Error shutting down chat session handler:", error); + } + } + // Clear event transport (no explicit cleanup needed, routes are removed when server stops) this.linearEventTransport = null; this.configUpdater = null; @@ -5097,24 +5106,6 @@ ${taskSection}`; return context; } - /** - * Resolve default model for a given runner from config with sensible built-in defaults. - * Supports legacy config keys for backwards compatibility. - */ - private getDefaultModelForRunner(runnerType: RunnerType): string { - return this.runnerSelectionService.getDefaultModelForRunner(runnerType); - } - - /** - * Resolve default fallback model for a given runner from config with sensible built-in defaults. - * Supports legacy Claude fallback key for backwards compatibility. - */ - private getDefaultFallbackModelForRunner(runnerType: RunnerType): string { - return this.runnerSelectionService.getDefaultFallbackModelForRunner( - runnerType, - ); - } - /** * Instantiate the appropriate runner for the given type. */ diff --git a/packages/edge-worker/src/SlackChatAdapter.ts b/packages/edge-worker/src/SlackChatAdapter.ts index a34ca4568..a1fbf4f35 100644 --- a/packages/edge-worker/src/SlackChatAdapter.ts +++ b/packages/edge-worker/src/SlackChatAdapter.ts @@ -1,4 +1,4 @@ -import type { IAgentRunner, ILogger } from "cyrus-core"; +import type { ILogger } from "cyrus-core"; import { createLogger } from "cyrus-core"; import { SlackMessageService, @@ -7,8 +7,8 @@ import { type SlackWebhookEvent, stripMention as stripSlackMention, } from "cyrus-slack-event-transport"; +import type { ChatPlatformAdapter } from "./AgentChatSessionHandler.js"; import type { ChatRepositoryProvider } from "./ChatRepositoryProvider.js"; -import type { ChatPlatformAdapter } from "./ChatSessionHandler.js"; /** * Slack implementation of ChatPlatformAdapter. @@ -195,35 +195,9 @@ Supported mrkdwn syntax: } } - async postReply( - event: SlackWebhookEvent, - runner: IAgentRunner, - ): Promise { + async postReply(event: SlackWebhookEvent, finalText: string): Promise { try { - // Get the last assistant message from the runner as the summary - const messages = runner.getMessages(); - const lastAssistantMessage = [...messages] - .reverse() - .find((m) => m.type === "assistant"); - - let summary = "Task completed."; - if ( - lastAssistantMessage && - lastAssistantMessage.type === "assistant" && - "message" in lastAssistantMessage - ) { - const msg = lastAssistantMessage as { - message: { - content: Array<{ type: string; text?: string }>; - }; - }; - const textBlock = msg.message.content?.find( - (block) => block.type === "text" && block.text, - ); - if (textBlock?.text) { - summary = textBlock.text; - } - } + const summary = finalText.trim() || "Task completed."; const token = this.getSlackBotToken(event); if (!token) { diff --git a/packages/edge-worker/src/index.ts b/packages/edge-worker/src/index.ts index 107e286b9..421387dcd 100644 --- a/packages/edge-worker/src/index.ts +++ b/packages/edge-worker/src/index.ts @@ -10,6 +10,12 @@ export type { UserIdentifier, Workspace, } from "cyrus-core"; +export type { + AgentChatSessionHandlerDeps, + ChatPlatformAdapter, + ChatPlatformName, +} from "./AgentChatSessionHandler.js"; +export { AgentChatSessionHandler } from "./AgentChatSessionHandler.js"; export { AgentSessionManager } from "./AgentSessionManager.js"; export type { AskUserQuestionHandlerConfig, @@ -18,12 +24,6 @@ export type { export { AskUserQuestionHandler } from "./AskUserQuestionHandler.js"; export type { ChatRepositoryProvider } from "./ChatRepositoryProvider.js"; export { LiveChatRepositoryProvider } from "./ChatRepositoryProvider.js"; -export type { - ChatPlatformAdapter, - ChatPlatformName, - ChatSessionHandlerDeps, -} from "./ChatSessionHandler.js"; -export { ChatSessionHandler } from "./ChatSessionHandler.js"; export { DefaultSkillsDeployer } from "./DefaultSkillsDeployer.js"; export { EdgeWorker } from "./EdgeWorker.js"; export { EgressProxy } from "./EgressProxy.js"; diff --git a/packages/edge-worker/test-scripts/slack-handler-proof.mjs b/packages/edge-worker/test-scripts/slack-handler-proof.mjs new file mode 100644 index 000000000..7c2223ac5 --- /dev/null +++ b/packages/edge-worker/test-scripts/slack-handler-proof.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +// End-to-end proof for AgentChatSessionHandler on real Daytona+Claude. +// +// Simulates two Slack mentions on the same thread: +// Mention 1: "Remember code word BANANA-7. Reply: noted" +// Mention 2: "What was the code word?" +// +// Validates: +// - First mention spawns Daytona sandbox + installs Claude + answers +// - Sandbox is paused (Daytona stop) after the first answer is posted +// - Second mention resumes the sandbox and Claude --continue knows the +// code word from the first turn +// - Posted replies include "BANANA-7" on the second turn +// +// Usage: +// set -a; source ~/.cyrus/secrets/daytona.env; source ~/.cyrus/secrets/claude.env; set +a +// pnpm --filter cyrus-agent-runtime build +// pnpm --filter cyrus-edge-worker build +// node packages/edge-worker/test-scripts/slack-handler-proof.mjs + +import { AgentChatSessionHandler } from "../dist/index.js"; + +const CODE_WORD = "BANANA-7"; + +function fmt(ms) { + return `${ms.toString().padStart(5, " ")}ms`; +} + +// Minimal stub adapter — only the bits handler.handleEvent reads. +const postedReplies = []; +const adapter = { + platformName: "slack", + extractTaskInstructions(event) { + return event.taskInstructions; + }, + getThreadKey(event) { + return event.threadKey; + }, + getEventId(event) { + return event.eventId; + }, + buildSystemPrompt(_event) { + return "You are a concise assistant. Reply in as few words as possible."; + }, + async fetchThreadContext(_event) { + return ""; + }, + async postReply(event, finalText) { + console.log( + ` [postReply for event=${event.eventId}] text=${JSON.stringify(finalText)}`, + ); + postedReplies.push({ eventId: event.eventId, text: finalText }); + }, + async acknowledgeReceipt(_event) { + /* no-op */ + }, + async notifyBusy(_event, threadKey) { + console.log(` [notifyBusy] thread=${threadKey}`); + }, +}; + +if (!process.env.DAYTONA_API_KEY?.trim()) { + console.error("DAYTONA_API_KEY missing"); + process.exit(1); +} +if ( + !( + process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim() || + process.env.ANTHROPIC_AUTH_TOKEN?.trim() + ) +) { + console.error("CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_AUTH_TOKEN missing"); + process.exit(1); +} + +const handler = new AgentChatSessionHandler(adapter, { + onWebhookStart() {}, + onWebhookEnd() {}, + onError(err) { + console.error(" [onError]", err.message); + }, + // Don't auto-evict during a 60s test run. + idleTtlMs: 30 * 60 * 1000, +}); + +const threadKey = `C-TEST:${Date.now()}`; + +const event1 = { + eventId: `evt-1-${Date.now()}`, + threadKey, + taskInstructions: `Remember this code word for me: ${CODE_WORD}. Reply with exactly one word: noted`, +}; +const event2 = { + eventId: `evt-2-${Date.now() + 1}`, + threadKey, + taskInstructions: `What was the code word? Reply with just the code word.`, +}; + +console.log( + "\n=== AgentChatSessionHandler end-to-end (Daytona + Claude, destroyWhileInactive) ===\n", +); + +try { + console.log("Mention 1: send code word (cold start)…"); + const t0 = Date.now(); + await handler.handleEvent(event1); + console.log(` Mention 1 handled in ${fmt(Date.now() - t0)}`); + + console.log("\n Sandbox should be paused now between mentions."); + + console.log("\nMention 2: ask for code word (sandbox resume + --continue)…"); + const t1 = Date.now(); + await handler.handleEvent(event2); + console.log(` Mention 2 handled in ${fmt(Date.now() - t1)}`); + + console.log("\n--- Replies posted to Slack ---"); + for (const r of postedReplies) { + console.log(` ${r.eventId}: ${JSON.stringify(r.text)}`); + } + + const reply2 = postedReplies.find((r) => r.eventId === event2.eventId); + if (!reply2) { + console.error("\n ✗ No reply was posted for mention 2."); + process.exit(1); + } + if (reply2.text.toUpperCase().includes(CODE_WORD)) { + console.log( + `\n ✓ End-to-end resume confirmed: mention-2 reply contains "${CODE_WORD}".`, + ); + } else { + console.error( + `\n ✗ Resume FAILED: mention-2 reply did not contain "${CODE_WORD}".`, + ); + process.exit(1); + } +} finally { + console.log("\nShutting down handler (destroys all warm sessions)…"); + await handler.shutdown(); + console.log(" done."); +} diff --git a/packages/edge-worker/test/AgentChatSessionHandler.provider.test.ts b/packages/edge-worker/test/AgentChatSessionHandler.provider.test.ts new file mode 100644 index 000000000..d2a284380 --- /dev/null +++ b/packages/edge-worker/test/AgentChatSessionHandler.provider.test.ts @@ -0,0 +1,393 @@ +import type { ILogger } from "cyrus-core"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + AgentChatSessionHandler, + type ChatPlatformAdapter, + readClaudeCredential, +} from "../src/AgentChatSessionHandler.js"; + +const silentLogger: ILogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} as unknown as ILogger; + +// Minimal stand-in for ChatPlatformAdapter. None of these methods are +// invoked by the constructor — they only matter once handleEvent runs — +// so we throw to make accidental invocations loud in test output. +function makeAdapter(): ChatPlatformAdapter { + const fail = (name: string) => () => { + throw new Error(`unexpected call to ${name} in constructor test`); + }; + return { + platformName: "slack", + extractTaskInstructions: fail("extractTaskInstructions") as never, + getThreadKey: fail("getThreadKey") as never, + getEventId: fail("getEventId") as never, + buildSystemPrompt: fail("buildSystemPrompt") as never, + fetchThreadContext: fail("fetchThreadContext") as never, + postReply: fail("postReply") as never, + acknowledgeReceipt: fail("acknowledgeReceipt") as never, + notifyBusy: fail("notifyBusy") as never, + }; +} + +function makeDeps( + overrides: Partial< + Parameters<(typeof AgentChatSessionHandler.prototype)["constructor"]>[1] + > = {}, +) { + return { + onWebhookStart: () => {}, + onWebhookEnd: () => {}, + onError: () => {}, + ...overrides, + }; +} + +describe("AgentChatSessionHandler provider selection", () => { + const SNAPSHOT_ENV_VARS = [ + "DAYTONA_API_KEY", + "DAYTONA_SNAPSHOT", + "DAYTONA_WORKING_DIR", + "DAYTONA_CLAUDE_CLI_PATH", + ] as const; + const originalEnv = new Map(); + + beforeEach(() => { + for (const name of SNAPSHOT_ENV_VARS) { + originalEnv.set(name, process.env[name]); + delete process.env[name]; + } + }); + + afterEach(async () => { + for (const name of SNAPSHOT_ENV_VARS) { + const prev = originalEnv.get(name); + if (prev === undefined) { + delete process.env[name]; + } else { + process.env[name] = prev; + } + } + originalEnv.clear(); + }); + + it("defaults to local provider when none specified and does not require DAYTONA_API_KEY", () => { + expect( + () => + new AgentChatSessionHandler(makeAdapter(), makeDeps(), silentLogger), + ).not.toThrow(); + }); + + it("accepts provider='local' without DAYTONA_API_KEY", () => { + expect( + () => + new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "local" }), + silentLogger, + ), + ).not.toThrow(); + }); + + it("throws when provider='daytona' is requested without DAYTONA_API_KEY", () => { + expect( + () => + new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ), + ).toThrow(/DAYTONA_API_KEY/); + }); + + it("accepts provider='daytona' when DAYTONA_API_KEY is set", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + expect( + () => + new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ), + ).not.toThrow(); + }); + + it("threads DAYTONA_SNAPSHOT into the Daytona sandbox config when set", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + process.env.DAYTONA_SNAPSHOT = "cyrus-base-v3"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "snap-set"); + expect(config.sandbox?.snapshot).toBe("cyrus-base-v3"); + }); + + it("omits sandbox.snapshot when DAYTONA_SNAPSHOT is unset", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "snap-unset"); + expect(config.sandbox?.snapshot).toBeUndefined(); + }); + + it("treats whitespace-only DAYTONA_SNAPSHOT as unset", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + process.env.DAYTONA_SNAPSHOT = " "; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "snap-empty"); + expect(config.sandbox?.snapshot).toBeUndefined(); + }); + + it("uses default working dir, CLI path, and npm setup commands when DAYTONA_SNAPSHOT is unset", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "defaults"); + expect(config.sandbox?.workingDirectory).toBe("/home/daytona"); + expect(config.harness?.command).toBe( + "/home/daytona/.npm-global/bin/claude", + ); + expect(config.packages?.commands).toEqual([ + "npm config set prefix /home/daytona/.npm-global", + // Pinned to match `@anthropic-ai/claude-agent-sdk@0.2.141` (the + // SDK version `HarnessRawByKind["claude"]` is typed against). + // If we bump the pin, we bump it here too. + "npm install -g @anthropic-ai/claude-code@2.1.145 >/dev/null 2>&1", + "/home/daytona/.npm-global/bin/claude --version", + ]); + }); + + it("skips npm setup and defaults claude to PATH when DAYTONA_SNAPSHOT is set", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + process.env.DAYTONA_SNAPSHOT = "cyrus-base-v3"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "with-snapshot"); + expect(config.harness?.command).toBe("claude"); + expect(config.packages).toBeUndefined(); + }); + + it("honors DAYTONA_WORKING_DIR override", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + process.env.DAYTONA_SNAPSHOT = "cyrus-base-v3"; + process.env.DAYTONA_WORKING_DIR = "/home/cyrus"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "wd-override"); + expect(config.sandbox?.workingDirectory).toBe("/home/cyrus"); + }); + + it("honors DAYTONA_CLAUDE_CLI_PATH override", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + process.env.DAYTONA_SNAPSHOT = "cyrus-base-v3"; + process.env.DAYTONA_CLAUDE_CLI_PATH = "/usr/local/bin/claude"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "cli-override"); + expect(config.harness?.command).toBe("/usr/local/bin/claude"); + }); + + it("bypasses Claude permission prompts for Daytona sessions", () => { + process.env.DAYTONA_API_KEY = "fake-key-for-test"; + const handler = new AgentChatSessionHandler( + makeAdapter(), + makeDeps({ provider: "daytona" }), + silentLogger, + ); + const config = buildDaytonaConfig(handler, "perm-bypass"); + expect(config.permissions?.mode).toBe("bypass"); + }); +}); + +interface DaytonaSessionConfigShape { + harness?: { command?: string }; + packages?: { commands?: string[] }; + permissions?: { mode?: string }; + sandbox?: { workingDirectory?: string; snapshot?: string }; + secrets?: Record; +} + +function buildDaytonaConfig( + handler: AgentChatSessionHandler, + sessionId: string, + credential: { + kind: "oauth" | "apiKey" | "authToken"; + token: string; + } = { kind: "apiKey", token: "tok" }, +): DaytonaSessionConfigShape { + return ( + handler as unknown as { + buildSessionConfig: (args: { + sessionId: string; + threadKey: string; + systemPrompt: string; + credential: typeof credential; + }) => DaytonaSessionConfigShape; + } + ).buildSessionConfig({ + sessionId, + threadKey: `thread-${sessionId}`, + systemPrompt: "sys", + credential, + }); +} + +describe("AgentChatSessionHandler credential detection", () => { + const CRED_ENV_VARS = [ + "CLAUDE_CODE_OAUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_AUTH_TOKEN", + ] as const; + const originalEnv = new Map(); + + beforeEach(() => { + for (const name of CRED_ENV_VARS) { + originalEnv.set(name, process.env[name]); + delete process.env[name]; + } + }); + + afterEach(() => { + for (const name of CRED_ENV_VARS) { + const prev = originalEnv.get(name); + if (prev === undefined) { + delete process.env[name]; + } else { + process.env[name] = prev; + } + } + originalEnv.clear(); + }); + + it("returns undefined when no credential env var is set", () => { + expect(readClaudeCredential()).toBeUndefined(); + }); + + it("detects ANTHROPIC_AUTH_TOKEN when it is the only one set", () => { + // Regression guard — earlier revision of this handler dropped + // ANTHROPIC_AUTH_TOKEN entirely, which broke deployments that + // auth Claude via a proxy/gateway. The legacy claude-runner + // (packages/claude-runner/src/session-env.ts AUTH_ENV_KEYS) still + // forwards this env var, so the chat handler must accept it too. + process.env.ANTHROPIC_AUTH_TOKEN = "auth-token-value"; + expect(readClaudeCredential()).toEqual({ + kind: "authToken", + token: "auth-token-value", + }); + }); + + it("CLAUDE_CODE_OAUTH_TOKEN > ANTHROPIC_API_KEY > ANTHROPIC_AUTH_TOKEN", () => { + // Precedence matches claude-runner's AUTH_ENV_KEYS scan order + // so the chat handler and the legacy runner pick the same one + // on hosts that have multiple set. + process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth"; + process.env.ANTHROPIC_API_KEY = "api"; + process.env.ANTHROPIC_AUTH_TOKEN = "auth"; + expect(readClaudeCredential()).toEqual({ kind: "oauth", token: "oauth" }); + + delete process.env.CLAUDE_CODE_OAUTH_TOKEN; + expect(readClaudeCredential()).toEqual({ kind: "apiKey", token: "api" }); + + delete process.env.ANTHROPIC_API_KEY; + expect(readClaudeCredential()).toEqual({ + kind: "authToken", + token: "auth", + }); + }); + + it("trims whitespace-only env vars to empty / undefined", () => { + process.env.ANTHROPIC_AUTH_TOKEN = " "; + expect(readClaudeCredential()).toBeUndefined(); + }); +}); + +describe("AgentChatSessionHandler credential forwarding", () => { + const SNAPSHOT_ENV_VARS = ["DAYTONA_API_KEY"] as const; + const originalEnv = new Map(); + + beforeEach(() => { + for (const name of SNAPSHOT_ENV_VARS) { + originalEnv.set(name, process.env[name]); + delete process.env[name]; + } + process.env.DAYTONA_API_KEY = "dt-test"; + }); + + afterEach(() => { + for (const name of SNAPSHOT_ENV_VARS) { + const prev = originalEnv.get(name); + if (prev === undefined) { + delete process.env[name]; + } else { + process.env[name] = prev; + } + } + originalEnv.clear(); + }); + + function makeHandler(): AgentChatSessionHandler { + return new AgentChatSessionHandler( + { adapter: makeAdapter(), provider: "daytona" }, + makeDeps(), + silentLogger, + ); + } + + it("forwards CLAUDE_CODE_OAUTH_TOKEN for kind='oauth'", () => { + const config = buildDaytonaConfig(makeHandler(), "cred-oauth", { + kind: "oauth", + token: "oauth-token", + }); + expect(config.secrets?.CLAUDE_CODE_OAUTH_TOKEN).toBe("oauth-token"); + expect(config.secrets?.ANTHROPIC_API_KEY).toBeUndefined(); + expect(config.secrets?.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + }); + + it("forwards ANTHROPIC_API_KEY for kind='apiKey'", () => { + const config = buildDaytonaConfig(makeHandler(), "cred-api", { + kind: "apiKey", + token: "api-key", + }); + expect(config.secrets?.ANTHROPIC_API_KEY).toBe("api-key"); + expect(config.secrets?.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + expect(config.secrets?.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + }); + + it("forwards ANTHROPIC_AUTH_TOKEN for kind='authToken'", () => { + // Mirrors the kind='apiKey' assertion. With Claude Code's distinct + // auth-mode handling, sending two of these env vars at once would + // conflate billing / routing, so the handler picks one and only + // sets that one — verify the right one ships through. + const config = buildDaytonaConfig(makeHandler(), "cred-auth", { + kind: "authToken", + token: "auth-token", + }); + expect(config.secrets?.ANTHROPIC_AUTH_TOKEN).toBe("auth-token"); + expect(config.secrets?.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + expect(config.secrets?.ANTHROPIC_API_KEY).toBeUndefined(); + }); +}); diff --git a/packages/edge-worker/test/chat-sessions.test.ts b/packages/edge-worker/test/chat-sessions.test.ts deleted file mode 100644 index 17e595817..000000000 --- a/packages/edge-worker/test/chat-sessions.test.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { join } from "node:path"; -import { getReadOnlyTools } from "cyrus-claude-runner"; -import type { RepositoryConfig } from "cyrus-core"; -import { describe, expect, it, vi } from "vitest"; -import type { ChatRepositoryProvider } from "../src/ChatRepositoryProvider.js"; -import { LiveChatRepositoryProvider } from "../src/ChatRepositoryProvider.js"; -import type { ChatPlatformAdapter } from "../src/ChatSessionHandler.js"; -import { ChatSessionHandler } from "../src/ChatSessionHandler.js"; -import type { RunnerConfigBuilder } from "../src/RunnerConfigBuilder.js"; -import { SlackChatAdapter } from "../src/SlackChatAdapter.js"; -import { TEST_CYRUS_CHAT } from "./test-dirs.js"; - -function createMockRunnerConfigBuilder(): RunnerConfigBuilder { - return { - buildChatConfig: (input: any) => { - const repositoryPaths = Array.from( - new Set((input.repositoryPaths ?? []).filter(Boolean)), - ); - return { - workingDirectory: input.workspacePath, - allowedTools: [ - ...new Set([...getReadOnlyTools(), "Bash(git -C * pull)"]), - ], - disallowedTools: [], - allowedDirectories: [input.workspacePath, ...repositoryPaths], - workspaceName: input.workspaceName, - cyrusHome: input.cyrusHome, - appendSystemPrompt: input.systemPrompt, - ...(input.resumeSessionId - ? { resumeSessionId: input.resumeSessionId } - : {}), - logger: input.logger, - maxTurns: 200, - onMessage: input.onMessage, - onError: input.onError, - }; - }, - buildIssueConfig: vi.fn(), - } as unknown as RunnerConfigBuilder; -} - -/** Minimal ChatRepositoryProvider backed by a plain array (for tests) */ -function createStaticProvider( - paths: string[], - defaultRepo?: RepositoryConfig, - linearWorkspaceId?: string, -): ChatRepositoryProvider { - return { - getRepositoryPaths: () => paths, - getDefaultRepository: () => defaultRepo, - getDefaultLinearWorkspaceId: () => linearWorkspaceId, - }; -} - -interface TestEvent { - eventId: string; - threadKey: string; -} - -class TestChatAdapter implements ChatPlatformAdapter { - public platformName = "slack" as const; - - constructor(private readonly threadKey: string) {} - - extractTaskInstructions(_event: TestEvent): string { - return "Inspect repository configuration"; - } - - getThreadKey(_event: TestEvent): string { - return this.threadKey; - } - - getEventId(_event: TestEvent): string { - return "test-event"; - } - - buildSystemPrompt(_event: TestEvent): string { - return "You are a test chat assistant."; - } - - async fetchThreadContext(_event: TestEvent): Promise { - return ""; - } - - async postReply(_event: TestEvent, _runner: unknown): Promise { - return; - } - - async acknowledgeReceipt(_event: TestEvent): Promise { - return; - } - - async notifyBusy(_event: TestEvent): Promise { - return; - } -} - -describe("ChatSessionHandler chat session permissions", () => { - it("grants read-only tools, explicit git pull, and repository read access", async () => { - const event: TestEvent = { - eventId: "test-event", - threadKey: "test-thread", - }; - const cyrusHome = TEST_CYRUS_CHAT; - const chatRepositoryPaths = ["/repo/chat-one", "/repo/chat-two"]; - let capturedConfig: any; - - const adapter = new TestChatAdapter("thread-key"); - const createRunner = vi.fn((config: any) => { - capturedConfig = config; - return { - supportsStreamingInput: false, - start: vi.fn().mockResolvedValue({ sessionId: "session-1" }), - stop: vi.fn(), - isRunning: vi.fn().mockReturnValue(false), - isStreaming: vi.fn().mockReturnValue(false), - addStreamMessage: vi.fn(), - getMessages: vi.fn().mockReturnValue([]), - } as any; - }); - const onWebhookStart = vi.fn(); - const onWebhookEnd = vi.fn(); - const onStateChange = vi.fn().mockResolvedValue(undefined); - const onClaudeError = vi.fn(); - - const handler = new ChatSessionHandler(adapter, { - cyrusHome, - chatRepositoryProvider: createStaticProvider(chatRepositoryPaths), - runnerConfigBuilder: createMockRunnerConfigBuilder(), - createRunner: createRunner, - onWebhookStart, - onWebhookEnd, - onStateChange, - onClaudeError, - }); - - await handler.handleEvent(event as any); - - expect(capturedConfig).toBeDefined(); - expect(capturedConfig.allowedTools).toContain("Read(**)"); - expect(capturedConfig.allowedTools).toContain("Glob"); - expect(capturedConfig.allowedTools).toContain("Bash(git -C * pull)"); - expect(capturedConfig.allowedTools).not.toContain("Edit(**)"); - - const expectedWorkspace = join(cyrusHome, "slack-workspaces", "thread-key"); - expect(capturedConfig.allowedDirectories).toContain(expectedWorkspace); - for (const path of chatRepositoryPaths) { - expect(capturedConfig.allowedDirectories).toContain(path); - } - }); -}); - -describe("SlackChatAdapter system prompt", () => { - it("includes configured repository context and git pull instructions", () => { - const repositoryPaths = ["/repo/chat-one", "/repo/chat-two"]; - const adapter = new SlackChatAdapter(createStaticProvider(repositoryPaths)); - const systemPrompt = adapter.buildSystemPrompt({ - payload: { - user: "U1", - channel: "C1", - text: "<@cyrus> inspect code", - ts: "1700000000.000100", - event_ts: "1700000000.000100", - type: "app_mention", - }, - } as any); - - expect(systemPrompt).toContain("## Repository Access"); - expect(systemPrompt).toContain("- /repo/chat-one"); - expect(systemPrompt).toContain("- /repo/chat-two"); - expect(systemPrompt).toContain("Bash(git -C * pull)"); - }); - - it("includes orchestrator routing context and self-assignment workflow", () => { - const repositoryPaths = ["/repo/chat-one", "/repo/chat-two"]; - const repositoryRoutingContext = - "\n Use repo routing tags.\n"; - const adapter = new SlackChatAdapter( - createStaticProvider(repositoryPaths), - undefined, - { repositoryRoutingContext }, - ); - const systemPrompt = adapter.buildSystemPrompt({ - payload: { - user: "U1", - channel: "C1", - text: "<@cyrus> assign this work", - ts: "1700000000.000100", - event_ts: "1700000000.000100", - type: "app_mention", - }, - } as any); - - expect(systemPrompt).toContain(repositoryRoutingContext); - expect(systemPrompt).toContain("mcp__linear__get_user"); - expect(systemPrompt).toContain('query: "me"'); - expect(systemPrompt).toContain("linear_get_agent_sessions"); - }); -}); - -describe("ChatRepositoryProvider runtime updates", () => { - const slackEvent = { - payload: { - user: "U1", - channel: "C1", - text: "<@cyrus> test", - ts: "1700000000.000100", - event_ts: "1700000000.000100", - type: "app_mention", - }, - } as any; - - it("SlackChatAdapter.buildSystemPrompt reflects repos added at runtime", () => { - const paths = ["/repo/A"]; - const provider: ChatRepositoryProvider = { - getRepositoryPaths: () => paths, - getDefaultRepository: () => undefined, - getDefaultLinearWorkspaceId: () => undefined, - }; - const adapter = new SlackChatAdapter(provider); - - // Initial state: only repo A - let prompt = adapter.buildSystemPrompt(slackEvent); - expect(prompt).toContain("- /repo/A"); - expect(prompt).not.toContain("- /repo/B"); - - // Simulate runtime config change: add repo B - paths.push("/repo/B"); - - prompt = adapter.buildSystemPrompt(slackEvent); - expect(prompt).toContain("- /repo/A"); - expect(prompt).toContain("- /repo/B"); - }); - - it("SlackChatAdapter.buildSystemPrompt reflects repos removed at runtime", () => { - const paths = ["/repo/A", "/repo/B"]; - const provider: ChatRepositoryProvider = { - getRepositoryPaths: () => paths, - getDefaultRepository: () => undefined, - getDefaultLinearWorkspaceId: () => undefined, - }; - const adapter = new SlackChatAdapter(provider); - - // Initial state: both repos - let prompt = adapter.buildSystemPrompt(slackEvent); - expect(prompt).toContain("- /repo/A"); - expect(prompt).toContain("- /repo/B"); - - // Simulate runtime config change: remove repo A - paths.splice(0, 1); - - prompt = adapter.buildSystemPrompt(slackEvent); - expect(prompt).not.toContain("- /repo/A"); - expect(prompt).toContain("- /repo/B"); - }); - - it("ChatSessionHandler reads live repository paths from provider at session build time", async () => { - const cyrusHome = TEST_CYRUS_CHAT; - const paths = ["/repo/A"]; - const provider: ChatRepositoryProvider = { - getRepositoryPaths: () => [...paths], - getDefaultRepository: () => undefined, - getDefaultLinearWorkspaceId: () => undefined, - }; - - let capturedConfig: any; - const createRunner = vi.fn((config: any) => { - capturedConfig = config; - return { - supportsStreamingInput: false, - start: vi.fn().mockResolvedValue({ sessionId: "session-1" }), - stop: vi.fn(), - isRunning: vi.fn().mockReturnValue(false), - isStreaming: vi.fn().mockReturnValue(false), - addStreamMessage: vi.fn(), - getMessages: vi.fn().mockReturnValue([]), - } as any; - }); - - const adapter = new TestChatAdapter("runtime-thread"); - const handler = new ChatSessionHandler(adapter, { - cyrusHome, - chatRepositoryProvider: provider, - runnerConfigBuilder: createMockRunnerConfigBuilder(), - createRunner, - onWebhookStart: vi.fn(), - onWebhookEnd: vi.fn(), - onStateChange: vi.fn().mockResolvedValue(undefined), - onClaudeError: vi.fn(), - }); - - // Add repo B at "runtime" before creating a session - paths.push("/repo/B"); - - await handler.handleEvent({ - eventId: "runtime-event", - threadKey: "runtime-thread", - } as any); - - expect(capturedConfig.allowedDirectories).toContain("/repo/A"); - expect(capturedConfig.allowedDirectories).toContain("/repo/B"); - }); - - it("ChatSessionHandler excludes removed repos from allowedDirectories", async () => { - const cyrusHome = TEST_CYRUS_CHAT; - const paths = ["/repo/A", "/repo/B"]; - const provider: ChatRepositoryProvider = { - getRepositoryPaths: () => [...paths], - getDefaultRepository: () => undefined, - getDefaultLinearWorkspaceId: () => undefined, - }; - - let capturedConfig: any; - const createRunner = vi.fn((config: any) => { - capturedConfig = config; - return { - supportsStreamingInput: false, - start: vi.fn().mockResolvedValue({ sessionId: "session-1" }), - stop: vi.fn(), - isRunning: vi.fn().mockReturnValue(false), - isStreaming: vi.fn().mockReturnValue(false), - addStreamMessage: vi.fn(), - getMessages: vi.fn().mockReturnValue([]), - } as any; - }); - - const adapter = new TestChatAdapter("remove-thread"); - const handler = new ChatSessionHandler(adapter, { - cyrusHome, - chatRepositoryProvider: provider, - runnerConfigBuilder: createMockRunnerConfigBuilder(), - createRunner, - onWebhookStart: vi.fn(), - onWebhookEnd: vi.fn(), - onStateChange: vi.fn().mockResolvedValue(undefined), - onClaudeError: vi.fn(), - }); - - // Remove repo A at "runtime" before creating a session - paths.splice(0, 1); - - await handler.handleEvent({ - eventId: "remove-event", - threadKey: "remove-thread", - } as any); - - expect(capturedConfig.allowedDirectories).not.toContain("/repo/A"); - expect(capturedConfig.allowedDirectories).toContain("/repo/B"); - }); -}); - -describe("LiveChatRepositoryProvider", () => { - function makeRepo(id: string, path: string): RepositoryConfig { - return { - id, - name: id, - repositoryPath: path, - baseBranch: "main", - workspaceBaseDir: "/tmp", - } as RepositoryConfig; - } - - it("returns current repository paths from the live map", () => { - const repos = new Map(); - repos.set("r1", makeRepo("r1", "/repo/alpha")); - - const provider = new LiveChatRepositoryProvider(repos, () => ({ ws1: {} })); - - expect(provider.getRepositoryPaths()).toEqual(["/repo/alpha"]); - - // Add a repo at "runtime" - repos.set("r2", makeRepo("r2", "/repo/beta")); - expect(provider.getRepositoryPaths()).toEqual([ - "/repo/alpha", - "/repo/beta", - ]); - - // Remove a repo at "runtime" - repos.delete("r1"); - expect(provider.getRepositoryPaths()).toEqual(["/repo/beta"]); - }); - - it("returns the first repo as default", () => { - const repos = new Map(); - const repo1 = makeRepo("r1", "/repo/alpha"); - repos.set("r1", repo1); - - const provider = new LiveChatRepositoryProvider(repos, () => ({})); - expect(provider.getDefaultRepository()).toBe(repo1); - }); - - it("returns undefined when no repos are configured", () => { - const repos = new Map(); - const provider = new LiveChatRepositoryProvider(repos, () => ({})); - expect(provider.getDefaultRepository()).toBeUndefined(); - expect(provider.getRepositoryPaths()).toEqual([]); - }); - - it("returns first linear workspace ID from live config", () => { - const repos = new Map(); - const workspaces = { ws1: {}, ws2: {} }; - const provider = new LiveChatRepositoryProvider(repos, () => workspaces); - - expect(provider.getDefaultLinearWorkspaceId()).toBe("ws1"); - }); -}); diff --git a/packages/simple-agent-runner/package.json b/packages/simple-agent-runner/package.json index afe79d97e..de309e3f4 100644 --- a/packages/simple-agent-runner/package.json +++ b/packages/simple-agent-runner/package.json @@ -16,7 +16,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.123", + "@anthropic-ai/claude-agent-sdk": "0.2.141", "cyrus-claude-runner": "workspace:*", "cyrus-core": "workspace:*" }, diff --git a/patches/@daytonaio__sdk@0.175.0.patch b/patches/@daytonaio__sdk@0.175.0.patch new file mode 100644 index 000000000..1c652cbb8 --- /dev/null +++ b/patches/@daytonaio__sdk@0.175.0.patch @@ -0,0 +1,12 @@ +diff --git a/package.json b/package.json +index 584c3dcd70df3deaff63f3b00c00543ad803c539..968c1ed91ac8e8f90276cc47ca83df3629f75f0f 100644 +--- a/package.json ++++ b/package.json +@@ -72,6 +72,7 @@ + "@aws-sdk/client-s3": "^3.787.0", + "@aws-sdk/lib-storage": "^3.798.0", + "@iarna/toml": "^2.2.5", ++ "tslib": "^2", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.217.0", + "@opentelemetry/instrumentation-http": "^0.217.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8dabc623..17ac193bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,11 +12,6 @@ overrides: vite: '>=7.1.11' zod: 4.3.6 hono: '>=4.12.18' - fast-uri: '>=3.1.2' - ip-address: '>=10.1.1' - '@anthropic-ai/sdk': '>=0.91.1' - '@opentelemetry/sdk-node': '>=0.217.0' - '@opentelemetry/exporter-prometheus': '>=0.217.0' '@hono/node-server': '>=1.19.10' rollup: '>=4.59.0' flatted: '>=3.4.0' @@ -31,6 +26,23 @@ 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' + protobufjs: '>=7.5.8' + ws: '>=8.20.1' + brace-expansion: '>=5.0.6' + '@anthropic-ai/sdk': '>=0.91.1' + '@daytonaio/sdk': '>=0.175.0' + +packageExtensionsChecksum: sha256-SAGU0eZjbtlthDg7QgtdUpUDPvSoDlnVKHwpQVguKnQ= + +patchedDependencies: + '@daytonaio/sdk@0.175.0': + hash: 2b2d1e24a8cd4da511937cc2273b84e85ac87d55a22542878b135b97ce3f9978 + path: patches/@daytonaio__sdk@0.175.0.patch importers: @@ -151,11 +163,51 @@ 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.28(ws@8.20.1) + '@cyrus-ai/cursor-runner': + specifier: workspace:* + version: link:../cursor-sdk-runner + computesdk: + specifier: ^4.0.0 + version: 4.1.1 + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@anthropic-ai/claude-agent-sdk': + specifier: 0.2.141 + version: 0.2.141(zod@4.3.6) + '@cursor/sdk': + specifier: 1.0.13 + version: 1.0.13 + '@google/gemini-cli-core': + specifier: 0.42.0 + version: 0.42.0(encoding@0.1.13)(express@5.2.1) + '@openai/codex-sdk': + specifier: 0.131.0 + version: 0.131.0 + '@opencode-ai/sdk': + specifier: 1.15.5 + version: 1.15.5 + '@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) + specifier: 0.2.141 + version: 0.2.141(zod@4.3.6) '@anthropic-ai/sdk': specifier: '>=0.91.1' version: 0.91.1(zod@4.3.6) @@ -257,8 +309,8 @@ importers: packages/core: dependencies: '@anthropic-ai/claude-agent-sdk': - specifier: 0.2.123 - version: 0.2.123(zod@4.3.6) + specifier: 0.2.141 + version: 0.2.141(zod@4.3.6) '@linear/sdk': specifier: ^64.0.0 version: 64.0.0(encoding@0.1.13) @@ -304,11 +356,30 @@ 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/cursor-sdk-runner: + dependencies: + '@cursor/sdk': + specifier: 1.0.13 + version: 1.0.13 + 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/edge-worker: dependencies: '@anthropic-ai/claude-agent-sdk': - specifier: 0.2.123 - version: 0.2.123(zod@4.3.6) + specifier: 0.2.141 + version: 0.2.141(zod@4.3.6) + '@computesdk/daytona': + specifier: ^1.7.26 + version: 1.7.28(ws@8.20.1) '@linear/sdk': specifier: ^64.0.0 version: 64.0.0(encoding@0.1.13) @@ -318,6 +389,12 @@ importers: chokidar: specifier: ^4.0.3 version: 4.0.3 + computesdk: + specifier: ^4.0.0 + version: 4.1.1 + cyrus-agent-runtime: + specifier: workspace:* + version: link:../agent-runtime cyrus-claude-runner: specifier: workspace:* version: link:../claude-runner @@ -511,7 +588,7 @@ importers: version: 11.3.4 openai: specifier: ^6.9.1 - version: 6.35.0(ws@8.20.0)(zod@4.3.6) + version: 6.35.0(ws@8.20.1)(zod@4.3.6) zod: specifier: 4.3.6 version: 4.3.6 @@ -532,8 +609,8 @@ importers: packages/simple-agent-runner: dependencies: '@anthropic-ai/claude-agent-sdk': - specifier: 0.2.123 - version: 0.2.123(zod@4.3.6) + specifier: 0.2.141 + version: 0.2.141(zod@4.3.6) cyrus-claude-runner: specifier: workspace:* version: link:../claude-runner @@ -572,56 +649,71 @@ importers: packages: + '@a2a-js/sdk@0.3.11': + resolution: {integrity: sha512-pXjjlL0ZYHgAxObov1J+W3ylfQV0rOrDBB8Eo4a9eCunqs7iNW5OIfMcV8YnZQdzeVSRomj8jHeudVz0zc4RNw==} + engines: {node: '>=18'} + peerDependencies: + '@bufbuild/protobuf': ^2.10.2 + '@grpc/grpc-js': ^1.11.0 + express: ^4.21.2 || ^5.1.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + '@grpc/grpc-js': + optional: true + express: + optional: true + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.123': - resolution: {integrity: sha512-tYAXCjlXZQklsUs0J//gip3fZQRzhlH5OCgvNXV70qe7A1iiwHqO2KPGvEHV1L+deEKQoMZmTaCOrQpN6zju3w==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.141': + resolution: {integrity: sha512-9HZ0ot6+FwOfQ1aeMqQLH4IJGMm/DcP08SysDxscVjBm6l2JjqleHohxi3zid0DurfGweqT+4x9GScJffwg55g==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.123': - resolution: {integrity: sha512-AcUC6sTon6z6HculP87KsAOeTMRLBwpovdhcXUTjXUpo/8nplJ7lBEzWjZCHt8FF1KuN/WBy1Z4bDg/59TQDmA==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.141': + resolution: {integrity: sha512-4iAdarJaQ+2R58s6QJswZCzUdz2WQmL5lYG7Y+FLzWbRSROFfcH0QYpmOqSaPXd2KRQhIJwEacqecDZd/Q1XKQ==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.123': - resolution: {integrity: sha512-bYgRiaf2q+yVbGAoUluuhqrEW1zexL34+3HDmK9DneKXa2K2EJpw4M6Sq4XoBD/JezGaemoAP78Xv/M/QUS1OQ==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.141': + resolution: {integrity: sha512-6H1AJ/AVaWNnV22kubUPkOTRzZFH0+qP9k7WlhriHMN9gtgZcVAsITMddDeGjQsQJMCAdhXFd6sgi7TM1LdeOQ==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.123': - resolution: {integrity: sha512-7+GnbcF3/aZ8RJ1WmU/ogtPsOpknBAoUPer90MvZuFYBLPT9iI/U7f24gjrOHuYdcbDA5n7jFlhcfIO26F5DJQ==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.141': + resolution: {integrity: sha512-Jdf0ZEwJzOP8sE6rPqdJN+SxMb0/L8sxJg4twCv/7S+Qzk0hJtls+wxSi+0Tjh6EEMaNxJqEGc7S3fx99Wi99Q==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.123': - resolution: {integrity: sha512-IX95lFKhmmndY/YPfWPsVV+C3rLYJmuuq5wCS53p6jYIkCMxH1iGfhBGF1EUWcXO4Uc8yqXFmQ3aaxMzOOPrwA==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.141': + resolution: {integrity: sha512-fTI1YuM4cxOa4nSgsyMAdB5ELizkWp+w5Ispo4JnnYtcczMAL4D9GBNjWPW0sUzKvjsJOUVim68SmWLWhUOpXQ==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.123': - resolution: {integrity: sha512-Xi+Rwk8uP5vWEnawJOlsk179fr0ATLl5J90MlbLj+puKaX5svEq8ljS+P3zq6zHTJeKh9GKLzPf7bc5YJKwcew==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.141': + resolution: {integrity: sha512-DVjp72f3HmrRYpbneWZZWIqkUht5kTZXS7wXGFiwzLz6eNYEgjjh+GcsnhIi8UOwZUtNiKUrjZnoP38ovFqV8A==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.123': - resolution: {integrity: sha512-WDZmAQG1rOiqNLZlSXaCjSWmqJvLk2io+vFQWWqSy2b5HCk9pa3PadLiaLztiihyk81wPhH9Q/44kOxdyfEGMw==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.141': + resolution: {integrity: sha512-Wm10J6kfbufbPGFELokiJ/7Y5Oqug4Uag3HXFsV8g7TWCpaItx/oqVaJoiGptuAtXQB7xGLQVTuk082wER+Y5w==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.123': - resolution: {integrity: sha512-588xrd1i6d4kXQ6FqwL+cgBiN4evRQSi5DCtPa02CZ3VEbuVQBeFlyPlD8tfWtNNeGZ4NM8kjPNNzZz5omezPA==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.141': + resolution: {integrity: sha512-IXuP29YJuWbR5Q6xOHrjFVGG54V2s1FC61UVNwEN5fpxL09MwPnbwtQL6fqgzt/U1MP7vWAwpXZriYAklkH/mg==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.2.123': - resolution: {integrity: sha512-a4TysYoR9DBdkM9Uwh4J5ub7TwKmRPe5hFiWh4En+IKC+qkk5UFkxFM22c//cZjYZKynHX0ah2t6LUqb+najYA==} + '@anthropic-ai/claude-agent-sdk@0.2.141': + resolution: {integrity: sha512-AIBacMWGcZIUcXlUoObqjwJ6pmJI3BayAqPAFXuvSq3DHJXdiuZVs7l/zTB5l3nRhRv5cqSrI2XbiDeHgZWizw==} engines: {node: '>=18.0.0'} peerDependencies: zod: 4.3.6 @@ -635,6 +727,131 @@ packages: zod: optional: 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.1050.0': + resolution: {integrity: sha512-9kgtv+bXZQrOIJT2INPPBCezrJu1FlgGrzEat/ut4A4V53IT00LynsBZgp12eFKbjJuNCeTo7iPSKjPsX8ub+A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.12': + resolution: {integrity: sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==} + 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.38': + resolution: {integrity: sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.40': + resolution: {integrity: sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.42': + resolution: {integrity: sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.42': + resolution: {integrity: sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.43': + resolution: {integrity: sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.38': + resolution: {integrity: sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.42': + resolution: {integrity: sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.42': + resolution: {integrity: sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/lib-storage@3.1050.0': + resolution: {integrity: sha512-+PyWDxWPRSEi0l4fU1EmhLK9XvRe70cXlfUPdJqHfJ3E3DKWrihj314i7K5Z69bv015qjorIyDeHLKMHcGGp+g==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@aws-sdk/client-s3': ^3.1050.0 + + '@aws-sdk/middleware-bucket-endpoint@3.972.14': + resolution: {integrity: sha512-Aaj0d+xbo1jJquBWJP0/9V/XZRYukO3LWIRp3dOLHmoFrYKb4YZ0aLefgVHfGcNOVBS2ZTq7L/n5JcrE7DaC+Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.12': + resolution: {integrity: sha512-dA5pKTom/Ls9mgeyeaRBNQrRIVOLVjv4AmKOB0/e4yaiXEUy0gSz2d3liP8JHtYoCAEWySU1jWnyzwLOREN+4g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.20': + resolution: {integrity: sha512-NdnMVQCR1YjIcqFAiNLdBiOwr2DyQDB2IiXQrBhzolKOv32ae4d4Ll7IzLMi04eMHiq/o/Y/GjFuVjF9HuG0QA==} + 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-sdk-s3@3.972.41': + resolution: {integrity: sha512-M4T2I2WPuH5WQpU8Tsp+u2bcO29zGRkU14ATzuqb9I4xh8tzsLqtp4hzaJM5aO2dhMZnHDzyQwSFVgc3XbnoGg==} + 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/nested-clients@3.997.10': + resolution: {integrity: sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.27': + resolution: {integrity: sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1049.0': + resolution: {integrity: sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + 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/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'} @@ -727,6 +944,18 @@ packages: '@bufbuild/protobuf@1.10.0': resolution: {integrity: sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==} + '@bufbuild/protobuf@2.12.0': + resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==} + + '@computesdk/cmd@0.4.1': + resolution: {integrity: sha512-hhcYrwMnOpRSwWma3gkUeAVsDFG56nURwSaQx8vCepv0IuUv39bK4mMkgszolnUQrVjBDdW7b3lV+l5B2S8fRA==} + + '@computesdk/daytona@1.7.28': + resolution: {integrity: sha512-nF/YGG0U9EgmOfj5+JOm1byh/9VWWnLGOkII37dsJASCzgAR2Nv47eIcP8tX2ud2klhW9u7DYsO73ejhIBJXEw==} + + '@computesdk/provider@2.1.1': + resolution: {integrity: sha512-pyz1Pm9Q/WOo/j5iRGebs4HC1C+NbGq1L8btzNbGs+qoQwqKgrGY85/jp6eWO9fRP2bjCy/1JSrjhMMO78ep2w==} + '@connectrpc/connect-node@1.7.0': resolution: {integrity: sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==} engines: {node: '>=16.0.0'} @@ -744,30 +973,68 @@ packages: cpu: [arm64] os: [darwin] + '@cursor/sdk-darwin-arm64@1.0.13': + resolution: {integrity: sha512-zHRTNtVRHw4KSAEFmtO0Av7jv9D60DrB+pygVNWGyKtRR44fcwtRHuLAJmO4HThxQw7MMvUJuAaNmCQxzHtPDQ==} + cpu: [arm64] + os: [darwin] + '@cursor/sdk-darwin-x64@1.0.11': resolution: {integrity: sha512-2352S+tGbaDgj2qb3oNN2FUG5250cn3cD+aKluETFd7jI7Pm3ctwInFN+/NWWnzwftibjKnwcc8ghm9q4xYfWg==} cpu: [x64] os: [darwin] + '@cursor/sdk-darwin-x64@1.0.13': + resolution: {integrity: sha512-7XsIkMKp6h/4W9zBx02Py1euJLAJVxlkwmm9GSoUjc+3hfFvHY/R/WTbX2TFgF4g1vOAq/HM7GmXBXq+e4M4+w==} + cpu: [x64] + os: [darwin] + '@cursor/sdk-linux-arm64@1.0.11': resolution: {integrity: sha512-SGnwU1caprU6L7XCMUH48pyGdrZz1YQhPNUzrUyixHpdfM951KJmAQyuW9Hj2J4J3C1PG4XwIYRHsGN8/EOF2g==} cpu: [arm64] os: [linux] + '@cursor/sdk-linux-arm64@1.0.13': + resolution: {integrity: sha512-bDgfPPgc84gUn3k+Iiq5OLZozzM0UYZdKbQ821pbZy1OPWTFaSkjXsoAB6xqf9wALWyW1eQxOC4RprPBLoy+yA==} + cpu: [arm64] + os: [linux] + '@cursor/sdk-linux-x64@1.0.11': resolution: {integrity: sha512-zzVwEMc9ykyyFgxaXwfiB0Nuqnp0PkKqiWSt6Iubmi7ADY87dtVS67qwtmVQ+FJVA7iXV+c7LY2sQ2qfQ4aP2w==} cpu: [x64] os: [linux] + '@cursor/sdk-linux-x64@1.0.13': + resolution: {integrity: sha512-BTccnB5hVqK8Y0778oql6gbk7kIIlzQrBqt5QNLJpwBidjjde/mlvAajVB9hB3a29jelOwm0gJjMsLfqTkEPdw==} + cpu: [x64] + os: [linux] + '@cursor/sdk-win32-x64@1.0.11': resolution: {integrity: sha512-iWvGDFhpW+C6/zah7feY3oURozJxQ78qjld+9ejOaRuuC6p33Q6D/3l6Ihst18lEH9WSjEJClydDFUbm7aPf5A==} cpu: [x64] os: [win32] + '@cursor/sdk-win32-x64@1.0.13': + resolution: {integrity: sha512-GxWlwj4G513EfGmvPVBa4y+vNn9B5Cj+npu8fVcJ0P+U9sruhgo4pvqGbWxkn5EIKbpGoraLq9QB4nFeoT1uRQ==} + cpu: [x64] + os: [win32] + '@cursor/sdk@1.0.11': resolution: {integrity: sha512-DkTwOAuao9wIeUioaM0aQi6hkWLC8oLAnqlR4HR9hn5xytd9A4cEB2fZpSHd8pJ2YRN0VJVkxnggxLRNT7ghuQ==} engines: {node: '>=18'} + '@cursor/sdk@1.0.13': + resolution: {integrity: sha512-w6MWkgOTL6yb6GV/4Odx7QcamQgqhzX/CzcMBkqiiOPTPuEWItWrgA0qdivchm5YJXTt+LZkFSEQ/Ti44hVbfg==} + 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==} @@ -954,6 +1221,9 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@github/keytar@7.10.6': + resolution: {integrity: sha512-mRW6cUsSG+nj4jp5gp8e91zPySaT73r+2JM6VyMZfrEgksjPmjSMr+tPGNOK3HUHV+GUU9B1LAiiYy/wmAnIxA==} + '@google-cloud/common@5.0.2': resolution: {integrity: sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==} engines: {node: '>=14.0.0'} @@ -1007,6 +1277,10 @@ packages: resolution: {integrity: sha512-WDpBYZiHeJyurZMmB9iYq5t3TsZnhmq1sCtX5ZIrRN7pvrhfVAkCAjQs7FHVFOQYYX4lvsIm7Epox1pgMb0ivw==} engines: {node: '>=20'} + '@google/gemini-cli-core@0.42.0': + resolution: {integrity: sha512-bdPGdoOLqCnMl7DAUtJdsvKJJP1bWvbh0FwWeQ63xk0hGOyPs0lzRshDYKq436R5+nEvzIFgx7kjA6WKG41lOg==} + engines: {node: '>=20'} + '@google/genai@1.16.0': resolution: {integrity: sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw==} engines: {node: '>=20.0.0'} @@ -1016,6 +1290,15 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@google/genai@1.30.0': + resolution: {integrity: sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': '>=1.26.0' + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: @@ -1233,6 +1516,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==} @@ -1245,6 +1543,10 @@ packages: resolution: {integrity: sha512-1xCIHdSbQVF880nJ2aVWdPIsWZbSpKODwuP9y/gvtChDYhYfYEW0DKp2H8ZlctkzIjlzS/WzYmP6ZZPHIvs2Dg==} engines: {node: '>=18'} + '@openai/codex-sdk@0.131.0': + resolution: {integrity: sha512-fSIJKPGkxVsKu5TsU9pcCvGfYxiPLtfuJOkuk9VIqeZHQwlhVByZ8MVTiBhsd4mr5hxgyeyPSiofUONULdaPWQ==} + engines: {node: '>=18'} + '@openai/codex@0.125.0': resolution: {integrity: sha512-GiE9wlgL95u/5BRirY5d3EaRLU1tu7Y1R09R8lCHHVmcQdSmhS809FdPDWH3gIYHS7ZriAPqXwJ3aLA0WKl40Q==} engines: {node: '>=16'} @@ -1286,10 +1588,62 @@ packages: cpu: [x64] os: [win32] + '@openai/codex@0.131.0': + resolution: {integrity: sha512-5/fNFAotnPaNSX1jGAAGgWk65HGZupWPnka+DzXdoNzl78RGw0eGpOjpowF+dtPRTEvdwt0U8qoptUjtefitBQ==} + engines: {node: '>=16'} + hasBin: true + + '@openai/codex@0.131.0-darwin-arm64': + resolution: {integrity: sha512-e4EZ7XjK1zrkW65nZdhCqQFVKd/zt/of26w4L32wcfibzKE15/Lw2i3FGTD9ufUUqVzF+gowu/lEiBRvDoh21w==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@openai/codex@0.131.0-darwin-x64': + resolution: {integrity: sha512-BkFwUVU+yJhw5a85p1oagiSjkRvBs9Xp1b1q1Qbvg4Y9Chtatj4SjI7HRjH5uespNs/Wnh48cMnTsxkAILqlBw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@openai/codex@0.131.0-linux-arm64': + resolution: {integrity: sha512-I12Ou5I5oR/nfUyMJ/D95OMm83HoXMBHdl+4YrPay/XGd9KkyMhvZxp9Es/h9x/xt8yiBIk+k0Ehtr093r8AFw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@openai/codex@0.131.0-linux-x64': + resolution: {integrity: sha512-Fj9P7h3iBgjAQKzoEyUkb1Q8QMVLqaf62UzlL1jYeDhIzbDMI/gaV0tOackIGXPcfguzzORJC1g5pD9SMWqU5g==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@openai/codex@0.131.0-win32-arm64': + resolution: {integrity: sha512-aGXsk8GYNFCjHYH61mVG0PulheBq6ZxZWM2BLYAJabzGarzRQpPknc60mx8mSm93BebjPkX0Wpf+0qInBPbF0w==} + engines: {node: '>=16'} + cpu: [arm64] + os: [win32] + + '@openai/codex@0.131.0-win32-x64': + resolution: {integrity: sha512-RlIRs0hi6xIaBXWFhiPTPlz+Dfibc3pXrnVXjJtpbeYcNyBOO+SIMMVuQUswpz3RnJpsxA70dQA+05hKYraKhA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@opencode-ai/sdk@1.15.5': + resolution: {integrity: sha512-ozJuEmXzrOvia5n0L1KAuvpyf9ESGmTk1FiPhn0RK5X1whbzjlTXL0NAxqNCEkqETxL35jS1KHArEiTpvtJ6FQ==} + '@opentelemetry/api-logs@0.203.0': resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.211.0': + resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} + 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.218.0': resolution: {integrity: sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==} engines: {node: '>=8.0.0'} @@ -1332,6 +1686,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.5.0': + resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.7.1': resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1344,6 +1704,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0': + resolution: {integrity: sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.218.0': resolution: {integrity: sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1356,6 +1722,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-http@0.211.0': + resolution: {integrity: sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-http@0.218.0': resolution: {integrity: sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1374,6 +1746,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0': + resolution: {integrity: sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-grpc@0.218.0': resolution: {integrity: sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1386,6 +1764,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-http@0.211.0': + resolution: {integrity: sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-http@0.218.0': resolution: {integrity: sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1410,6 +1794,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0': + resolution: {integrity: sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.218.0': resolution: {integrity: sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1422,6 +1812,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.211.0': + resolution: {integrity: sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@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-http@0.218.0': resolution: {integrity: sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1494,6 +1896,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-http@0.211.0': + resolution: {integrity: sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==} + engines: {node: ^18.19.0 || >=20.6.0} + 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'} @@ -1584,6 +1998,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.211.0': + resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} + engines: {node: ^18.19.0 || >=20.6.0} + 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.218.0': resolution: {integrity: sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1602,6 +2028,18 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.211.0': + resolution: {integrity: sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==} + engines: {node: ^18.19.0 || >=20.6.0} + 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-exporter-base@0.218.0': resolution: {integrity: sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1614,6 +2052,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.211.0': + resolution: {integrity: sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.218.0': resolution: {integrity: sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1626,19 +2070,31 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.218.0': - resolution: {integrity: sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==} + '@opentelemetry/otlp-transformer@0.211.0': + resolution: {integrity: sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==} 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==} + '@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.0.0 <1.10.0' + '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-jaeger@2.7.1': + '@opentelemetry/otlp-transformer@0.218.0': + resolution: {integrity: sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==} + 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.7.1': resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: @@ -1666,6 +2122,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.5.0': + resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.7.1': resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1678,6 +2140,18 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.211.0': + resolution: {integrity: sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==} + engines: {node: ^18.19.0 || >=20.6.0} + 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-logs@0.218.0': resolution: {integrity: sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1690,6 +2164,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.5.0': + resolution: {integrity: sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.7.1': resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1714,6 +2194,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.5.0': + resolution: {integrity: sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.7.1': resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1758,35 +2244,10 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.1': - resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@puppeteer/browsers@2.13.2': + resolution: {integrity: sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==} + engines: {node: '>=18'} + hasBin: true '@rolldown/binding-android-arm64@1.0.0-rc.17': resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} @@ -1936,6 +2397,42 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@smithy/core@3.24.3': + resolution: {integrity: sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.3': + resolution: {integrity: sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.3': + resolution: {integrity: sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==} + 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.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.3': + resolution: {integrity: sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} + 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==} @@ -1953,6 +2450,9 @@ packages: resolution: {integrity: sha512-VyMVKRrpHTT8PnotUeV8L/mDaMwD5DaAKCFLP73zAqAtvF0FCqky+Ki7BYbFCYQmqFyTe9316Ed5zS70QUR9eg==} engines: {node: '>= 10'} + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2154,6 +2654,9 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} @@ -2162,6 +2665,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.12: resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} @@ -2178,13 +2685,66 @@ packages: axios@1.15.2: resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.8.3: + resolution: {integrity: sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.1: + resolution: {integrity: sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.1: + resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.1: + resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.3: + resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -2202,8 +2762,11 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -2216,6 +2779,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==} @@ -2223,6 +2789,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'} @@ -2251,6 +2821,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -2274,6 +2848,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -2285,6 +2863,11 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + chromium-bidi@14.0.0: + resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==} + peerDependencies: + devtools-protocol: '*' + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -2329,10 +2912,16 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + computesdk@4.1.1: + resolution: {integrity: sha512-fiWnRTDKs6jAl0dTUPZ6MJZPK6TwE7TIGUJHi4ytMA4Bzym2XToVgKbnEYHpLBFggWy1Wa349Nw0OqXMzF8tEA==} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -2364,6 +2953,18 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + daemond@0.1.3: + resolution: {integrity: sha512-Veq4R2JEKb7x/PI7jX50XFUnjuQAj4RnohrNKi/OJgsQJ63JR39+BaOPA3YQuqTOAXy15dHbINjOemxHwU2xpA==} + engines: {node: '>=18.0.0'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2409,10 +3010,18 @@ packages: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2432,6 +3041,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devtools-protocol@0.0.1608973: + resolution: {integrity: sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==} + diff@9.0.0: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} @@ -2453,6 +3065,10 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2542,9 +3158,27 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2560,6 +3194,13 @@ packages: resolution: {integrity: sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==} engines: {node: '>=10'} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + 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'} @@ -2576,6 +3217,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'} @@ -2604,6 +3249,13 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + 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==} @@ -2616,6 +3268,13 @@ packages: 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==} @@ -2637,6 +3296,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -2695,6 +3358,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -2740,10 +3407,18 @@ packages: resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} engines: {node: '>=14'} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + gcp-metadata@6.1.1: resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} engines: {node: '>=14'} + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2771,6 +3446,10 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -2792,6 +3471,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} @@ -2804,6 +3487,10 @@ packages: resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} engines: {node: '>=14'} + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + googleapis-common@7.2.0: resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} engines: {node: '>=14.0.0'} @@ -2839,6 +3526,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2854,6 +3544,10 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + 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'} @@ -2890,6 +3584,10 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http2-wrapper@2.2.1: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} @@ -2935,6 +3633,9 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + import-in-the-middle@3.0.1: resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} engines: {node: '>=18'} @@ -3044,12 +3745,24 @@ packages: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '>=8.20.1' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3085,6 +3798,10 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -3101,9 +3818,16 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -3230,6 +3954,10 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3261,6 +3989,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'} @@ -3337,6 +4073,9 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -3374,6 +4113,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + node-abi@3.89.0: resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} engines: {node: '>=10'} @@ -3385,6 +4128,11 @@ packages: resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} engines: {node: ^18 || ^20 || >= 21} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3394,6 +4142,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.4.0: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} @@ -3453,6 +4205,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + obliterator@2.0.5: resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} @@ -3479,7 +4235,7 @@ packages: resolution: {integrity: sha512-L/skwIGnt5xQZHb0UfTu9uAUKbis3ehKypOuJKi20QvG7UStV6C8IC3myGYHcdiF4kms/bAvOJ9UqqNWqi8x/Q==} hasBin: true peerDependencies: - ws: ^8.18.0 + ws: '>=8.20.1' zod: 4.3.6 peerDependenciesMeta: ws: @@ -3495,6 +4251,14 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -3506,6 +4270,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==} @@ -3517,6 +4285,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'} @@ -3628,6 +4400,10 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -3640,18 +4416,28 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + proto3-json-serializer@2.0.2: resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} engines: {node: '>=14.0.0'} - protobufjs@7.5.6: - resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} + protobufjs@8.4.0: + resolution: {integrity: sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -3665,10 +4451,17 @@ packages: pumpify@2.0.1: resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + puppeteer-core@24.43.1: + resolution: {integrity: sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==} + engines: {node: '>=18'} + qs@6.15.1: 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==} @@ -3708,6 +4501,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -3784,6 +4581,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==} @@ -3823,6 +4623,10 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -3900,6 +4704,10 @@ packages: resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} engines: {node: '>= 10'} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + socks@2.8.8: resolution: {integrity: sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} @@ -3911,6 +4719,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -3944,12 +4756,22 @@ 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'} + + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3989,9 +4811,16 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + 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'} @@ -4011,13 +4840,25 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + systeminformation@5.31.6: + resolution: {integrity: sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@3.1.2: + resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + tar@7.5.13: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} @@ -4026,10 +4867,16 @@ packages: resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} engines: {node: '>=14'} + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + test-exclude@7.0.2: resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thread-stream@4.0.0: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} @@ -4125,6 +4972,9 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typed-query-selector@2.12.2: + resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -4175,6 +5025,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + uuid@13.0.2: + resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -4274,6 +5132,10 @@ packages: jsdom: optional: true + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-tree-sitter@0.25.10: resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} peerDependencies: @@ -4282,6 +5144,9 @@ packages: '@types/emscripten': optional: true + webdriver-bidi-protocol@0.4.1: + resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4316,8 +5181,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4336,6 +5201,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'} @@ -4381,49 +5250,57 @@ packages: snapshots: + '@a2a-js/sdk@0.3.11(@bufbuild/protobuf@2.12.0)(@grpc/grpc-js@1.14.3)(express@5.2.1)': + dependencies: + uuid: 11.1.1 + optionalDependencies: + '@bufbuild/protobuf': 2.12.0 + '@grpc/grpc-js': 1.14.3 + express: 5.2.1 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.123': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.123': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.123': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.123': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.123': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.123': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.123': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.123': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.141': optional: true - '@anthropic-ai/claude-agent-sdk@0.2.123(zod@4.3.6)': + '@anthropic-ai/claude-agent-sdk@0.2.141(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) zod: 4.3.6 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.2.123 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.2.123 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.2.123 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.2.123 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.2.123 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.2.123 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.2.123 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.2.123 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.2.141 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.2.141 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.2.141 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.2.141 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.2.141 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.2.141 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.2.141 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.2.141 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color @@ -4434,34 +5311,309 @@ snapshots: optionalDependencies: zod: 4.3.6 - '@babel/code-frame@7.29.0': + '@aws-crypto/crc32@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/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 - '@babel/helper-validator-identifier@7.28.5': {} + '@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/parser@7.29.2': + '@aws-crypto/sha256-browser@5.2.0': dependencies: - '@babel/types': 7.29.0 + '@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/runtime@7.29.2': {} + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 - '@babel/types@7.29.0': + '@aws-crypto/supports-web-crypto@5.2.0': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + tslib: 2.8.1 - '@bcoe/v8-coverage@1.0.2': {} + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 - '@biomejs/biome@2.4.13': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.13 - '@biomejs/cli-darwin-x64': 2.4.13 - '@biomejs/cli-linux-arm64': 2.4.13 + '@aws-sdk/client-s3@3.1050.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.12 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/middleware-bucket-endpoint': 3.972.14 + '@aws-sdk/middleware-expect-continue': 3.972.12 + '@aws-sdk/middleware-flexible-checksums': 3.974.20 + '@aws-sdk/middleware-location-constraint': 3.972.10 + '@aws-sdk/middleware-sdk-s3': 3.972.41 + '@aws-sdk/middleware-ssec': 3.972.10 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.12': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.24 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.8': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.40': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-login': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.43': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-ini': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/token-providers': 3.1049.0 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/lib-storage@3.1050.0(@aws-sdk/client-s3@3.1050.0)': + dependencies: + '@aws-sdk/client-s3': 3.1050.0 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-bucket-endpoint@3.972.14': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.12': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.20': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/crc64-nvme': 3.972.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.41': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.10': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.27': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1049.0': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.24': + dependencies: + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.2 + 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': {} + + '@biomejs/biome@2.4.13': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.13 + '@biomejs/cli-darwin-x64': 2.4.13 + '@biomejs/cli-linux-arm64': 2.4.13 '@biomejs/cli-linux-arm64-musl': 2.4.13 '@biomejs/cli-linux-x64': 2.4.13 '@biomejs/cli-linux-x64-musl': 2.4.13 @@ -4496,7 +5648,27 @@ snapshots: '@bufbuild/protobuf@1.10.0': {} - '@connectrpc/connect-node@1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0))': + '@bufbuild/protobuf@2.12.0': {} + + '@computesdk/cmd@0.4.1': {} + + '@computesdk/daytona@1.7.28(ws@8.20.1)': + dependencies: + '@computesdk/provider': 2.1.1 + '@daytonaio/sdk': 0.175.0(patch_hash=2b2d1e24a8cd4da511937cc2273b84e85ac87d55a22542878b135b97ce3f9978)(ws@8.20.1) + computesdk: 4.1.1 + transitivePeerDependencies: + - debug + - supports-color + - ws + + '@computesdk/provider@2.1.1': + dependencies: + '@computesdk/cmd': 0.4.1 + computesdk: 4.1.1 + daemond: 0.1.3 + + '@connectrpc/connect-node@1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@2.12.0))': dependencies: '@bufbuild/protobuf': 1.10.0 '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) @@ -4509,23 +5681,38 @@ snapshots: '@cursor/sdk-darwin-arm64@1.0.11': optional: true + '@cursor/sdk-darwin-arm64@1.0.13': + optional: true + '@cursor/sdk-darwin-x64@1.0.11': optional: true + '@cursor/sdk-darwin-x64@1.0.13': + optional: true + '@cursor/sdk-linux-arm64@1.0.11': optional: true + '@cursor/sdk-linux-arm64@1.0.13': + optional: true + '@cursor/sdk-linux-x64@1.0.11': optional: true + '@cursor/sdk-linux-x64@1.0.13': + optional: true + '@cursor/sdk-win32-x64@1.0.11': optional: true + '@cursor/sdk-win32-x64@1.0.13': + optional: true + '@cursor/sdk@1.0.11': dependencies: '@bufbuild/protobuf': 1.10.0 '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) - '@connectrpc/connect-node': 1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@1.10.0)) + '@connectrpc/connect-node': 1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@2.12.0)) '@statsig/js-client': 3.31.0 sqlite3: 5.1.7 zod: 4.3.6 @@ -4539,6 +5726,67 @@ snapshots: - bluebird - supports-color + '@cursor/sdk@1.0.13': + dependencies: + '@bufbuild/protobuf': 1.10.0 + '@connectrpc/connect': 1.7.0(@bufbuild/protobuf@1.10.0) + '@connectrpc/connect-node': 1.7.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.7.0(@bufbuild/protobuf@2.12.0)) + '@statsig/js-client': 3.31.0 + sqlite3: 5.1.7 + zod: 4.3.6 + optionalDependencies: + '@cursor/sdk-darwin-arm64': 1.0.13 + '@cursor/sdk-darwin-x64': 1.0.13 + '@cursor/sdk-linux-arm64': 1.0.13 + '@cursor/sdk-linux-x64': 1.0.13 + '@cursor/sdk-win32-x64': 1.0.13 + transitivePeerDependencies: + - 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(patch_hash=2b2d1e24a8cd4da511937cc2273b84e85ac87d55a22542878b135b97ce3f9978)(ws@8.20.1)': + dependencies: + '@aws-sdk/client-s3': 3.1050.0 + '@aws-sdk/lib-storage': 3.1050.0(@aws-sdk/client-s3@3.1050.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.218.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.1) + pathe: 2.0.3 + shell-quote: 1.8.3 + tar: 7.5.13 + tslib: 2.8.1 + transitivePeerDependencies: + - debug + - supports-color + - ws + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -4659,6 +5907,11 @@ snapshots: '@gar/promisify@1.1.3': optional: true + '@github/keytar@7.10.6': + dependencies: + node-addon-api: 8.7.0 + optional: true + '@google-cloud/common@5.0.2(encoding@0.1.13)': dependencies: '@google-cloud/projectify': 4.0.0 @@ -4793,7 +6046,7 @@ snapshots: tree-sitter-bash: 0.25.1 undici: 8.1.0 web-tree-sitter: 0.25.10 - ws: 8.20.0 + ws: 8.20.1 zod: 4.3.6 optionalDependencies: '@lydell/node-pty': 1.1.0 @@ -4816,10 +6069,103 @@ snapshots: - tree-sitter - utf-8-validate + '@google/gemini-cli-core@0.42.0(encoding@0.1.13)(express@5.2.1)': + dependencies: + '@a2a-js/sdk': 0.3.11(@bufbuild/protobuf@2.12.0)(@grpc/grpc-js@1.14.3)(express@5.2.1) + '@bufbuild/protobuf': 2.12.0 + '@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.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.30.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) + '@grpc/grpc-js': 1.14.3 + '@iarna/toml': 2.2.5 + '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.218.0(@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 + '@types/html-to-text': 9.0.4 + '@xterm/headless': 5.5.0 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + chardet: 2.1.1 + chokidar: 5.0.0 + command-exists: 1.2.9 + diff: 9.0.0 + dotenv: 17.4.2 + dotenv-expand: 12.0.3 + execa: 9.6.1 + fast-levenshtein: 2.0.6 + fdir: 6.5.0(picomatch@4.0.4) + fzf: 0.5.2 + glob: 12.0.0 + google-auth-library: 9.15.1(encoding@0.1.13) + html-to-text: 9.0.5 + https-proxy-agent: 7.0.6 + ignore: 7.0.5 + ipaddr.js: 1.9.1 + isbinaryfile: 5.0.7 + js-yaml: 4.1.1 + json-stable-stringify: 1.3.0 + marked: 15.0.12 + mime: 4.0.7 + mnemonist: 0.40.4 + open: 10.2.0 + picomatch: 4.0.4 + proper-lockfile: 4.1.2 + puppeteer-core: 24.43.1 + read-package-up: 11.0.0 + shell-quote: 1.8.3 + simple-git: 3.36.0 + strip-ansi: 7.2.0 + strip-json-comments: 3.1.1 + systeminformation: 5.31.6 + tree-sitter-bash: 0.25.1 + undici: 8.1.0 + uuid: 13.0.2 + web-tree-sitter: 0.25.10 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + optionalDependencies: + '@github/keytar': 7.10.6 + '@lydell/node-pty': 1.1.0 + '@lydell/node-pty-darwin-arm64': 1.1.0 + '@lydell/node-pty-darwin-x64': 1.1.0 + '@lydell/node-pty-linux-x64': 1.1.0 + '@lydell/node-pty-win32-arm64': 1.1.0 + '@lydell/node-pty-win32-x64': 1.1.0 + node-pty: 1.1.0 + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@types/emscripten' + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - express + - react-native-b4a + - supports-color + - tree-sitter + - utf-8-validate + '@google/genai@1.16.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(encoding@0.1.13)': dependencies: google-auth-library: 9.15.1(encoding@0.1.13) - ws: 8.20.0 + ws: 8.20.1 optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) transitivePeerDependencies: @@ -4828,6 +6174,17 @@ snapshots: - supports-color - utf-8-validate + '@google/genai@1.30.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))': + dependencies: + google-auth-library: 10.6.2 + ws: 8.20.1 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@graphql-typed-document-node/core@3.2.0(graphql@15.10.2)': dependencies: graphql: 15.10.2 @@ -4841,14 +6198,14 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.6 + protobufjs: 8.4.0 yargs: 17.7.2 '@grpc/proto-loader@0.8.0': dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.6 + protobufjs: 8.4.0 yargs: 17.7.2 '@hono/node-server@2.0.1(hono@4.12.18)': @@ -5042,6 +6399,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 @@ -5058,6 +6429,10 @@ snapshots: dependencies: '@openai/codex': 0.125.0 + '@openai/codex-sdk@0.131.0': + dependencies: + '@openai/codex': 0.131.0 + '@openai/codex@0.125.0': optionalDependencies: '@openai/codex-darwin-arm64': '@openai/codex@0.125.0-darwin-arm64' @@ -5085,10 +6460,49 @@ snapshots: '@openai/codex@0.125.0-win32-x64': optional: true + '@openai/codex@0.131.0': + optionalDependencies: + '@openai/codex-darwin-arm64': '@openai/codex@0.131.0-darwin-arm64' + '@openai/codex-darwin-x64': '@openai/codex@0.131.0-darwin-x64' + '@openai/codex-linux-arm64': '@openai/codex@0.131.0-linux-arm64' + '@openai/codex-linux-x64': '@openai/codex@0.131.0-linux-x64' + '@openai/codex-win32-arm64': '@openai/codex@0.131.0-win32-arm64' + '@openai/codex-win32-x64': '@openai/codex@0.131.0-win32-x64' + + '@openai/codex@0.131.0-darwin-arm64': + optional: true + + '@openai/codex@0.131.0-darwin-x64': + optional: true + + '@openai/codex@0.131.0-linux-arm64': + optional: true + + '@openai/codex@0.131.0-linux-x64': + optional: true + + '@openai/codex@0.131.0-win32-arm64': + optional: true + + '@openai/codex@0.131.0-win32-x64': + optional: true + + '@opencode-ai/sdk@1.15.5': + dependencies: + cross-spawn: 7.0.6 + '@opentelemetry/api-logs@0.203.0': dependencies: '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs@0.211.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.217.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs@0.218.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -5123,6 +6537,11 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5138,6 +6557,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.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -5157,6 +6586,15 @@ 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-http@0.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5189,6 +6627,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.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -5210,6 +6660,15 @@ 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-http@0.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5248,6 +6707,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.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -5268,6 +6738,24 @@ 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-http@0.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.5.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.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-trace-otlp-http@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5370,6 +6858,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation-http@0.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + 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 @@ -5504,6 +7012,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.211.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + 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.218.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5531,6 +7057,18 @@ 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.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.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-exporter-base@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5545,6 +7083,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.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -5562,7 +7108,29 @@ 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.4.0 + + '@opentelemetry/otlp-transformer@0.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.1) + protobufjs: 8.4.0 + + '@opentelemetry/otlp-transformer@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/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.4.0 '@opentelemetry/otlp-transformer@0.218.0(@opentelemetry/api@1.9.1)': dependencies: @@ -5608,6 +7176,12 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5621,6 +7195,21 @@ 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.211.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@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-logs@0.218.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5635,6 +7224,12 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics@2.5.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5686,6 +7281,13 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -5725,28 +7327,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': + '@puppeteer/browsers@2.13.2': dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.1': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.1': {} + debug: 4.4.3 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.4 + tar-fs: 3.1.2 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true @@ -5861,24 +7455,72 @@ snapshots: transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/opentelemetry@9.47.1(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@sentry/core': 9.47.1 + + '@simple-git/args-pathspec@1.0.3': {} + + '@simple-git/argv-parser@1.1.1': + dependencies: + '@simple-git/args-pathspec': 1.0.3 + + '@sindresorhus/is@7.2.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@smithy/core@3.24.3': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.3': + dependencies: + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/core': 9.47.1 + tslib: 2.8.1 - '@simple-git/args-pathspec@1.0.3': {} + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 - '@simple-git/argv-parser@1.1.1': + '@smithy/signature-v4@5.4.3': dependencies: - '@simple-git/args-pathspec': 1.0.3 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 - '@sindresorhus/is@7.2.0': {} + '@smithy/types@4.14.2': + dependencies: + tslib: 2.8.1 - '@sindresorhus/merge-streams@4.0.0': {} + '@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': {} @@ -5897,6 +7539,8 @@ snapshots: '@tootallnate/once@3.0.1': {} + '@tootallnate/quickjs-emscripten@0.23.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -6135,10 +7779,16 @@ snapshots: readable-stream: 3.6.2 optional: true + argparse@2.0.1: {} + arrify@2.0.1: {} assertion-error@2.0.1: {} + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + ast-v8-to-istanbul@0.3.12: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -6162,10 +7812,46 @@ snapshots: transitivePeerDependencies: - debug + b4a@1.8.1: {} + balanced-match@4.0.4: {} + bare-events@2.8.3: {} + + bare-fs@4.7.1: + dependencies: + bare-events: 2.8.3 + bare-path: 3.0.0 + bare-stream: 2.13.1(bare-events@2.8.3) + bare-url: 2.4.3 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.1: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.9.1 + + bare-stream@2.13.1(bare-events@2.8.3): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.3 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.3: + dependencies: + bare-path: 3.0.0 + base64-js@1.5.1: {} + basic-ftp@5.3.1: {} + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} @@ -6194,7 +7880,9 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@5.0.5: + bowser@2.14.1: {} + + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -6206,6 +7894,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 @@ -6215,6 +7908,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: {} @@ -6262,6 +7959,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6295,6 +7999,10 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@1.1.4: {} chownr@2.0.0: @@ -6302,6 +8010,12 @@ snapshots: chownr@3.0.0: {} + chromium-bidi@14.0.0(devtools-protocol@0.0.1608973): + dependencies: + devtools-protocol: 0.0.1608973 + mitt: 3.0.1 + zod: 4.3.6 + cjs-module-lexer@1.4.3: {} cjs-module-lexer@2.2.0: {} @@ -6341,8 +8055,15 @@ snapshots: dependencies: delayed-stream: 1.0.0 + command-exists@1.2.9: {} + commander@14.0.3: {} + computesdk@4.1.1: + dependencies: + '@computesdk/cmd': 0.4.1 + daemond: 0.1.3 + console-control-strings@1.1.0: optional: true @@ -6367,6 +8088,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + daemond@0.1.3: {} + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + debug@3.2.7(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -6398,8 +8125,20 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + delayed-stream@1.0.0: {} delegates@1.0.0: @@ -6411,6 +8150,8 @@ snapshots: detect-libc@2.1.2: {} + devtools-protocol@0.0.1608973: {} + diff@9.0.0: {} dom-serializer@2.0.0: @@ -6435,6 +8176,10 @@ snapshots: dependencies: is-obj: 2.0.0 + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + dotenv@16.6.1: {} dotenv@17.4.2: {} @@ -6537,10 +8282,24 @@ snapshots: escape-html@1.0.3: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} event-target-shim@5.0.1: {} @@ -6551,6 +8310,14 @@ snapshots: dependencies: uuid: 8.3.2 + events-universal@1.0.1: + dependencies: + bare-events: 2.8.3 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -6574,6 +8341,10 @@ 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): @@ -6630,6 +8401,16 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + + 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 @@ -6647,6 +8428,18 @@ snapshots: 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: '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) @@ -6686,6 +8479,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} figures@6.1.0: @@ -6754,6 +8552,10 @@ snapshots: hasown: 2.0.3 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -6806,6 +8608,14 @@ snapshots: - encoding - supports-color + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + gcp-metadata@6.1.1(encoding@0.1.13): dependencies: gaxios: 6.7.1(encoding@0.1.13) @@ -6815,6 +8625,14 @@ snapshots: - encoding - supports-color + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + get-caller-file@2.0.5: {} get-east-asian-width@1.5.0: {} @@ -6850,6 +8668,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -6884,6 +8710,17 @@ snapshots: path-is-absolute: 1.0.1 optional: true + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + google-auth-library@9.15.1(encoding@0.1.13): dependencies: base64-js: 1.5.1 @@ -6907,7 +8744,7 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) object-hash: 3.0.0 proto3-json-serializer: 2.0.2 - protobufjs: 7.5.6 + protobufjs: 8.4.0 retry-request: 7.0.2(encoding@0.1.13) uuid: 9.0.1 transitivePeerDependencies: @@ -6916,6 +8753,8 @@ snapshots: google-logging-utils@0.0.2: {} + google-logging-utils@1.1.3: {} + googleapis-common@7.2.0(encoding@0.1.13): dependencies: extend: 3.0.2 @@ -6969,6 +8808,10 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -6982,6 +8825,10 @@ snapshots: dependencies: function-bind: 1.1.2 + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + hono@4.12.18: {} hosted-git-info@7.0.2: @@ -7034,6 +8881,13 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http2-wrapper@2.2.1: dependencies: quick-lru: 5.1.1 @@ -7084,6 +8938,13 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 + import-in-the-middle@2.0.6: + 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 + import-in-the-middle@3.0.1: dependencies: acorn: 8.16.0 @@ -7165,6 +9026,10 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@2.0.5: {} + + isbinaryfile@5.0.7: {} + isexe@2.0.0: {} isomorphic-unfetch@3.1.0(encoding@0.1.13): @@ -7174,6 +9039,10 @@ snapshots: transitivePeerDependencies: - encoding + isomorphic-ws@5.0.0(ws@8.20.1): + dependencies: + ws: 8.20.1 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -7213,6 +9082,10 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + json-bigint@1.0.0: dependencies: bignumber.js: 9.3.1 @@ -7230,12 +9103,22 @@ snapshots: json-schema-typed@8.0.2: {} + json-stable-stringify@1.3.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + jsonfile@6.2.1: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 + jsonify@0.0.1: {} + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -7351,6 +9234,8 @@ snapshots: yallist: 4.0.0 optional: true + lru-cache@7.18.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7396,6 +9281,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: {} @@ -7418,7 +9310,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimist@1.2.8: {} @@ -7468,6 +9360,8 @@ snapshots: dependencies: minipass: 7.1.3 + mitt@3.0.1: {} + mkdirp-classic@0.5.3: {} mkdirp@1.0.4: @@ -7492,6 +9386,8 @@ snapshots: negotiator@1.0.0: {} + netmask@2.1.1: {} + node-abi@3.89.0: dependencies: semver: 7.7.4 @@ -7500,12 +9396,20 @@ snapshots: node-addon-api@8.7.0: {} + node-domexception@1.0.0: {} + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-forge@1.4.0: {} node-gyp-build@4.8.4: {} @@ -7579,6 +9483,8 @@ snapshots: object-inspect@1.13.4: {} + object-keys@1.1.1: {} + obliterator@2.0.5: {} on-exit-leak-free@2.1.2: {} @@ -7602,9 +9508,9 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - openai@6.35.0(ws@8.20.0)(zod@4.3.6): + openai@6.35.0(ws@8.20.1)(zod@4.3.6): optionalDependencies: - ws: 8.20.0 + ws: 8.20.1 zod: 4.3.6 p-cancelable@4.0.1: {} @@ -7614,6 +9520,24 @@ snapshots: aggregate-error: 3.1.0 optional: true + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + package-json-from-dist@1.0.1: {} parse-json@8.3.0: @@ -7624,6 +9548,8 @@ snapshots: parse-ms@4.0.0: {} + parse-passwd@1.0.0: {} + parseley@0.12.1: dependencies: leac: 0.6.0 @@ -7633,6 +9559,8 @@ snapshots: path-exists@5.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: optional: true @@ -7741,6 +9669,8 @@ snapshots: process-warning@5.0.0: {} + progress@2.0.3: {} + promise-inflight@1.0.1: optional: true @@ -7750,23 +9680,18 @@ snapshots: retry: 0.12.0 optional: true + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + proto3-json-serializer@2.0.2: dependencies: - protobufjs: 7.5.6 + protobufjs: 8.4.0 - protobufjs@7.5.6: + protobufjs@8.4.0: dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 20.19.39 long: 5.3.2 proxy-addr@2.0.7: @@ -7774,6 +9699,21 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} pstree.remy@1.1.8: {} @@ -7789,10 +9729,29 @@ snapshots: inherits: 2.0.4 pump: 3.0.4 + puppeteer-core@24.43.1: + dependencies: + '@puppeteer/browsers': 2.13.2 + chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973) + debug: 4.4.3 + devtools-protocol: 0.0.1608973 + typed-query-selector: 2.12.2 + webdriver-bidi-protocol: 0.4.1 + ws: 8.20.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + qs@6.15.1: dependencies: side-channel: 1.1.0 + queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} quick-lru@5.1.1: {} @@ -7839,6 +9798,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + real-require@0.2.0: {} require-directory@2.1.1: {} @@ -7891,8 +9852,7 @@ snapshots: - encoding - supports-color - retry@0.12.0: - optional: true + retry@0.12.0: {} reusify@1.1.0: {} @@ -7936,6 +9896,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: @@ -7984,6 +9948,15 @@ snapshots: set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -8026,8 +9999,7 @@ snapshots: siginfo@2.0.0: {} - signal-exit@3.0.7: - optional: true + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -8069,8 +10041,7 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - smart-buffer@4.2.0: - optional: true + smart-buffer@4.2.0: {} socks-proxy-agent@6.2.1: dependencies: @@ -8081,11 +10052,18 @@ snapshots: - supports-color optional: true + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.8 + transitivePeerDependencies: + - supports-color + socks@2.8.8: dependencies: ip-address: 10.1.1 smart-buffer: 4.2.0 - optional: true sonic-boom@4.2.1: dependencies: @@ -8093,6 +10071,9 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: + optional: true + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -8132,12 +10113,28 @@ 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: {} + + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-argv@0.3.2: {} string-width@4.2.3: @@ -8179,10 +10176,14 @@ snapshots: strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 + strnum@2.3.0: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -8199,6 +10200,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + systeminformation@5.31.6: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -8206,6 +10209,18 @@ snapshots: pump: 3.0.4 tar-stream: 2.2.0 + tar-fs@3.1.2: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.0 + optionalDependencies: + bare-fs: 4.7.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -8214,6 +10229,17 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.1 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + tar@7.5.13: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -8233,12 +10259,25 @@ snapshots: - encoding - supports-color + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 glob: 10.5.0 minimatch: 10.2.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + thread-stream@4.0.0: dependencies: real-require: 0.2.0 @@ -8291,8 +10330,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsx@4.21.0: dependencies: @@ -8313,6 +10351,8 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typed-query-selector@2.12.2: {} + typescript@5.9.3: {} uint8array-extras@1.5.0: {} @@ -8347,6 +10387,10 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.1: {} + + uuid@13.0.2: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -8443,8 +10487,12 @@ snapshots: - tsx - yaml + web-streams-polyfill@3.3.3: {} + web-tree-sitter@0.25.10: {} + webdriver-bidi-protocol@0.4.1: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -8486,7 +10534,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.0: {} + ws@8.20.1: {} wsl-utils@0.1.0: dependencies: @@ -8494,6 +10542,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"],