diff --git a/eslint.config.mjs b/eslint.config.mjs index 565c4a7..92516c7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,7 @@ const eslintConfig = [ { - ignores: ['node_modules/**', 'out/**', 'build/**'], + ignores: ['node_modules/**', 'out/**', 'build/**', 'dist/**', '**/dist/**'], }, ] diff --git a/packages/open-agent-kernel/docs/superpowers/specs/2026-06-23-oak-local-runtime-sandbox-design.md b/packages/open-agent-kernel/docs/superpowers/specs/2026-06-23-oak-local-runtime-sandbox-design.md new file mode 100644 index 0000000..5525fd4 --- /dev/null +++ b/packages/open-agent-kernel/docs/superpowers/specs/2026-06-23-oak-local-runtime-sandbox-design.md @@ -0,0 +1,476 @@ +# OAK 本地 Runtime 沙箱过渡方案 + +**Status:** Draft — 待 Phase 0 实施 +**Date:** 2026-06-23 +**Scope:** `@cloudbase/open-agent-kernel` +**Related:** `AgsStatefulSandbox`、`tcb-remote-workspace` workspace snapshot、Claude Agent SDK 内置工具 + +--- + +## 1. 背景 + +### 1.1 现状 + +OAK 当前默认沙箱方案基于 **腾讯云 AGS(Agent Sandbox)** 产品: + +- 控制面:AGS OpenAPI(CreateSandboxTool / StartSandboxInstance) +- 数据面:TRW 业务镜像 HTTP Gateway(`/api/tools/*`、`/api/workspace/*`) +- 工具暴露:禁用 Claude SDK 内置工具,改注入 `mcp__sandbox__*` MCP server + +用户启用沙箱需自行配置:`CLOUDBASE_APIKEY`、`OAK_SANDBOX_IMAGE`、`OAK_SANDBOX_TOOL_ROLE_ARN`、`credentials` 等 AGS 控制面凭证。 + +### 1.2 问题 + +1. **AGS 产品化未完成**:云开发用户接入 OAK 仍需自行打通 AGS 链路,接入成本高。 +2. **TCB↔AGS 对接排期中**:产品化链路尚未上线,短期无法依赖默认 AGS 路径。 +3. **云函数等 Serverless Runtime 已可运行 OAK**:宿主进程本身具备可写目录(如 `/tmp`),具备「本地文件 + bash」的物理条件。 + +### 1.3 目标 + +在 AGS 产品化就绪前,提供 **Local Runtime 沙箱** 过渡方案: + +- 在 OAK **宿主进程**(HTTP 云函数、CloudRun 等)内直接完成文件读写与 bash +- 在配置的工作目录下,于合适时机完成 **COS 双向同步**(restore + snapshot) +- **保留** 现有 AGS 实现,通过 provider 分支切换,便于产品化后平滑迁移 + +### 1.4 非目标 + +- 不替代 TRW 全能力(PTY、preview、in-sandbox agents、mcporter 动态 MCP 等) +- 不提供容器级隔离(接受过渡期的安全风险) +- Phase 0 不要求 CloudBase MCP 在 local 模式立即可用 + +--- + +## 2. 设计原则 + +| 原则 | 说明 | +|------|------| +| **Strategy 分支,不删 AGS** | `SandboxRuntime` 协议不变;新增 `LocalRuntimeSandbox`,保留 `AgsStatefulSandbox` | +| **默认 local,显式 ags** | 新用户 `sandbox: { enabled: true }` 默认 `provider: 'local'`;AGS 用户显式 `provider: 'ags-stateful'` | +| **cwd 由调用方主导** | kernel 不硬编码 `/home/user`;推导规则透明、可覆盖 | +| **COS 格式对齐 TRW** | local 模式 snapshot 使用与 TRW 兼容的 tar.zst + key 布局,便于日后切换 backend | +| **生命周期钩子复用** | send 前 restore、send 后 snapshot 沿用现有 `create-agent.ts` 编排 | + +--- + +## 3. 架构总览 + +``` +createAgent({ sandbox: { provider: 'local' | 'ags-stateful' } }) + │ + ▼ +resolveSandboxConfig() + │ + ┌────┴────┐ + │ │ + local ags-stateful + │ │ + ▼ ▼ +LocalRuntimeSandbox AgsStatefulSandbox.acquire() + │ │ + │ └──► SandboxInstance.request() ──► TRW :9000 + │ + ├──► SDK 内置工具 (Bash/Read/Write/Edit/Glob/Grep) + │ + └──► LocalWorkspaceSyncEngine ──► COS (restore / snapshot) +``` + +### 3.1 工具面分支 + +| 模式 | 文件 / Shell | CloudBase 工具 | +|------|-------------|----------------| +| **local** | Claude SDK **内置工具** | Phase 0 默认关闭;Phase 1+ 进程内 MCP | +| **ags-stateful** | `mcp__sandbox__*`(TRW HTTP) | `mcp__cloudbase__*`(沙箱内 mcporter) | +| **none** | 无(维持现状) | 无 | + +`permissionMode: 'bypassPermissions'` + PreToolUse hook **两种模式均保留**。 + +--- + +## 4. 配置模型 + +### 4.1 SandboxConfig 扩展 + +```typescript +export interface SandboxConfig { + enabled?: boolean + + /** + * - 'local'(新默认):宿主进程本地 FS + SDK 内置工具 + * - 'ags-stateful':现有 AGS + TRW 远程沙箱 + */ + provider?: 'local' | 'ags-stateful' + + /** local 模式:工作区根目录;未设则按 deriveDefaultWorkspaceRoot 推导 */ + workspaceRoot?: string + + /** ags-stateful 专用 */ + apiKey?: string + scope?: 'session' | 'shared' + runtime?: unknown + + cloudbaseTools?: boolean + userCredentials?: SandboxUserCredentials | (() => Promise) + + workspaceSnapshot?: 'auto' | 'enabled' | 'disabled' + workspaceSnapshotTimeoutMs?: number + workspaceInitTimeoutMs?: number + + /** local 模式 COS 配置(Phase 1) */ + cos?: LocalCosConfig +} + +interface LocalCosConfig { + bucket?: string + region?: string + /** 对象前缀,默认 `/oak-workspaces/{userId}` */ + prefix?: string +} +``` + +### 4.2 resolveSandboxConfig 行为 + +```typescript +function resolveSandboxConfig(config: AgentConfig): AgentConfig['sandbox'] | undefined { + const sandbox = config.sandbox + if (!sandbox || sandbox.enabled === false) return undefined + if (sandbox.runtime) return sandbox // 显式 runtime 优先 + + const provider = sandbox.provider ?? 'local' + + if (provider === 'local') { + return { + ...sandbox, + enabled: true, + provider: 'local', + runtime: new LocalRuntimeSandbox({ workspaceRoot: sandbox.workspaceRoot }), + } + } + + if (provider === 'ags-stateful') { + // 现有逻辑:apiKey、AgsStatefulSandbox、scope 默认 shared + } + + throw new InvalidConfigError(`unsupported provider: ${provider}`) +} +``` + +### 4.3 配置示例 + +**云函数 / Serverless(过渡默认):** + +```typescript +createAgent({ + envId: 'my-env', + credentials: { secretId, secretKey }, + model: 'glm-5.1', + cwd: '/tmp/oak-workspaces/demo', // 或省略,使用自动推导 + sandbox: { + enabled: true, + // provider: 'local' // 可省略 + workspaceSnapshot: 'auto', + cloudbaseTools: false, // Phase 0 + }, +}) +``` + +**AGS 产品化路径(不变):** + +```typescript +createAgent({ + envId: 'my-env', + credentials: { secretId, secretKey }, + model: 'glm-5.1', + sandbox: { + enabled: true, + provider: 'ags-stateful', + apiKey: process.env.CLOUDBASE_APIKEY, + scope: 'shared', + workspaceSnapshot: 'auto', + }, +}) +``` + +--- + +## 5. LocalRuntimeSandbox + +### 5.1 职责 + +实现现有 `SandboxRuntime` 协议(`backend = 'local'`): + +- `acquire(ctx)`:创建/确认 workspace 目录,返回轻量 `SandboxInstance` +- `release()`:可选触发最终 snapshot;**不删除**本地目录(COS 为 source of truth) + +local 模式 **不暴露 HTTP 数据面**。`SandboxInstance.request()` 若被调用应抛明确错误;snapshot 走 `LocalWorkspaceSyncEngine`,不经过 TRW。 + +### 5.2 acquire 流程 + +``` +1. resolveWorkspacePath(ctx) + → config.cwd ?? sandbox.workspaceRoot ?? deriveDefaultWorkspaceRoot(ctx) + +2. fs.mkdir(root, { recursive: true }) + +3. probeWritable(root) — 不可写则 ConfigError(serverless 只读 FS 早 fail) + +4. return SandboxInstance { id: `local:${conversationId}`, release } +``` + +--- + +## 6. cwd 与工作目录 + +### 6.1 为何不用 `/home/user` 作为通用默认 + +| 环境 | 说明 | +|------|------| +| TRW / AGS 容器 | `/home/user` 是镜像约定 ✅ | +| SCF 云函数 | 常见可写路径为 `/tmp`,非 `/home/user` | +| CloudRun | 取决于镜像,不一定存在 `/home/user` | + +**OAK 不应假设容器布局**;由调用方或平台 env 指定可写路径。 + +### 6.2 推导优先级 + +``` +effectiveWorkspaceRoot = + config.cwd // 1. AgentConfig.cwd(同时作为 SDK cwd) + ?? config.sandbox?.workspaceRoot // 2. sandbox 级覆盖 + ?? deriveDefaultWorkspaceRoot(ctx) // 3. 平台推导 + +deriveDefaultWorkspaceRoot(ctx): + base = process.env.OAK_WORKSPACE_ROOT ?? path.join(os.tmpdir(), 'oak-workspaces') + return path.join(base, ctx.envId, ctx.userId, ctx.conversationId) +``` + +### 6.3 与 Claude SDK 对齐 + +local 模式下 **`AgentConfig.cwd` 必须等于 workspace root**(或与 `sandbox.workspaceRoot` 显式一致)。SDK 内置工具以 `cwd` 为工作目录;COS 同步同一目录树。 + +若 `config.cwd` 与 `sandbox.workspaceRoot` 冲突 → `ConfigError`。 + +--- + +## 7. SDK 内置工具分支 + +### 7.1 当前行为 + +`agent-builder.ts` 禁用全部 SDK 内置工具,仅保留 `Skill`(若启用 skills): + +```typescript +tools: config.skills?.enabled !== undefined ? ['Skill'] : [], +``` + +远程沙箱能力通过 `createSandboxMcpServer(sandboxInstance)` 注入。 + +### 7.2 改造后 + +```typescript +function resolveSdkTools(config, sandboxMode: 'local' | 'remote' | 'none') { + if (sandboxMode === 'local') { + const tools = ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep'] + if (config.skills?.enabled !== undefined) tools.push('Skill') + return tools + } + if (sandboxMode === 'remote') { + return config.skills?.enabled !== undefined ? ['Skill'] : [] + } + return config.skills?.enabled !== undefined ? ['Skill'] : [] +} +``` + +| sandboxMode | 注入 sandbox MCP | SDK tools | +|-------------|------------------|-----------| +| `local` | 否 | Bash, Read, Write, Edit, Glob, Grep (+ Skill) | +| `remote` | 是 (`mcp__sandbox__*`) | Skill only 或 [] | +| `none` | 否 | 维持现状 | + +--- + +## 8. COS 双向同步(Local 模式) + +### 8.1 与 AGS 模式对比 + +| | AGS + TRW | Local | +|--|-----------|-------| +| Restore | `POST /api/workspace/init`(TRW 内 restoreFromCos) | OAK 进程内 `restoreFromCos()` | +| Snapshot | `POST /api/workspace/snapshot` | OAK 进程内 `snapshotToCos()` | +| 格式 | tar.zst | **兼容同一格式** | + +### 8.2 模块结构(Phase 1 新增) + +``` +src/sandbox/local-workspace-sync/ +├── cos-client.ts # CloudBase / COS SDK 上传下载 +├── snapshot-pack.ts # tar + zstd(参考 TRW tests/helpers) +├── restore.ts +├── snapshot.ts +├── excludes.ts # node_modules、.git 等(对齐 TRW snapshot-excludes) +└── engine.ts # LocalWorkspaceSyncEngine +``` + +### 8.3 COS Key 布局(与 TRW 对齐) + +``` +cos://{bucket}/oak-workspaces/{userId}/.snapshot-{timestamp}.tar.zst +cos://{bucket}/oak-workspaces/{userId}/.snapshot-latest.json +``` + +### 8.4 触发时机 + +沿用 `create-agent.ts` / `runClaudeQuery` 现有钩子: + +| 时机 | 动作 | 失败策略 | +|------|------|----------| +| 首次 `send()` 前 | `restoreFromCos(workspaceRoot)` | **致命**(与 Spec B 一致) | +| 每次 `send()` finally | `snapshotToCos(workspaceRoot)` | **非致命** warning | +| `session.abort()` | 可选最终 snapshot | best-effort | +| 写操作后 debounced sync | Phase 2 可选 | 非致命 | + +### 8.5 resolveSnapshotMode 扩展 + +```typescript +const supportsSnapshot = backend === 'ags-stateful' || backend === 'local' + +// ags-stateful:仍要求 scope === 'shared'(现有约束) +// local:不要求 shared scope;用 conversationId 子目录 + COS userId prefix 隔离 +``` + +--- + +## 9. CloudBase MCP(local 模式) + +当前 `cloudbase-mcp.ts` 依赖 `SandboxInstance.request` + 沙箱内 mcporter bash,**无法在 local 模式直接复用**。 + +| Phase | 方案 | +|-------|------| +| **P0** | local 默认 `cloudbaseTools: false` | +| **P1** | `createCloudBaseMcpServerInProcess()`:进程内 mcporter 或 TCB Node SDK 封装 | +| **P2** | TCB 产品化 MCP Gateway,统一 local / remote | + +--- + +## 10. Session 编排改造 + +### 10.1 抽象 sandboxMode + +```typescript +type SandboxMode = 'none' | 'local' | 'remote' + +async function prepareSandboxSession(...) { + if (mode === 'local') { + const workspaceRoot = await localRuntime.prepare(ctx) + if (syncEngine) await syncEngine.restore(workspaceRoot) + return { mode: 'local', workspaceRoot } + } + if (mode === 'remote') { + const sandbox = await agsRuntime.acquire(ctx) + if (syncEngine) await syncEngine.bootstrap(sandbox) + return { mode: 'remote', sandboxInstance: sandbox } + } +} +``` + +### 10.2 buildClaudeQueryOptions 入参 + +```typescript +buildClaudeQueryOptions(config, { + sandboxMode: 'local' | 'remote' | 'none', + sandboxInstance?: SandboxInstance, // remote only + workspaceRoot?: string, // local only + ... +}) +``` + +--- + +## 11. 安全与风险 + +local 模式为 **临时过渡**,必须在文档与运行时 warning 中明确: + +| 风险 | 说明 | 缓解 | +|------|------|------| +| 无容器隔离 | agent bash 与 OAK 同进程/同 VM | 仅 trusted 场景;生产加 network policy | +| 路径逃逸 | 内置工具可能访问 cwd 外 | Phase 2:workspaceRoot path guard | +| 多租户 | 同实例多 user | 必须 userId + conversationId 子目录 + COS prefix | +| 凭证泄露 | bash 可读进程 env | 延续 AGENTS.md 日志静态字符串规则 | +| 并发 snapshot | 云函数多并发同 conversation | Phase 2:COS 乐观锁 / 版本号 | + +**启动 warning(每 session 一次):** + +``` +[oak][sandbox] provider=local is transitional — no container isolation. +Use provider='ags-stateful' when TCB sandbox product is available. +``` + +--- + +## 12. 实施分期 + +### Phase 0 — 最小可用 + +- [ ] `LocalRuntimeSandbox` + `provider: 'local'` 默认 +- [ ] `resolveSdkTools`:local 启用 SDK builtin +- [ ] `workspaceRoot` / `cwd` 推导与可写性检查 +- [ ] `workspaceSnapshot: 'disabled'` 为 local 默认(先验证工具链) +- [ ] 示例 `examples/08-local-sandbox.ts` +- [ ] AGS 路径零行为变化(显式 `provider: 'ags-stateful'`) + +### Phase 1 — COS 持久化 + +- [ ] `local-workspace-sync` 模块(restore + snapshot) +- [ ] `resolveSnapshotMode` 支持 `backend = 'local'` +- [ ] send 前 restore / send 后 snapshot +- [ ] E2E:写文件 → 新 session 读回 + +### Phase 2 — 体验与安全 + +- [ ] workspaceRoot path guard +- [ ] debounced snapshot、并发锁 +- [ ] CloudBase in-process MCP +- [ ] OpenVibeCoding server 默认切 local provider + +### Phase 3 — 产品化切换 + +- [ ] TCB↔AGS 上线后:`provider` 默认改 `ags-stateful` 或 `auto`(平台能力探测) +- [ ] local 标记 `@deprecated`,保留供本地 dev / 单测 + +--- + +## 13. 文件改动清单(预估) + +| 文件 | 改动 | +|------|------| +| `src/public/types.ts` | `provider`、`workspaceRoot`、`cos` | +| `src/public/create-agent.ts` | `resolveSandboxConfig` 默认 local;session 编排 | +| `src/sandbox/local-runtime-sandbox.ts` | **新增** | +| `src/sandbox/local-workspace-sync/*` | **新增**(Phase 1) | +| `src/runtime/agent-builder.ts` | `resolveSdkTools`、snapshot mode、MCP 分支 | +| `src/sandbox/workspace-snapshot/*` | 抽象 sync backend(HTTP vs local) | +| `src/sandbox/ags-stateful-sandbox.ts` | 保留,无删改 | +| `examples/08-local-sandbox.ts` | **新增** | +| `README.md` | 过渡方案说明 | + +--- + +## 14. 决策记录 + +| 决策 | 选择 | 理由 | +|------|------|------| +| 是否删除 AGS 实现 | **否** | 产品化排期中,避免重复建设 | +| 默认 provider | **local** | 降低云开发用户接入门槛 | +| cwd 默认值 | **不硬编码 /home/user** | Serverless 环境差异大 | +| local 工具来源 | **SDK 内置** | 无需 TRW HTTP,与 remote MCP 对称分支 | +| COS 格式 | **对齐 TRW tar.zst** | 便于 backend 切换与数据迁移 | +| CloudBase MCP P0 | **默认关闭** | 依赖沙箱 bash,需独立 Phase | + +--- + +## 15. 参考 + +- `packages/open-agent-kernel/src/sandbox/ags-stateful-sandbox.ts` — AGS 控制面/数据面 +- `packages/open-agent-kernel/docs/superpowers/specs/2026-06-08-oak-workspace-snapshot.md` — Spec B 快照 +- `tcb-remote-workspace/src/cos-sync.ts` — TRW COS rsync / tar.zst 实现 +- `tcb-remote-workspace/src/routes/api.ts` — TRW HTTP 路由 +- `packages/open-agent-kernel/src/runtime/agent-builder.ts` — SDK tools / cwd 逻辑 diff --git a/packages/open-agent-kernel/examples/08-sandbox.ts b/packages/open-agent-kernel/examples/08-sandbox.ts index d5020b8..1465b3a 100644 --- a/packages/open-agent-kernel/examples/08-sandbox.ts +++ b/packages/open-agent-kernel/examples/08-sandbox.ts @@ -1,8 +1,8 @@ /** - * Example 08: AGS Stateful Sandbox(腾讯云 Agent Sandbox 产品) + * Example 08: Local Runtime Sandbox(Phase 0 默认沙箱) * - * 演示 agent 在真实远程沙箱里跑文件系统 + shell: - * 1. 让 agent 在沙箱里写一个 README.md + * 演示 agent 在宿主进程本地 workspace 里跑文件系统 + shell: + * 1. 让 agent 在本地 workspace 写一个 README.md * 2. 让 agent 跑 `ls` 列目录 * 3. 让 agent 读回 README.md 验证 * @@ -14,9 +14,11 @@ * pnpm dlx tsx packages/open-agent-kernel/examples/08-sandbox.ts * * 注意: - * - 第一次运行会触发 CreateSandboxTool(~30s)+ StartSandboxInstance(~30-60s) - * - 之后同一 envId 会复用 ToolId(内存 cache),但每个 session 仍会启新实例 + * - local 是过渡方案:无容器级隔离,适合可信 serverless / CloudRun 场景 + * - 如需使用 AGS 远程沙箱,请显式配置 sandbox.provider = 'ags-stateful' */ +import * as os from 'node:os' +import * as path from 'node:path' import { getEnvId, getModel, getPlatformCredentials } from './_shared/env.js' import { createAgent } from '@cloudbase/open-agent-kernel' @@ -24,19 +26,25 @@ import { createAgent } from '@cloudbase/open-agent-kernel' async function main(): Promise { const envId = getEnvId() const credentials = getPlatformCredentials() + const workspaceRoot = path.join(os.tmpdir(), 'oak-example-08-local') const agent = createAgent({ envId, credentials, model: getModel(), systemPrompt: - 'You are a helpful coding assistant working inside a sandbox. ' + - 'You have access to bash / read / write tools (mcp__sandbox__*). ' + + 'You are a helpful coding assistant working inside a local runtime sandbox. ' + + 'You have access to Bash / Read / Write / Edit / Glob / Grep tools. ' + 'Always use the tools to interact with the filesystem—never fabricate output. ' + 'Reply concisely in Chinese.', + cwd: workspaceRoot, sandbox: { enabled: true, - // 默认使用 AgsStatefulSandbox + scope: 'shared' + // provider: 'local' 是 Phase 0 默认值;这里显式写出便于阅读。 + provider: 'local', + workspaceRoot, + cloudbaseTools: false, + workspaceSnapshot: 'disabled', }, }) @@ -44,9 +52,9 @@ async function main(): Promise { const prompt = '请完成以下任务:\n' + - '1. 在工作目录用 write 工具创建一个 README.md,内容是 "# Hello from open-agent-kernel sandbox"\n' + - '2. 用 bash 工具跑 `ls -la` 看下当前目录\n' + - '3. 用 read 工具读 README.md 的内容并展示给我\n' + + '1. 在工作目录创建一个 README.md,内容是 "# Hello from open-agent-kernel local sandbox"\n' + + '2. 跑 `ls -la` 看下当前目录\n' + + '3. 读 README.md 的内容并展示给我\n' + '完成后告诉我结果。' console.log('User:', prompt, '\n') @@ -65,8 +73,8 @@ async function main(): Promise { } } - console.log('\n\n--- Cleaning up sandbox ---') - await session.abort() // 触发 PauseSandboxInstance + console.log('\n\n--- Cleaning up local sandbox session ---') + await session.abort() console.log('--- Done ---') } diff --git a/packages/open-agent-kernel/examples/20-local-workspace-sync.ts b/packages/open-agent-kernel/examples/20-local-workspace-sync.ts new file mode 100644 index 0000000..be64dc8 --- /dev/null +++ b/packages/open-agent-kernel/examples/20-local-workspace-sync.ts @@ -0,0 +1,58 @@ +/** + * Example 20: Local Runtime Workspace Sync(Phase 1) + * + * 不依赖真实 COS 凭证。用 FileSystemLocalWorkspaceStore 模拟远端持久化层, + * 验证 local runtime sandbox 的 send 前 restore / send 后 snapshot 同步契约: + * 1. workspace A 写入文件并 snapshot + * 2. workspace B bootstrap restore + * 3. 从 workspace B 读回文件 + * + * 运行: + * pnpm dlx tsx packages/open-agent-kernel/examples/20-local-workspace-sync.ts + */ +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { FileSystemLocalWorkspaceStore, LocalRuntimeSandbox } from '../src/sandbox/index.js' + +async function main(): Promise { + const base = await fs.mkdtemp(path.join(os.tmpdir(), 'oak-local-sync-example-')) + const storeRoot = path.join(base, 'remote-store') + const workspaceA = path.join(base, 'workspace-a') + const workspaceB = path.join(base, 'workspace-b') + + const store = new FileSystemLocalWorkspaceStore({ root: storeRoot }) + const runtimeA = new LocalRuntimeSandbox({ workspaceRoot: workspaceA, workspaceSyncStore: store }) + const runtimeB = new LocalRuntimeSandbox({ workspaceRoot: workspaceB, workspaceSyncStore: store }) + const ctx = { envId: 'example-env', userId: 'example-user', conversationId: 'example-conv' } + + const instanceA = await runtimeA.acquire(ctx) + const engineA = requireEngine(runtimeA.createWorkspaceSyncEngine(ctx)) + await engineA.bootstrap(instanceA, { credentials: {} }) + await fs.writeFile(path.join(instanceA.workspaceRoot!, 'README.md'), '# persisted from workspace A\n', 'utf8') + await engineA.snapshot(instanceA) + + const instanceB = await runtimeB.acquire(ctx) + const engineB = requireEngine(runtimeB.createWorkspaceSyncEngine(ctx)) + const status = await engineB.bootstrap(instanceB, { credentials: {} }) + const restored = await fs.readFile(path.join(instanceB.workspaceRoot!, 'README.md'), 'utf8') + + if (!restored.includes('persisted from workspace A')) { + throw new Error('restored workspace did not contain expected README.md content') + } + + console.log('Local workspace sync example passed.') + console.log(status?.restored === 'full' ? 'Restore status check passed.' : 'Restore status check skipped.') +} + +function requireEngine( + engine: ReturnType, +): NonNullable> { + if (!engine) throw new Error('LocalRuntimeSandbox did not create a workspace sync engine') + return engine +} + +main().catch((err) => { + console.error('[fatal]', err instanceof Error ? err.message : String(err)) + process.exit(1) +}) diff --git a/packages/open-agent-kernel/examples/21-local-cloudbase-mcp.ts b/packages/open-agent-kernel/examples/21-local-cloudbase-mcp.ts new file mode 100644 index 0000000..70f5224 --- /dev/null +++ b/packages/open-agent-kernel/examples/21-local-cloudbase-mcp.ts @@ -0,0 +1,42 @@ +/** + * Example 21: Local Runtime CloudBase MCP (in-process) + * + * This validates the Phase 2 local CloudBase MCP path without AGS/TRW: + * - import @cloudbase/cloudbase-mcp in the OAK process + * - connect with in-memory MCP transport + * - expose all discovered CloudBase tools as an SDK MCP server + * + * It only checks tool discovery. It does not call real CloudBase write tools. + * + * Run: + * pnpm dlx tsx packages/open-agent-kernel/examples/21-local-cloudbase-mcp.ts + */ +import { getEnvId, getPlatformCredentials } from './_shared/env.js' + +import { createCloudBaseMcpServerInProcess } from '@cloudbase/open-agent-kernel' + +async function main(): Promise { + const envId = getEnvId() + const credentials = getPlatformCredentials() + + const bundle = await createCloudBaseMcpServerInProcess({ + workspaceFolderPaths: process.cwd(), + getCredentials: async () => ({ + envId, + secretId: credentials.secretId, + secretKey: credentials.secretKey, + sessionToken: credentials.sessionToken, + }), + }) + + if (bundle.toolCount <= 0) { + throw new Error(`Expected CloudBase MCP tools, got degradedReason=${bundle.degradedReason ?? ''}`) + } + + console.log(`CloudBase MCP in-process tools discovered: ${bundle.toolCount}`) +} + +main().catch((err) => { + console.error('[fatal]', err) + process.exit(1) +}) diff --git a/packages/open-agent-kernel/package.json b/packages/open-agent-kernel/package.json index 19a769c..ae411ae 100644 --- a/packages/open-agent-kernel/package.json +++ b/packages/open-agent-kernel/package.json @@ -30,9 +30,19 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.146", "@anthropic-ai/sdk": "^0.93.0", + "@cloudbase/cals": "^1.2.25", + "@cloudbase/cloudbase-mcp": "^2.21.1", "@cloudbase/manager-node": "^4.10.6", "@cloudbase/node-sdk": "^3.18.1", + "@cloudbase/toolbox": "^0.7.19", "@modelcontextprotocol/sdk": "^1.29.0", + "adm-zip": "^0.5.17", + "express": "^5.2.1", + "lockfile": "^1.0.4", + "open": "^11.0.0", + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0", + "ws": "^8.21.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/packages/open-agent-kernel/src/index.ts b/packages/open-agent-kernel/src/index.ts index 6c8c290..fbad802 100644 --- a/packages/open-agent-kernel/src/index.ts +++ b/packages/open-agent-kernel/src/index.ts @@ -10,6 +10,30 @@ // 公共 API:唯一的入口点 export { createAgent } from './public/create-agent.js' +export { AcpStreamAdapter } from './adapters/index.js' + +export type { AcpStreamAdapterOptions, StreamAdapter, StreamAdapterContext } from './adapters/index.js' +export type { + AcpSessionUpdate, + AcpTextBlock, + AgentMessageChunkUpdate, + AgentThoughtChunkUpdate, + ToolCallUpdate, + ToolCallStatusUpdate, + AvailableCommandsUpdate, + LogUpdate, + TaskProgressUpdate, + FileChangeUpdate, + ThinkingUpdate, + AskUserUpdate, + ToolConfirmUpdate, + ArtifactUpdate, + HistoryMessage, + HistoryMessagePart, + HistoryPageUpdate, + AgentPhaseName, + AgentPhaseUpdate, +} from './acp/index.js' // 公共类型:完整对外契约 export type { @@ -21,7 +45,6 @@ export type { SessionSummary, // 输入 / 事件 SessionInput, - SessionEvent, MessageRecord, MessagePart, AttachmentInput, @@ -92,13 +115,25 @@ export { type DeleteUserMemoryFilesOptions, } from './user-memory/index.js' -// Sandbox:可选用于让 agent 在远程容器里跑文件系统/shell(PR #6A) +// Sandbox:可选用于让 agent 通过 local/AGS runtime 跑文件系统/shell export { AgsStatefulSandbox, type AgsStatefulSandboxOptions, + LocalRuntimeSandbox, + type LocalRuntimeSandboxOptions, + LocalWorkspaceSyncEngine, + FileSystemLocalWorkspaceStore, + CloudBaseCosLocalWorkspaceStore, + type LocalWorkspaceSyncContext, + type LocalWorkspaceSyncStore, + type LocalWorkspaceSyncEngineOptions, + type FileSystemLocalWorkspaceStoreOptions, + type CloudBaseCosLocalWorkspaceStoreOptions, type SandboxRuntime, type SandboxInstance, type SandboxAcquireContext, + createCloudBaseMcpServerInProcess, + type CreateCloudBaseMcpInProcessOptions, } from './sandbox/index.js' // Permissions / HITL(PR #7.0 + PR #7.1) diff --git a/packages/open-agent-kernel/src/public/__tests__/create-agent-sandbox-default.test.ts b/packages/open-agent-kernel/src/public/__tests__/create-agent-sandbox-default.test.ts index 71c5dd9..e87fc25 100644 --- a/packages/open-agent-kernel/src/public/__tests__/create-agent-sandbox-default.test.ts +++ b/packages/open-agent-kernel/src/public/__tests__/create-agent-sandbox-default.test.ts @@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ agsStatefulSandbox: vi.fn(), + localRuntimeSandbox: vi.fn(), + cloudBaseCosLocalWorkspaceStore: vi.fn(), })) vi.mock('../../sandbox/index.js', () => { @@ -16,9 +18,27 @@ vi.mock('../../sandbox/index.js', () => { throw new Error('not used in this test') } } + class MockLocalRuntimeSandbox { + readonly backend = 'local' + + constructor(opts?: unknown) { + mocks.localRuntimeSandbox(opts) + } + + async acquire(): Promise { + throw new Error('not used in this test') + } + } + class MockCloudBaseCosLocalWorkspaceStore { + constructor(opts?: unknown) { + mocks.cloudBaseCosLocalWorkspaceStore(opts) + } + } return { AgsStatefulSandbox: MockAgsStatefulSandbox, + LocalRuntimeSandbox: MockLocalRuntimeSandbox, + CloudBaseCosLocalWorkspaceStore: MockCloudBaseCosLocalWorkspaceStore, } }) @@ -27,24 +47,55 @@ const { createAgent } = await import('../create-agent.js') describe('createAgent — default sandbox runtime', () => { beforeEach(() => { mocks.agsStatefulSandbox.mockClear() + mocks.localRuntimeSandbox.mockClear() + mocks.cloudBaseCosLocalWorkspaceStore.mockClear() delete process.env.CLOUDBASE_APIKEY delete process.env.OAK_SANDBOX_API_KEY }) - it('creates default AgsStatefulSandbox when sandbox is enabled', () => { + it('creates default LocalRuntimeSandbox when sandbox is enabled', () => { + createAgent({ + envId: 'env-test', + model: 'glm-5.1', + sandbox: { + enabled: true, + }, + }) + + expect(mocks.localRuntimeSandbox).toHaveBeenCalledWith({ cwd: undefined, workspaceRoot: undefined }) + expect(mocks.agsStatefulSandbox).not.toHaveBeenCalled() + }) + + it('passes cwd and workspaceRoot to LocalRuntimeSandbox', () => { + createAgent({ + envId: 'env-test', + model: 'glm-5.1', + cwd: '/tmp/oak-local', + sandbox: { + enabled: true, + workspaceRoot: '/tmp/oak-local', + }, + }) + + expect(mocks.localRuntimeSandbox).toHaveBeenCalledWith({ cwd: '/tmp/oak-local', workspaceRoot: '/tmp/oak-local' }) + }) + + it('creates AgsStatefulSandbox when provider is ags-stateful', () => { createAgent({ envId: 'env-test', model: 'glm-5.1', sandbox: { enabled: true, + provider: 'ags-stateful', apiKey: 'sandbox-api-key', }, }) expect(mocks.agsStatefulSandbox).toHaveBeenCalledWith({ apiKey: 'sandbox-api-key' }) + expect(mocks.localRuntimeSandbox).not.toHaveBeenCalled() }) - it('reads CLOUDBASE_APIKEY for the default sandbox runtime', () => { + it('reads CLOUDBASE_APIKEY for the ags-stateful provider', () => { process.env.CLOUDBASE_APIKEY = 'env-sandbox-api-key' createAgent({ @@ -52,24 +103,62 @@ describe('createAgent — default sandbox runtime', () => { model: 'glm-5.1', sandbox: { enabled: true, + provider: 'ags-stateful', }, }) expect(mocks.agsStatefulSandbox).toHaveBeenCalledWith({ apiKey: 'env-sandbox-api-key' }) }) - it('requires an api key when default sandbox runtime is enabled', () => { + it('requires an api key when ags-stateful provider is enabled', () => { expect(() => createAgent({ envId: 'env-test', model: 'glm-5.1', sandbox: { enabled: true, + provider: 'ags-stateful', }, }), ).toThrow(/sandbox\.apiKey/) }) + it('allows local mode to explicitly enable cloudbaseTools', () => { + expect(() => + createAgent({ + envId: 'env-test', + model: 'glm-5.1', + sandbox: { + enabled: true, + cloudbaseTools: true, + }, + }), + ).not.toThrow() + }) + + it('configures a CloudBase workspace sync store when local workspaceSnapshot is enabled', () => { + createAgent({ + envId: 'env-test', + model: 'glm-5.1', + credentials: { envId: 'env-test', secretId: 'test-id', secretKey: 'test-key' }, + sandbox: { + enabled: true, + workspaceSnapshot: 'enabled', + }, + }) + + expect(mocks.cloudBaseCosLocalWorkspaceStore).toHaveBeenCalledWith({ + credentials: { envId: 'env-test', secretId: 'test-id', secretKey: 'test-key' }, + }) + expect(mocks.localRuntimeSandbox).toHaveBeenCalledWith( + expect.objectContaining({ + cwd: undefined, + workspaceRoot: undefined, + workspaceSyncStore: expect.any(Object), + }), + ) + }) + it('keeps custom sandbox runtime untouched', () => { const runtime = { backend: 'custom', diff --git a/packages/open-agent-kernel/src/public/create-agent.ts b/packages/open-agent-kernel/src/public/create-agent.ts index 6484b2f..4b7f8b6 100644 --- a/packages/open-agent-kernel/src/public/create-agent.ts +++ b/packages/open-agent-kernel/src/public/create-agent.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto' import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk' import type { McpServerConfig as SdkMcpServerConfig } from '@anthropic-ai/claude-agent-sdk' -import { InvalidConfigError, ResourceError } from '../internal/errors.js' +import { ConfigError, InvalidConfigError, ResourceError } from '../internal/errors.js' import { createHookLocalState, InMemoryAskUserStore, @@ -16,10 +16,11 @@ import { import { buildClaudeQueryOptions } from '../runtime/agent-builder.js' import { createTranslatorState, translateSdkMessage } from '../runtime/event-translator.js' import { buildPromptAsync } from '../runtime/prompt-builder.js' +import { createCloudBaseMcpServerInProcess } from '../sandbox/cloudbase-mcp-inprocess.js' import { createCloudBaseMcpServer, type CloudBaseUserCredentials } from '../sandbox/cloudbase-mcp.js' -import { AgsStatefulSandbox } from '../sandbox/index.js' +import { AgsStatefulSandbox, CloudBaseCosLocalWorkspaceStore, LocalRuntimeSandbox } from '../sandbox/index.js' import type { SandboxInstance, SandboxRuntime } from '../sandbox/types.js' -import type { WorkspaceSnapshotEngine } from '../sandbox/workspace-snapshot/index.js' +import type { WorkspaceSnapshotRuntime } from '../sandbox/workspace-snapshot/index.js' import { CloudBaseDbDriver, CloudBaseSessionStore } from '../session-store/index.js' import { CloudBaseStorage } from '../storage/cloudbase-storage.js' import type { StorageProvider } from '../storage/types.js' @@ -148,17 +149,43 @@ function resolveSandboxConfig(config: AgentConfig): AgentConfig['sandbox'] { const sandbox = config.sandbox if (!sandbox || sandbox.enabled === false) return undefined - if (sandbox.runtime) return sandbox + if (sandbox.runtime) { + const runtime = sandbox.runtime as SandboxRuntime + return { + ...sandbox, + enabled: true, + ...(!sandbox.provider && runtime.backend === 'local' ? { provider: 'local' as const } : {}), + } + } - const provider = sandbox.provider ?? 'ags-stateful' - if (provider !== 'ags-stateful') { + const provider = sandbox.provider ?? 'local' + if (provider !== 'local' && provider !== 'ags-stateful') { throw new InvalidConfigError( `AgentConfig.sandbox.provider="${provider}" is not supported yet. ` + - 'The built-in sandbox currently supports provider="ags-stateful". ' + + 'The built-in sandbox currently supports provider="local" and provider="ags-stateful". ' + 'Pass a custom SandboxRuntime via AgentConfig.sandbox.runtime for advanced scenarios.', ) } + if (provider === 'local') { + const credentials = resolvePlatformCredentials(config) + return { + ...sandbox, + enabled: true, + provider, + workspaceSnapshot: sandbox.workspaceSnapshot ?? 'disabled', + runtime: new LocalRuntimeSandbox({ + cwd: config.cwd, + workspaceRoot: sandbox.workspaceRoot, + ...(sandbox.workspaceSnapshot === 'enabled' && { + workspaceSyncStore: new CloudBaseCosLocalWorkspaceStore({ + credentials: credentials ? { ...credentials, envId: credentials.envId ?? config.envId } : undefined, + }), + }), + }), + } + } + const apiKey = sandbox.apiKey ?? process.env.CLOUDBASE_APIKEY ?? process.env.OAK_SANDBOX_API_KEY if (!apiKey) { throw new InvalidConfigError( @@ -327,7 +354,7 @@ function createSession(deps: SessionDeps): Session { // Spec B(Task 8):workspace snapshot engine 由 buildClaudeQueryOptions 在 // 第一次 send 时构造并通过本闭包变量记录。bootstrap 仅执行一次(首次 acquire 之后)。 // 注意:engine 本身是无状态构造,跨 send 持有同一个实例没有副作用。 - let sessionSnapshotEngine: WorkspaceSnapshotEngine | undefined + let sessionSnapshotEngine: WorkspaceSnapshotRuntime | undefined let snapshotBootstrapped = false let snapshotBootstrapPromise: Promise | undefined @@ -382,10 +409,17 @@ function createSession(deps: SessionDeps): Session { if (!cloudbaseMcpPromise) { cloudbaseMcpPromise = (async (): Promise => { try { - const bundle = await createCloudBaseMcpServer({ - sandbox, - getCredentials: () => resolveUserCredentials(config), - }) + const getCredentials = () => resolveUserCredentials(config) + const bundle = + resolveSandboxMode(sandbox) === 'local' + ? await createCloudBaseMcpServerInProcess({ + getCredentials, + workspaceFolderPaths: sandbox.workspaceRoot, + }) + : await createCloudBaseMcpServer({ + sandbox, + getCredentials, + }) if (process.env.OAK_DEBUG === '1') { // eslint-disable-next-line no-console console.error( @@ -420,7 +454,7 @@ function createSession(deps: SessionDeps): Session { * 由 runClaudeQuery 的 catch 块翻译为 'error' 事件 + session_idle('error')。 * 这是 spec §6.2"restore failed → 视为致命"行为。 */ - async function ensureSnapshotBootstrap(engine: WorkspaceSnapshotEngine, sandbox: SandboxInstance): Promise { + async function ensureSnapshotBootstrap(engine: WorkspaceSnapshotRuntime, sandbox: SandboxInstance): Promise { if (snapshotBootstrapped) return if (!snapshotBootstrapPromise) { snapshotBootstrapPromise = (async () => { @@ -444,7 +478,7 @@ function createSession(deps: SessionDeps): Session { } } - function onSnapshotEngine(engine: WorkspaceSnapshotEngine | undefined): void { + function onSnapshotEngine(engine: WorkspaceSnapshotRuntime | undefined): void { if (engine && !sessionSnapshotEngine) { sessionSnapshotEngine = engine } @@ -965,9 +999,9 @@ interface RunClaudeQueryArgs { ensureSandbox: () => Promise ensureCloudbaseMcp: (sandbox: SandboxInstance) => Promise /** Spec B(Task 8):首次 send 时执行 snapshot bootstrap(restore)*/ - ensureSnapshotBootstrap: (engine: WorkspaceSnapshotEngine, sandbox: SandboxInstance) => Promise + ensureSnapshotBootstrap: (engine: WorkspaceSnapshotRuntime, sandbox: SandboxInstance) => Promise /** Spec B(Task 8):把 buildClaudeQueryOptions 拿到的 engine 上抛给 session 闭包 */ - onSnapshotEngine: (engine: WorkspaceSnapshotEngine | undefined) => void + onSnapshotEngine: (engine: WorkspaceSnapshotRuntime | undefined) => void permissionStore?: PermissionStore /** PR #7.1: names of user-defined client-side tools (config.tools[].name set). */ clientToolNames?: ReadonlySet @@ -1019,6 +1053,8 @@ async function* runClaudeQuery(args: RunClaudeQueryArgs): AsyncGenerator Promise ensureCloudbaseMcp: (sandbox: SandboxInstance) => Promise - ensureSnapshotBootstrap: (engine: WorkspaceSnapshotEngine, sandbox: SandboxInstance) => Promise - onSnapshotEngine: (engine: WorkspaceSnapshotEngine | undefined) => void + ensureSnapshotBootstrap: (engine: WorkspaceSnapshotRuntime, sandbox: SandboxInstance) => Promise + onSnapshotEngine: (engine: WorkspaceSnapshotRuntime | undefined) => void permissionStore?: PermissionStore clientToolNames?: ReadonlySet clientToolStore?: ClientToolResultStore @@ -1249,8 +1285,8 @@ interface RunClientToolResumeArgs { abortController: AbortController ensureSandbox: () => Promise ensureCloudbaseMcp: (sandbox: SandboxInstance) => Promise - ensureSnapshotBootstrap: (engine: WorkspaceSnapshotEngine, sandbox: SandboxInstance) => Promise - onSnapshotEngine: (engine: WorkspaceSnapshotEngine | undefined) => void + ensureSnapshotBootstrap: (engine: WorkspaceSnapshotRuntime, sandbox: SandboxInstance) => Promise + onSnapshotEngine: (engine: WorkspaceSnapshotRuntime | undefined) => void permissionStore?: PermissionStore clientToolNames: ReadonlySet clientToolStore?: ClientToolResultStore @@ -1355,8 +1391,8 @@ interface RunAskUserResumeArgs { abortController: AbortController ensureSandbox: () => Promise ensureCloudbaseMcp: (sandbox: SandboxInstance) => Promise - ensureSnapshotBootstrap: (engine: WorkspaceSnapshotEngine, sandbox: SandboxInstance) => Promise - onSnapshotEngine: (engine: WorkspaceSnapshotEngine | undefined) => void + ensureSnapshotBootstrap: (engine: WorkspaceSnapshotRuntime, sandbox: SandboxInstance) => Promise + onSnapshotEngine: (engine: WorkspaceSnapshotRuntime | undefined) => void permissionStore?: PermissionStore clientToolNames: ReadonlySet clientToolStore?: ClientToolResultStore @@ -1487,8 +1523,15 @@ function extractSandboxRuntime(config: AgentConfig): SandboxRuntime | undefined } function isCloudbaseToolsEnabled(config: AgentConfig): boolean { - if (!config.sandbox?.runtime) return false - return config.sandbox.cloudbaseTools !== false + const runtime = config.sandbox?.runtime as SandboxRuntime | undefined + if (!runtime) return false + return config.sandbox?.cloudbaseTools !== false +} + +function resolveSandboxMode(sandbox: SandboxInstance | undefined): 'none' | 'local' | 'remote' { + if (!sandbox) return 'none' + if (sandbox.backend === 'local' || sandbox.id.startsWith('local:')) return 'local' + return 'remote' } async function resolveUserCredentials(config: AgentConfig): Promise { diff --git a/packages/open-agent-kernel/src/public/types.ts b/packages/open-agent-kernel/src/public/types.ts index 5997b56..b982877 100644 --- a/packages/open-agent-kernel/src/public/types.ts +++ b/packages/open-agent-kernel/src/public/types.ts @@ -55,18 +55,23 @@ export interface SandboxConfig { /** * 是否启用默认 sandbox。 * - * - `true`:使用内置默认 provider(当前为 `ags-stateful`),并补齐默认 runtime/scope + * - `true`:使用内置默认 provider(当前为 `local`),并补齐默认 runtime * - `false` / 未配置 sandbox:不启用 sandbox * * 如果显式传了 `runtime`,即使不传 `enabled` 也会启用 sandbox。 */ enabled?: boolean /** - * 默认 sandbox 产品类型。当前仅内置 `ags-stateful`。 + * 默认 sandbox 产品类型。当前内置 `local` 和 `ags-stateful`。 * * 未来可扩展到其他 CloudBase/第三方 sandbox 产品;高级用户也可直接传 `runtime`。 */ - provider?: 'ags-stateful' + provider?: 'local' | 'ags-stateful' + /** + * local provider 的工作区根目录。未设置时按 session 上下文推导: + * OAK_WORKSPACE_ROOT 或 os.tmpdir()/oak-workspaces/{envId}/{userId}/{conversationId}。 + */ + workspaceRoot?: string /** * 默认 AGS 数据面认证 JWT。 * @@ -77,7 +82,7 @@ export interface SandboxConfig { /** * Sandbox 后端实例(由用户从 `@cloudbase/open-agent-kernel/sandbox` 子模块构造, * 例如 `new AgsStatefulSandbox()`)。 - * 不传 `runtime` 但 `enabled: true` 时,默认使用 AgsStatefulSandbox。 + * 不传 `runtime` 但 `enabled: true` 时,默认使用 LocalRuntimeSandbox。 * * 类型故意宽泛(unknown),避免公共类型层依赖底层实现。 */ @@ -105,7 +110,7 @@ export interface SandboxConfig { * - `true`(默认):sandbox acquire 之后,自动调 `mcporter list cloudbase --schema` * 发现 cloudbase 工具集(DB / COS / 云函数 / 静态托管 / …), * 注入沙箱内 `/api/workspace/env` 凭证,然后封装为 `mcp__cloudbase__*` 工具暴露给 agent。 - * - `false`:完全不暴露 cloudbase 工具,agent 只能用 `mcp__sandbox__*` 文件系统/shell 工具。 + * - `false`:完全不暴露 cloudbase 工具,agent 只能使用当前 sandbox provider 提供的文件系统/shell 工具。 * * 仅在镜像内置 mcporter + cloudbase-mcp 时生效(默认 OpenVibeCoding 公开 vibecoding 镜像)。 * 镜像不带这两个工具时会自动 degrade(warning,不阻塞 session 启动)。 @@ -129,8 +134,8 @@ export interface SandboxConfig { /** * Spec B 新增。控制 cwd 是否在 send 边界自动快照到 COS。 - * - 'auto'(默认):runtime.backend === 'ags-stateful' 时启用,其他 runtime 关闭 - * - 'enabled':强制启用 — 若 runtime 不支持则 startSession 抛 ConfigError + * - 'auto'(默认):runtime.backend === 'ags-stateful' 时启用,local/其他 runtime 关闭 + * - 'enabled':强制启用 — local runtime 需配置 workspaceSyncStore,其他不支持 runtime 抛 ConfigError * - 'disabled':显式关闭 * @default 'auto' * @注意 启用时要求 sandbox.scope === 'shared'(详 Spec B §1.3) diff --git a/packages/open-agent-kernel/src/runtime/__tests__/agent-builder.test.ts b/packages/open-agent-kernel/src/runtime/__tests__/agent-builder.test.ts index e372db7..521d77e 100644 --- a/packages/open-agent-kernel/src/runtime/__tests__/agent-builder.test.ts +++ b/packages/open-agent-kernel/src/runtime/__tests__/agent-builder.test.ts @@ -32,12 +32,10 @@ beforeEach(() => { }) describe('buildClaudeQueryOptions — cwd / settingSources', () => { - it('no cwd → ephemeral cwd + settingSources=[]', () => { + it('no cwd → process cwd + settingSources=[]', () => { const { options } = buildClaudeQueryOptions(baseConfig) - expect(options.cwd).toMatch(/oak-ephemeral-/) - expect(options.cwd?.startsWith(os.tmpdir())).toBe(true) + expect(options.cwd).toBe(process.cwd()) expect(options.settingSources).toEqual([]) - // C1 fix verification:ephemeral dir 实际被创建 expect(existsSync(options.cwd!)).toBe(true) }) @@ -152,15 +150,9 @@ describe('buildClaudeQueryOptions — userMemory', () => { expect(options.settingSources).toContain('user') }) - // userMemory 启用且无 cwd → effectiveCwd 应该用 per-user 稳定路径 - // (而非 ephemeral 随机),让 SDK projects// 跨节点稳定 - it('userMemory.enabled + userId without cwd → effectiveCwd is stable per-user (not ephemeral)', () => { + it('userMemory.enabled + userId without cwd → effectiveCwd remains process cwd', () => { const { options } = buildClaudeQueryOptions({ ...baseConfig, userMemory: { enabled: true } }, { userId: 'alice' }) - expect(options.cwd).not.toMatch(/oak-ephemeral-/) - expect(options.cwd).toContain('alice') - // cwd 应是 claudeConfigDir 的上一级(去掉末尾 .claude) - expect(options.cwd?.endsWith('/.claude')).toBe(false) - // 跨调用应稳定(同 envId+userId 永远一致) + expect(options.cwd).toBe(process.cwd()) const second = buildClaudeQueryOptions({ ...baseConfig, userMemory: { enabled: true } }, { userId: 'alice' }) expect(second.options.cwd).toBe(options.cwd) }) @@ -202,22 +194,22 @@ describe('buildClaudeQueryOptions — userMemory', () => { expect(options.persistSession).toBe(false) }) - it('userMemory.enabled but no userId → no syncEngine, no CLAUDE_CONFIG_DIR', () => { + it('userMemory.enabled but no userId → no syncEngine, fallback CLAUDE_CONFIG_DIR', () => { const { options, syncEngine } = buildClaudeQueryOptions( { ...baseConfig, userMemory: { enabled: true } }, {}, // no userId ) expect(syncEngine).toBeUndefined() - expect(options.env?.CLAUDE_CONFIG_DIR).toBeUndefined() + expect(options.env?.CLAUDE_CONFIG_DIR).toContain('.oak') }) - it('userMemory disabled → no syncEngine even with userId', () => { + it('userMemory disabled → no syncEngine even with userId, fallback CLAUDE_CONFIG_DIR', () => { const { options, syncEngine } = buildClaudeQueryOptions( { ...baseConfig, userMemory: { enabled: false } }, { userId: 'alice' }, ) expect(syncEngine).toBeUndefined() - expect(options.env?.CLAUDE_CONFIG_DIR).toBeUndefined() + expect(options.env?.CLAUDE_CONFIG_DIR).toContain('.oak') }) it('userMemory + missing credentials → graceful degrade (no syncEngine, no throw)', () => { @@ -229,8 +221,7 @@ describe('buildClaudeQueryOptions — userMemory', () => { { userId: 'alice' }, ) expect(syncEngine).toBeUndefined() - // CLAUDE_CONFIG_DIR 也跟着清空(graceful degrade 全套不留半截状态) - expect(options.env?.CLAUDE_CONFIG_DIR).toBeUndefined() + expect(options.env?.CLAUDE_CONFIG_DIR).toContain('.oak') }).not.toThrow() }) }) @@ -248,6 +239,21 @@ describe('buildClaudeQueryOptions — workspaceSnapshot', () => { backend: 'docker-local', acquire: vi.fn(), } + const localRuntime: SandboxRuntime = { + backend: 'local', + acquire: vi.fn(), + } + const localRuntimeWithSync = { + backend: 'local', + acquire: vi.fn(), + createWorkspaceSyncEngine: vi.fn(() => ({ + bootstrap: vi.fn(), + snapshot: vi.fn(), + getRestoreStatus: vi.fn(), + })), + } satisfies SandboxRuntime & { + createWorkspaceSyncEngine: Function + } it('returns snapshotEngine when sandbox.runtime is ags-stateful and scope=shared (auto)', () => { const result = buildClaudeQueryOptions({ @@ -282,6 +288,47 @@ describe('buildClaudeQueryOptions — workspaceSnapshot', () => { ).toThrow(/does not support snapshot/) }) + it('returns no snapshotEngine for local mode when workspaceSnapshot=auto', () => { + const result = buildClaudeQueryOptions({ + ...baseConfig, + sandbox: { runtime: localRuntime, workspaceSnapshot: 'auto' }, + }) + expect(result.snapshotEngine).toBeUndefined() + }) + + it('throws ConfigError when local workspaceSnapshot is enabled without a sync engine', () => { + expect(() => + buildClaudeQueryOptions( + { + ...baseConfig, + sandbox: { runtime: localRuntime, workspaceSnapshot: 'enabled' }, + // buildClaudeQueryOptions gets these from createAgent.startSession. + }, + { userId: 'alice', conversationId: 'conv-1' }, + ), + ).toThrow(/workspaceSyncStore|createWorkspaceSyncEngine/) + }) + + it('returns snapshotEngine when local workspaceSnapshot is enabled with a sync engine', () => { + const result = buildClaudeQueryOptions( + { + ...baseConfig, + sandbox: { runtime: localRuntimeWithSync, workspaceSnapshot: 'enabled' }, + }, + { userId: 'alice', conversationId: 'conv-1' }, + ) + expect(result.snapshotEngine).toBeDefined() + }) + + it('throws ConfigError when local workspaceSnapshot is enabled without session context', () => { + expect(() => + buildClaudeQueryOptions({ + ...baseConfig, + sandbox: { runtime: localRuntimeWithSync, workspaceSnapshot: 'enabled' }, + }), + ).toThrow(/userId and conversationId/) + }) + it('throws ConfigError when snapshot enabled but scope=session', () => { expect(() => buildClaudeQueryOptions({ @@ -314,3 +361,53 @@ describe('buildClaudeQueryOptions — workspaceSnapshot', () => { expect(result.snapshotEngine).toBeDefined() }) }) + +describe('buildClaudeQueryOptions — sandbox mode tools', () => { + const sandboxInstance = { + id: 'sandbox-test', + async request(): Promise { + throw new Error('not used in this test') + }, + async release(): Promise {}, + } + + it('local mode enables SDK built-in tools and does not inject sandbox MCP', () => { + const workspaceRoot = os.tmpdir() + const { options } = buildClaudeQueryOptions( + { + ...baseConfig, + sandbox: { + runtime: { backend: 'local', acquire: vi.fn() }, + }, + }, + { + sandboxInstance: { ...sandboxInstance, id: 'local:conv', backend: 'local', workspaceRoot }, + sandboxMode: 'local', + workspaceRoot, + }, + ) + + expect(options.cwd).toBe(workspaceRoot) + expect(options.tools).toEqual(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep']) + expect(options.mcpServers).toBeUndefined() + }) + + it('local mode keeps Skill when skills are enabled', () => { + const { options } = buildClaudeQueryOptions( + { ...baseConfig, cwd: os.tmpdir(), skills: { enabled: 'all' } }, + { sandboxMode: 'local', workspaceRoot: os.tmpdir() }, + ) + + expect(options.tools).toEqual(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Skill']) + }) + + it('remote mode injects sandbox MCP and keeps SDK built-in tools disabled', () => { + const { options } = buildClaudeQueryOptions(baseConfig, { + sandboxInstance, + sandboxMode: 'remote', + }) + + expect(options.tools).toEqual([]) + expect(options.mcpServers).toHaveProperty('sandbox') + }) +}) diff --git a/packages/open-agent-kernel/src/runtime/agent-builder.ts b/packages/open-agent-kernel/src/runtime/agent-builder.ts index 1ce957d..6b3712c 100644 --- a/packages/open-agent-kernel/src/runtime/agent-builder.ts +++ b/packages/open-agent-kernel/src/runtime/agent-builder.ts @@ -45,9 +45,10 @@ import { } from '../claude-home/index.js' import { ConfigError, InvalidConfigError } from '../internal/errors.js' import type { AgentConfig, SandboxConfig, ToolDefinition, UserMemoryConfig } from '../public/types.js' +import type { LocalWorkspaceSyncEngine } from '../sandbox/local-workspace-sync/index.js' import { createSandboxMcpServer } from '../sandbox/sandbox-tools.js' import type { SandboxInstance, SandboxRuntime } from '../sandbox/types.js' -import { WorkspaceSnapshotEngine } from '../sandbox/workspace-snapshot/index.js' +import { WorkspaceSnapshotEngine, type WorkspaceSnapshotRuntime } from '../sandbox/workspace-snapshot/index.js' import { PACKAGE_VERSION } from '../version.js' import { resolveCredential, type ResolvedCredential } from './credential-factory.js' @@ -77,7 +78,7 @@ export interface BuiltClaudeQueryParams { * - send-end 后调用 engine.snapshot(inst) * - session.snapshotWorkspace() / getRestoreStatus() 转发到 engine */ - snapshotEngine?: WorkspaceSnapshotEngine + snapshotEngine?: WorkspaceSnapshotRuntime /** * OAK_DEBUG=1 时返回:我们指定给 SDK 的 claude CLI debug-file 绝对路径。 * SDK 把子进程 --debug 的详细输出写到这个文件(而非 stderr),调用方(create-agent) @@ -86,6 +87,8 @@ export interface BuiltClaudeQueryParams { debugFilePath?: string } +type SandboxMode = 'none' | 'local' | 'remote' + /** * 把 kernel 的 AgentConfig 翻译为 Claude Agent SDK query() 的 options。 * @@ -104,6 +107,8 @@ export function buildClaudeQueryOptions( config: AgentConfig, extra: { sandboxInstance?: SandboxInstance + sandboxMode?: SandboxMode + workspaceRoot?: string extraMcpServers?: Record conversationId?: string hookLocalState?: PreToolUseHookLocalState @@ -135,10 +140,14 @@ export function buildClaudeQueryOptions( // // 安全:'user' 在我们的部署模型里**不指宿主机 ~/.claude**,因为我们在 userMemory // 启用时把 CLAUDE_CONFIG_DIR 显式 redirect 到 per-user 派生目录。 + const sandboxMode = extra.sandboxMode ?? (extra.sandboxInstance ? 'remote' : 'none') const userCwd = config.cwd if (userCwd) { assertSafeUserCwd(userCwd) } + if (extra.workspaceRoot) { + assertSafeUserCwd(extra.workspaceRoot) + } // userMemory 启用时,先派生 claudeConfigDir(per-user 稳定路径)。 // 也用作 effectiveCwd:让 SDK 的 projects//memory/ 跨节点可复用。 @@ -177,7 +186,7 @@ export function buildClaudeQueryOptions( // 兜底是 process.cwd() —— 不自作主张造 ephemeral 目录(那是越权:kernel 不知道宿主 // 哪里可写、session 目录怎么隔离/GC)。需要可写、隔离的工作目录时,由调用方(agent // runtime)显式传 config.cwd(它才掌握运行环境)。 - const effectiveCwd = userCwd ?? process.cwd() + const effectiveCwd = extra.workspaceRoot ?? userCwd ?? process.cwd() // ── cwd 可写性检查(serverless 只读 FS)───────────────────────────── // claude CLI 在 cwd 里会做事(git 检测、写临时文件、--add-dir 等)。cwd 不可写会让 @@ -197,13 +206,13 @@ export function buildClaudeQueryOptions( // - userMemory 启用 → 'user'(SDK auto-memory / 用户级 CLAUDE.md / agent-memory) // 'user' 安全性:CLAUDE_CONFIG_DIR override 让 'user' 指向 per-user 隔离目录,不是宿主机。 const settingSources: SettingSource[] = [] - if (userCwd) settingSources.push('project') + if (userCwd || extra.workspaceRoot) settingSources.push('project') if (claudeConfigDir) settingSources.push('user') // ── Skills 启用前置校验(spec §4.1.2)────────── // 启用 skills 但未传 cwd → SDK 找不到 SKILL.md(settingSources 没含 'project') // 静默无效易混淆 → 显式 warning(不抛错,不破坏向后兼容) - if (config.skills?.enabled !== undefined && !userCwd) { + if (config.skills?.enabled !== undefined && !userCwd && !extra.workspaceRoot) { // eslint-disable-next-line no-console console.warn( '[oak/skills] skills configured but cwd not set — SKILL.md will not be discovered. ' + @@ -307,7 +316,7 @@ export function buildClaudeQueryOptions( const mergedMcpServers: Record | undefined = (() => { const userServers = config.mcpServers ? validateMcpServers(config.mcpServers) : undefined const merged: Record = { ...(userServers ?? {}) } - if (extra.sandboxInstance) { + if (extra.sandboxInstance && sandboxMode === 'remote') { // key 'sandbox' 决定工具名前缀:mcp__sandbox__bash 等 merged.sandbox = createSandboxMcpServer(extra.sandboxInstance) } @@ -367,17 +376,11 @@ export function buildClaudeQueryOptions( // 注意:必须用条件展开避免把 undefined 透到 engine —— `{ ...DEFAULT, ...opts }` // 模式下,显式赋 undefined 会覆盖默认值,导致 setTimeout(undefined) 立即触发, // bootstrap 会以 SandboxRestoreTimeout: init timeout after undefinedms 失败。 - const snapshotEnabled = resolveSnapshotMode(config.sandbox) - const snapshotEngine = snapshotEnabled - ? new WorkspaceSnapshotEngine({ - ...(config.sandbox?.workspaceSnapshotTimeoutMs !== undefined && { - snapshotTimeoutMs: config.sandbox.workspaceSnapshotTimeoutMs, - }), - ...(config.sandbox?.workspaceInitTimeoutMs !== undefined && { - initTimeoutMs: config.sandbox.workspaceInitTimeoutMs, - }), - }) - : undefined + const snapshotEngine = buildWorkspaceSnapshotRuntime(config.sandbox, { + envId: config.envId, + userId: extra.userId, + conversationId: extra.conversationId, + }) const options: ClaudeOptions = { model: credential.modelId, @@ -413,7 +416,7 @@ export function buildClaudeQueryOptions( // 例外:启用 skills 时必须保留 'Skill' 工具,否则模型无法 invoke discovered skills // (SDK 文档:"If you also pass an explicit tools list, include 'Skill' in that list // so Claude can invoke skills.") - tools: config.skills?.enabled !== undefined ? ['Skill'] : [], + tools: resolveSdkTools(config, sandboxMode), } return { options, credential, syncEngine, snapshotEngine, ...(debugFilePath ? { debugFilePath } : {}) } @@ -425,6 +428,14 @@ function isUserMemoryEnabled(config: UserMemoryConfig | undefined): boolean { return config === true || (typeof config === 'object' && config.enabled === true) } +function resolveSdkTools(config: AgentConfig, sandboxMode: SandboxMode): string[] { + const tools = sandboxMode === 'local' ? ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep'] : [] + if (config.skills?.enabled !== undefined) { + tools.push('Skill') + } + return tools +} + /** * Spec B:解析 workspaceSnapshot 模式 + 校验 scope。 * @@ -440,12 +451,60 @@ function isUserMemoryEnabled(config: UserMemoryConfig | undefined): boolean { * 启用后 scope 必须是 'shared'(同 envId 共享容器,跨 session 接续 cwd), * 否则 throw ConfigError(包括 scope='session' 和 scope undefined 默认场景)。 */ +function buildWorkspaceSnapshotRuntime( + sandboxConfig: SandboxConfig | undefined, + ctx: { envId: string; userId?: string; conversationId?: string }, +): WorkspaceSnapshotRuntime | undefined { + if (!resolveSnapshotMode(sandboxConfig)) return undefined + + const runtime = sandboxConfig?.runtime as SandboxRuntime | undefined + if (runtime?.backend === 'local') { + if (!ctx.userId || !ctx.conversationId) { + throw new ConfigError('local workspaceSnapshot requires userId and conversationId from startSession context.') + } + const factory = (runtime as LocalWorkspaceSyncRuntime).createWorkspaceSyncEngine + if (typeof factory !== 'function') { + throw new ConfigError( + 'workspaceSnapshot="enabled" with provider="local" requires LocalRuntimeSandbox or a runtime that implements createWorkspaceSyncEngine().', + ) + } + const engine = factory.call(runtime, { + envId: ctx.envId, + userId: ctx.userId, + conversationId: ctx.conversationId, + }) + if (!engine) { + throw new ConfigError( + 'workspaceSnapshot="enabled" with provider="local" requires LocalRuntimeSandboxOptions.workspaceSyncStore.', + ) + } + return engine + } + + return new WorkspaceSnapshotEngine({ + ...(sandboxConfig?.workspaceSnapshotTimeoutMs !== undefined && { + snapshotTimeoutMs: sandboxConfig.workspaceSnapshotTimeoutMs, + }), + ...(sandboxConfig?.workspaceInitTimeoutMs !== undefined && { + initTimeoutMs: sandboxConfig.workspaceInitTimeoutMs, + }), + }) +} + +interface LocalWorkspaceSyncRuntime { + createWorkspaceSyncEngine?: (ctx: { + envId: string + userId: string + conversationId: string + }) => LocalWorkspaceSyncEngine | undefined +} + function resolveSnapshotMode(sandboxConfig: SandboxConfig | undefined): boolean { const mode = sandboxConfig?.workspaceSnapshot ?? 'auto' const scope = sandboxConfig?.scope ?? 'session' const runtime = sandboxConfig?.runtime as SandboxRuntime | undefined const backend = runtime?.backend - const supportsSnapshot = backend === 'ags-stateful' + const supportsSnapshot = backend === 'ags-stateful' || backend === 'local' if (mode === 'disabled') return false @@ -457,11 +516,13 @@ function resolveSnapshotMode(sandboxConfig: SandboxConfig | undefined): boolean ) } - // mode='auto' + 不支持 snapshot 的 runtime → 静默不启用 - if (mode === 'auto' && !supportsSnapshot) return false + // mode='auto' keeps local disabled to avoid surprising COS/network failures + // when callers have not provided a persistence backend. + if (mode === 'auto' && backend !== 'ags-stateful') return false - // 到这里 mode 是 'enabled' 或 'auto',且 backend 支持 snapshot → 必须 scope='shared' - if (scope !== 'shared') { + // AGS/TRW snapshot still requires shared scope. Local snapshot is scoped by + // userId + conversationId and does not share an AGS instance. + if (backend === 'ags-stateful' && scope !== 'shared') { throw new ConfigError( `workspaceSnapshot 要求 sandbox.scope='shared'(同 envId 共享容器,跨 session 接续 cwd),` + `当前 scope='${scope}'。改为 createAgent({ sandbox: { scope: 'shared', ... } })。` + diff --git a/packages/open-agent-kernel/src/sandbox/__tests__/cloudbase-mcp-inprocess.test.ts b/packages/open-agent-kernel/src/sandbox/__tests__/cloudbase-mcp-inprocess.test.ts new file mode 100644 index 0000000..0c01421 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/__tests__/cloudbase-mcp-inprocess.test.ts @@ -0,0 +1,113 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + createCloudBaseMcpServer: vi.fn(), + connectServer: vi.fn(), + ping: vi.fn(), + clientConnect: vi.fn(), + listTools: vi.fn(), + callTool: vi.fn(), + createLinkedPair: vi.fn(), +})) + +vi.mock('@cloudbase/cloudbase-mcp', () => ({ + createCloudBaseMcpServer: mocks.createCloudBaseMcpServer, +})) + +vi.mock('@modelcontextprotocol/sdk/inMemory.js', () => ({ + InMemoryTransport: { + createLinkedPair: mocks.createLinkedPair, + }, +})) + +vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({ + Client: class { + connect = mocks.clientConnect + listTools = mocks.listTools + callTool = mocks.callTool + }, +})) + +const { createCloudBaseMcpServerInProcess } = await import('../cloudbase-mcp-inprocess.js') + +describe('createCloudBaseMcpServerInProcess', () => { + beforeEach(() => { + mocks.createCloudBaseMcpServer.mockReset() + mocks.connectServer.mockReset() + mocks.ping.mockReset() + mocks.clientConnect.mockReset() + mocks.listTools.mockReset() + mocks.callTool.mockReset() + mocks.createLinkedPair.mockReset() + + mocks.createLinkedPair.mockReturnValue([{ kind: 'client' }, { kind: 'server' }]) + mocks.createCloudBaseMcpServer.mockResolvedValue({ + connect: mocks.connectServer, + server: { ping: mocks.ping }, + }) + mocks.listTools.mockResolvedValue({ + tools: [ + { + name: 'envQuery', + description: 'Query environment', + inputSchema: { type: 'object', properties: {}, required: [] }, + }, + { + name: 'readNoSqlDatabaseContent', + description: 'Read NoSQL content', + inputSchema: { + type: 'object', + properties: { collectionName: { type: 'string' } }, + required: ['collectionName'], + }, + }, + ], + }) + }) + + it('creates cloudbase-mcp in process and registers all listed tools', async () => { + const bundle = await createCloudBaseMcpServerInProcess({ + createServer: mocks.createCloudBaseMcpServer, + workspaceFolderPaths: '/tmp/oak-local', + getCredentials: async () => ({ + envId: 'env-test', + secretId: 'secret-id', + secretKey: 'secret-key', + sessionToken: 'token', + }), + }) + + expect(mocks.createCloudBaseMcpServer).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'cloudbase-mcp', + cloudBaseOptions: { + envId: 'env-test', + secretId: 'secret-id', + secretKey: 'secret-key', + token: 'token', + }, + ide: 'open-agent-kernel', + cloudMode: true, + workspaceFolderPaths: '/tmp/oak-local', + }), + ) + expect(mocks.clientConnect).toHaveBeenCalled() + expect(mocks.connectServer).toHaveBeenCalled() + expect(mocks.listTools).toHaveBeenCalled() + expect(bundle.toolCount).toBe(2) + expect(bundle.degradedReason).toBeUndefined() + }) + + it('degrades when credentials are unavailable', async () => { + const bundle = await createCloudBaseMcpServerInProcess({ + createServer: mocks.createCloudBaseMcpServer, + getCredentials: async () => { + throw new Error('missing credentials') + }, + }) + + expect(bundle.toolCount).toBe(0) + expect(bundle.degradedReason).toMatch(/credentials unavailable/) + expect(mocks.createCloudBaseMcpServer).not.toHaveBeenCalled() + }) +}) diff --git a/packages/open-agent-kernel/src/sandbox/__tests__/local-runtime-sandbox.test.ts b/packages/open-agent-kernel/src/sandbox/__tests__/local-runtime-sandbox.test.ts new file mode 100644 index 0000000..bcca8f4 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/__tests__/local-runtime-sandbox.test.ts @@ -0,0 +1,94 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { FileSystemLocalWorkspaceStore } from '../local-workspace-sync/index.js' +import { LocalRuntimeSandbox } from '../local-runtime-sandbox.js' + +const cleanup: string[] = [] + +afterEach(async () => { + while (cleanup.length > 0) { + const dir = cleanup.pop() + if (dir) await rm(dir, { recursive: true, force: true }) + } +}) + +describe('LocalRuntimeSandbox', () => { + it('creates a writable workspaceRoot and returns a local instance', async () => { + const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'oak-local-test-')) + cleanup.push(workspaceRoot) + + const runtime = new LocalRuntimeSandbox({ workspaceRoot }) + const instance = await runtime.acquire({ + envId: 'env-test', + conversationId: 'conv-test', + userId: 'user-test', + }) + + expect(instance.id).toBe('local:conv-test') + expect(instance.backend).toBe('local') + expect(instance.workspaceRoot).toBe(workspaceRoot) + }) + + it('derives a per-session workspaceRoot when none is configured', async () => { + const base = await mkdtemp(path.join(os.tmpdir(), 'oak-local-base-')) + cleanup.push(base) + process.env.OAK_WORKSPACE_ROOT = base + + try { + const runtime = new LocalRuntimeSandbox() + const instance = await runtime.acquire({ + envId: 'env/test', + conversationId: 'conv/test', + userId: 'user/test', + }) + + expect(instance.workspaceRoot).toBe(path.join(base, 'env-test', 'user-test', 'conv-test')) + } finally { + delete process.env.OAK_WORKSPACE_ROOT + } + }) + + it('throws a clear error when request is used in local mode', async () => { + const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'oak-local-test-')) + cleanup.push(workspaceRoot) + + const runtime = new LocalRuntimeSandbox({ workspaceRoot }) + const instance = await runtime.acquire({ + envId: 'env-test', + conversationId: 'conv-test', + userId: 'user-test', + }) + + await expect(instance.request('/api/tools/bash')).rejects.toThrow(/does not expose an HTTP data plane/) + }) + + it('rejects conflicting cwd and workspaceRoot', () => { + expect( + () => + new LocalRuntimeSandbox({ + cwd: '/tmp/oak-a', + workspaceRoot: '/tmp/oak-b', + }), + ).toThrow(/must point to the same directory/) + }) + + it('creates a local workspace sync engine when a store is configured', async () => { + const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'oak-local-test-')) + const storeRoot = await mkdtemp(path.join(os.tmpdir(), 'oak-local-store-')) + cleanup.push(workspaceRoot, storeRoot) + + const runtime = new LocalRuntimeSandbox({ + workspaceRoot, + workspaceSyncStore: new FileSystemLocalWorkspaceStore({ root: storeRoot }), + }) + const engine = runtime.createWorkspaceSyncEngine({ + envId: 'env-test', + conversationId: 'conv-test', + userId: 'user-test', + }) + + expect(engine).toBeDefined() + }) +}) diff --git a/packages/open-agent-kernel/src/sandbox/cloudbase-mcp-inprocess.ts b/packages/open-agent-kernel/src/sandbox/cloudbase-mcp-inprocess.ts new file mode 100644 index 0000000..f1afbd4 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/cloudbase-mcp-inprocess.ts @@ -0,0 +1,180 @@ +/** + * CloudBase MCP tools for local runtime sandbox. + * + * Unlike the AGS/TRW path in `cloudbase-mcp.ts`, this implementation does not + * call mcporter or any sandbox HTTP data plane. It runs `@cloudbase/cloudbase-mcp` + * in the OAK process and connects to it with an in-memory MCP transport. + */ + +import { createRequire } from 'node:module' +import { createSdkMcpServer, tool as sdkTool } from '@anthropic-ai/claude-agent-sdk' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' +import type { CloudBaseUserCredentials, CloudBaseMcpBundle } from './cloudbase-mcp.js' +import { jsonSchemaToZodRawShapeForCloudBaseMcp } from './cloudbase-mcp.js' + +interface CloudBaseMcpModule { + createCloudBaseMcpServer(options: Record): Promise<{ + connect(transport: unknown): Promise + server?: { ping?: () => Promise } + }> +} + +interface McpToolDef { + name: string + description?: string + inputSchema?: Parameters[0] +} + +const require = createRequire(import.meta.url) + +export interface CreateCloudBaseMcpInProcessOptions { + /** + * 获取用户租户 CloudBase 凭证。初始化时调用;工具调用如遇凭证问题由 + * cloudbase-mcp 自身返回错误,后续可扩展为重建 client。 + */ + getCredentials: () => Promise + /** local runtime workspace root,用于 cloudbase-mcp 的 WORKSPACE_FOLDER_PATHS 语义。 */ + workspaceFolderPaths?: string + /** 透传给 cloudbase-mcp 的 IDE 标识。 */ + integrationIde?: string + /** 诊断日志回调(不传则按 OAK_DEBUG=1 走 console.error)。 */ + log?: (msg: string) => void + /** 测试注入点;生产代码不需要传。 */ + createServer?: CloudBaseMcpModule['createCloudBaseMcpServer'] +} + +function defaultLog(msg: string): void { + if (process.env.OAK_DEBUG === '1') { + // eslint-disable-next-line no-console + console.error(`[oak][cloudbase-mcp-local] ${msg}`) + } +} + +function buildEmptyServer(reason: string, log: (msg: string) => void): CloudBaseMcpBundle { + log(`degraded: ${reason}`) + return { + server: createSdkMcpServer({ + name: 'cloudbase', + version: '1.0.0', + tools: [], + }), + toolCount: 0, + degradedReason: reason, + } +} + +/** + * 在 OAK 本进程内构造 CloudBase MCP server。 + * + * 失败策略与远程 sandbox 版本一致:初始化失败时返回空 server,让 agent 仍可使用 + * local 文件系统/Bash 工具。 + */ +export async function createCloudBaseMcpServerInProcess( + options: CreateCloudBaseMcpInProcessOptions, +): Promise { + const { + getCredentials, + workspaceFolderPaths = '', + integrationIde = 'open-agent-kernel', + log = defaultLog, + createServer, + } = options + + let credentials: CloudBaseUserCredentials + try { + credentials = await getCredentials() + } catch (err) { + return buildEmptyServer(`credentials unavailable: ${(err as Error).message}`, log) + } + + let createCloudBaseMcpServer: CloudBaseMcpModule['createCloudBaseMcpServer'] + if (createServer) { + createCloudBaseMcpServer = createServer + } else { + try { + const mod = require('@cloudbase/cloudbase-mcp') as CloudBaseMcpModule + createCloudBaseMcpServer = mod.createCloudBaseMcpServer + if (typeof createCloudBaseMcpServer !== 'function') { + return buildEmptyServer('createCloudBaseMcpServer export missing', log) + } + } catch (err) { + return buildEmptyServer(`load @cloudbase/cloudbase-mcp failed: ${(err as Error).message}`, log) + } + } + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair() + const client = new Client({ + name: 'open-agent-kernel-cloudbase', + version: '1.0.0', + }) + + try { + const server = await createCloudBaseMcpServer({ + name: 'cloudbase-mcp', + version: '1.0.0', + cloudBaseOptions: { + envId: credentials.envId, + secretId: credentials.secretId, + secretKey: credentials.secretKey, + token: credentials.sessionToken, + }, + ide: integrationIde, + cloudMode: true, + workspaceFolderPaths, + }) + + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]) + await server.server?.ping?.() + } catch (err) { + return buildEmptyServer(`in-process cloudbase-mcp init failed: ${(err as Error).message}`, log) + } + + let toolDefs: McpToolDef[] + try { + const listed = await client.listTools() + toolDefs = listed.tools as McpToolDef[] + } catch (err) { + return buildEmptyServer(`listTools failed: ${(err as Error).message}`, log) + } + + const tools = toolDefs + .filter((t) => typeof t.name === 'string' && t.name.length > 0) + .map((t) => { + const zodShape = jsonSchemaToZodRawShapeForCloudBaseMcp(t.inputSchema) + return sdkTool( + t.name, + (t.description ?? `CloudBase tool: ${t.name}`) + + '\n\nNOTE: localPath refers to paths inside the local runtime workspace.', + zodShape, + async (args: Record) => { + try { + const res = await client.callTool({ + name: t.name, + arguments: args, + }) + return { + content: [{ type: 'text', text: JSON.stringify(res) }], + isError: false, + } + } catch (err) { + return { + content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }], + isError: true, + } + } + }, + ) + }) + + log(`registered ${tools.length} in-process cloudbase tools`) + + return { + server: createSdkMcpServer({ + name: 'cloudbase', + version: '1.0.0', + tools, + }), + toolCount: tools.length, + } +} diff --git a/packages/open-agent-kernel/src/sandbox/cloudbase-mcp.ts b/packages/open-agent-kernel/src/sandbox/cloudbase-mcp.ts index b34e358..fca046d 100644 --- a/packages/open-agent-kernel/src/sandbox/cloudbase-mcp.ts +++ b/packages/open-agent-kernel/src/sandbox/cloudbase-mcp.ts @@ -76,7 +76,7 @@ export interface CloudBaseMcpBundle { // ─── JSON Schema → zod raw shape ────────────────────────────────────── -interface JsonSchemaProperty { +export interface JsonSchemaProperty { type?: string description?: string enum?: string[] @@ -85,7 +85,7 @@ interface JsonSchemaProperty { required?: string[] } -interface JsonSchemaObject { +export interface JsonSchemaObject { type?: string properties?: Record required?: string[] @@ -97,7 +97,9 @@ interface JsonSchemaObject { * 直接照搬 stateful-infra 的实现,cloudbase 工具的 schema 都是简单类型(string / number / * boolean / array / object / enum),不涉及复杂组合式校验,转换成本低。 */ -function jsonSchemaToZodRawShape(schema: JsonSchemaObject | undefined): Record { +export function jsonSchemaToZodRawShapeForCloudBaseMcp( + schema: JsonSchemaObject | undefined, +): Record { if (!schema || schema.type !== 'object' || !schema.properties) return {} const shape: Record = {} const required = new Set(schema.required ?? []) @@ -492,7 +494,7 @@ export async function createCloudBaseMcpServer(options: CreateCloudBaseMcpOption const tools = toolDefs .filter((t) => t.name && !SKIP_TOOLS.has(t.name)) .map((t) => { - const zodShape = jsonSchemaToZodRawShape(t.inputSchema) + const zodShape = jsonSchemaToZodRawShapeForCloudBaseMcp(t.inputSchema) return sdkTool( t.name, (t.description ?? `CloudBase tool: ${t.name}`) + diff --git a/packages/open-agent-kernel/src/sandbox/index.ts b/packages/open-agent-kernel/src/sandbox/index.ts index 83fcd70..2ddf317 100644 --- a/packages/open-agent-kernel/src/sandbox/index.ts +++ b/packages/open-agent-kernel/src/sandbox/index.ts @@ -12,12 +12,23 @@ * }) * ``` * - * 高级用户仍可显式传入 `sandbox.runtime` 覆盖默认 AgsStatefulSandbox。 + * 高级用户仍可显式传入 `sandbox.runtime` 覆盖默认 runtime。 */ export type { SandboxRuntime, SandboxInstance, SandboxAcquireContext } from './types.js' export { AgsStatefulSandbox, type AgsStatefulSandboxOptions } from './ags-stateful-sandbox.js' +export { LocalRuntimeSandbox, type LocalRuntimeSandboxOptions } from './local-runtime-sandbox.js' +export { + LocalWorkspaceSyncEngine, + FileSystemLocalWorkspaceStore, + CloudBaseCosLocalWorkspaceStore, + type LocalWorkspaceSyncContext, + type LocalWorkspaceSyncStore, + type LocalWorkspaceSyncEngineOptions, + type FileSystemLocalWorkspaceStoreOptions, + type CloudBaseCosLocalWorkspaceStoreOptions, +} from './local-workspace-sync/index.js' export { createSandboxMcpServer } from './sandbox-tools.js' @@ -27,3 +38,7 @@ export { type CloudBaseMcpBundle, type CloudBaseUserCredentials, } from './cloudbase-mcp.js' +export { + createCloudBaseMcpServerInProcess, + type CreateCloudBaseMcpInProcessOptions, +} from './cloudbase-mcp-inprocess.js' diff --git a/packages/open-agent-kernel/src/sandbox/local-runtime-sandbox.ts b/packages/open-agent-kernel/src/sandbox/local-runtime-sandbox.ts new file mode 100644 index 0000000..a6d56d5 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-runtime-sandbox.ts @@ -0,0 +1,107 @@ +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { ConfigError, SandboxError } from '../internal/errors.js' +import { LocalWorkspaceSyncEngine, type LocalWorkspaceSyncStore } from './local-workspace-sync/index.js' +import type { SandboxAcquireContext, SandboxInstance, SandboxRuntime } from './types.js' + +export interface LocalRuntimeSandboxOptions { + /** + * Local runtime workspace root. If omitted, acquire() derives a per-session + * directory from OAK_WORKSPACE_ROOT or os.tmpdir(). + */ + workspaceRoot?: string + /** + * AgentConfig.cwd mirrored into the runtime by createAgent(). It has higher + * priority than workspaceRoot because Claude SDK built-in tools run from cwd. + */ + cwd?: string + /** + * Optional persistence backend used when sandbox.workspaceSnapshot is enabled. + * Production callers can use CloudBaseCosLocalWorkspaceStore; tests and + * examples can use FileSystemLocalWorkspaceStore. + */ + workspaceSyncStore?: LocalWorkspaceSyncStore +} + +export class LocalRuntimeSandbox implements SandboxRuntime { + readonly backend = 'local' + + private readonly options: LocalRuntimeSandboxOptions + + constructor(options: LocalRuntimeSandboxOptions = {}) { + if (options.cwd && options.workspaceRoot && path.resolve(options.cwd) !== path.resolve(options.workspaceRoot)) { + throw new ConfigError( + 'AgentConfig.cwd and sandbox.workspaceRoot must point to the same directory when sandbox.provider is local.', + ) + } + this.options = options + } + + async acquire(ctx: SandboxAcquireContext): Promise { + const workspaceRoot = this.resolveWorkspaceRoot(ctx) + await fs.mkdir(workspaceRoot, { recursive: true }) + await this.assertWritable(workspaceRoot) + + ctx.onProgress?.({ + phase: 'local_workspace', + message: 'local sandbox workspace is ready', + }) + + return { + id: `local:${ctx.conversationId}`, + backend: this.backend, + workspaceRoot, + async request(): Promise { + throw new SandboxError( + 'LocalRuntimeSandbox does not expose an HTTP data plane. Use Claude SDK built-in tools in local mode.', + ) + }, + async release(): Promise { + // Phase 0 keeps local workspaces in place. Phase 1 may trigger a final snapshot here. + }, + } + } + + createWorkspaceSyncEngine(ctx: SandboxAcquireContext): LocalWorkspaceSyncEngine | undefined { + if (!this.options.workspaceSyncStore) return undefined + return new LocalWorkspaceSyncEngine({ + store: this.options.workspaceSyncStore, + ctx: { + envId: ctx.envId, + userId: ctx.userId ?? 'default', + conversationId: ctx.conversationId, + }, + }) + } + + private resolveWorkspaceRoot(ctx: SandboxAcquireContext): string { + const configuredRoot = this.options.cwd ?? this.options.workspaceRoot + if (configuredRoot) return path.resolve(configuredRoot) + + const base = process.env.OAK_WORKSPACE_ROOT || path.join(os.tmpdir(), 'oak-workspaces') + return path.resolve( + base, + safeSegment(ctx.envId), + safeSegment(ctx.userId ?? 'default'), + safeSegment(ctx.conversationId), + ) + } + + private async assertWritable(dir: string): Promise { + const probe = path.join(dir, `.oak-write-probe-${process.pid}-${Date.now()}`) + try { + await fs.writeFile(probe, 'ok', { flag: 'wx' }) + await fs.unlink(probe) + } catch (err) { + throw new ConfigError( + 'LocalRuntimeSandbox workspaceRoot is not writable. Set AgentConfig.cwd or sandbox.workspaceRoot to a writable directory.', + ) + } + } +} + +function safeSegment(input: string): string { + const normalized = input.replace(/[^a-zA-Z0-9._-]/g, '-') + return normalized || 'default' +} diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/__tests__/engine.test.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/__tests__/engine.test.ts new file mode 100644 index 0000000..f1d0e71 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/__tests__/engine.test.ts @@ -0,0 +1,99 @@ +import * as fs from 'node:fs/promises' +import * as os from 'node:os' +import * as path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { FileSystemLocalWorkspaceStore } from '../filesystem-store.js' +import { LocalWorkspaceSyncEngine } from '../engine.js' +import type { SandboxInstance } from '../../types.js' + +const cleanup: string[] = [] + +afterEach(async () => { + while (cleanup.length > 0) { + const dir = cleanup.pop() + if (dir) await fs.rm(dir, { recursive: true, force: true }) + } +}) + +async function tmpDir(prefix: string): Promise { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)) + cleanup.push(dir) + return dir +} + +async function writeFile(root: string, relPath: string, content: string): Promise { + const abs = path.join(root, relPath) + await fs.mkdir(path.dirname(abs), { recursive: true }) + await fs.writeFile(abs, content) +} + +async function readFile(root: string, relPath: string): Promise { + return fs.readFile(path.join(root, relPath), 'utf8') +} + +function localInstance(workspaceRoot: string): SandboxInstance { + return { + id: 'local:conv-1', + backend: 'local', + workspaceRoot, + async request(): Promise { + throw new Error('not used') + }, + async release(): Promise {}, + } +} + +describe('LocalWorkspaceSyncEngine', () => { + it('restores fresh when the store is empty', async () => { + const storeRoot = await tmpDir('oak-local-store-') + const workspaceRoot = await tmpDir('oak-local-workspace-') + const engine = new LocalWorkspaceSyncEngine({ + store: new FileSystemLocalWorkspaceStore({ root: storeRoot }), + ctx: { envId: 'env-1', userId: 'alice', conversationId: 'conv-1' }, + }) + + const status = await engine.bootstrap(localInstance(workspaceRoot), { credentials: {} }) + expect(status?.restored).toBe('fresh') + expect(await engine.getRestoreStatus(localInstance(workspaceRoot))).toBe('fresh') + }) + + it('snapshots then restores workspace files into a new local workspace', async () => { + const storeRoot = await tmpDir('oak-local-store-') + const workspaceA = await tmpDir('oak-local-workspace-a-') + const workspaceB = await tmpDir('oak-local-workspace-b-') + const store = new FileSystemLocalWorkspaceStore({ root: storeRoot }) + const ctx = { envId: 'env-1', userId: 'alice', conversationId: 'conv-1' } + const first = new LocalWorkspaceSyncEngine({ store, ctx }) + const second = new LocalWorkspaceSyncEngine({ store, ctx }) + + await first.bootstrap(localInstance(workspaceA), { credentials: {} }) + await writeFile(workspaceA, 'README.md', 'hello local workspace') + await writeFile(workspaceA, 'src/index.ts', 'export const ok = true\n') + await first.snapshot(localInstance(workspaceA)) + + const restored = await second.bootstrap(localInstance(workspaceB), { credentials: {} }) + expect(restored?.restored).toBe('full') + expect(await readFile(workspaceB, 'README.md')).toBe('hello local workspace') + expect(await readFile(workspaceB, 'src/index.ts')).toContain('ok') + }) + + it('replaces the remote snapshot so deleted files stay deleted on restore', async () => { + const storeRoot = await tmpDir('oak-local-store-') + const workspaceA = await tmpDir('oak-local-workspace-a-') + const workspaceB = await tmpDir('oak-local-workspace-b-') + const store = new FileSystemLocalWorkspaceStore({ root: storeRoot }) + const ctx = { envId: 'env-1', userId: 'alice', conversationId: 'conv-1' } + const engine = new LocalWorkspaceSyncEngine({ store, ctx }) + + await engine.bootstrap(localInstance(workspaceA), { credentials: {} }) + await writeFile(workspaceA, 'keep.txt', 'keep') + await writeFile(workspaceA, 'delete.txt', 'delete') + await engine.snapshot(localInstance(workspaceA)) + await fs.rm(path.join(workspaceA, 'delete.txt')) + await engine.snapshot(localInstance(workspaceA)) + + await new LocalWorkspaceSyncEngine({ store, ctx }).bootstrap(localInstance(workspaceB), { credentials: {} }) + await expect(fs.access(path.join(workspaceB, 'delete.txt'))).rejects.toThrow() + expect(await readFile(workspaceB, 'keep.txt')).toBe('keep') + }) +}) diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/cloudbase-cos-store.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/cloudbase-cos-store.ts new file mode 100644 index 0000000..409fff8 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/cloudbase-cos-store.ts @@ -0,0 +1,183 @@ +import * as fs from 'node:fs/promises' +import * as path from 'node:path' +import { InvalidConfigError, ResourceError } from '../../internal/errors.js' +import type { PlatformCredentials } from '../../public/types.js' +import { assertSafeRelativePath, listWorkspaceFiles, safeSegment } from './file-utils.js' +import type { LocalWorkspaceSyncContext, LocalWorkspaceSyncStore } from './types.js' +import type { Restored, SyncStatus } from '../workspace-snapshot/index.js' + +export interface CloudBaseCosLocalWorkspaceStoreOptions { + credentials?: PlatformCredentials & { envId: string } + prefix?: string +} + +interface ManagerStorage { + uploadFile(args: { localPath: string; cloudPath: string }): Promise + walkCloudDir(prefix: string): Promise> + getTemporaryUrl( + fileList: Array<{ cloudPath: string; maxAge?: number }>, + ): Promise> + deleteFile(cloudPathList: string[]): Promise +} + +interface CloudBaseManagerInstance { + storage: ManagerStorage +} + +interface ManagerCtor { + new (opts: { + secretId: string + secretKey: string + envId: string + token?: string + region?: string + }): CloudBaseManagerInstance +} + +const DEFAULT_PREFIX = 'oak-workspaces' +const DELETE_NOT_EXIST_CODES = new Set(['STORAGE.FileNotFound', 'STORAGE_FILE_NONEXIST', 'NoSuchKey']) + +export class CloudBaseCosLocalWorkspaceStore implements LocalWorkspaceSyncStore { + private readonly credentials: PlatformCredentials & { envId: string } + private readonly prefix: string + private readonly status = new Map() + private manager: CloudBaseManagerInstance | null = null + + constructor(opts: CloudBaseCosLocalWorkspaceStoreOptions = {}) { + if (!opts.credentials?.envId || !opts.credentials.secretId || !opts.credentials.secretKey) { + throw new InvalidConfigError( + 'CloudBaseCosLocalWorkspaceStore requires AgentConfig.credentials for local workspace snapshot.', + ) + } + this.credentials = opts.credentials + this.prefix = normalizePrefix(opts.prefix ?? DEFAULT_PREFIX) + } + + async restore(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise { + const started = Date.now() + const manager = await this.getManager() + const prefix = this.resolvePrefix(ctx) + const listed = await manager.storage.walkCloudDir(prefix) + let restoredFiles = 0 + + await fs.mkdir(workspaceRoot, { recursive: true }) + await Promise.all( + listed.map(async (item) => { + const key = item.Key + if (!key || key.endsWith('/')) return + const size = typeof item.Size === 'number' ? item.Size : Number(item.Size) + if (Number.isFinite(size) && size === 0) return + const relPath = key.slice(prefix.length) + if (!relPath) return + assertSafeRelativePath(relPath) + + const signed = await manager.storage.getTemporaryUrl([{ cloudPath: key, maxAge: 600 }]) + const url = signed?.[0]?.url + if (!url) throw new Error('CloudBase temporary URL is empty') + const resp = await fetch(url) + if (!resp.ok) throw new Error('CloudBase workspace file download failed') + const buf = Buffer.from(await resp.arrayBuffer()) + const localPath = path.join(workspaceRoot, relPath) + await fs.mkdir(path.dirname(localPath), { recursive: true }) + await fs.writeFile(localPath, buf) + restoredFiles += 1 + }), + ) + + const restored: Restored = restoredFiles > 0 ? 'full' : 'fresh' + this.status.set(this.statusKey(ctx), restored) + return { + restored, + restoredAt: new Date().toISOString(), + restoreMs: Date.now() - started, + source: restoredFiles > 0 ? 'cos' : 'none', + cosMetaFileCount: restoredFiles, + } + } + + async snapshot(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise<{ ms: number }> { + const started = Date.now() + const manager = await this.getManager() + const prefix = this.resolvePrefix(ctx) + const localFiles = await listWorkspaceFiles(workspaceRoot) + const localKeys = new Set(localFiles.map((relPath) => prefix + relPath)) + + await Promise.all( + localFiles.map(async (relPath) => { + assertSafeRelativePath(relPath) + await manager.storage.uploadFile({ + localPath: path.join(workspaceRoot, relPath), + cloudPath: prefix + relPath, + }) + }), + ) + + const remoteFiles = await manager.storage.walkCloudDir(prefix) + const toDelete = remoteFiles + .map((item) => item.Key) + .filter((key): key is string => Boolean(key && !key.endsWith('/') && !localKeys.has(key))) + + if (toDelete.length > 0) { + try { + await manager.storage.deleteFile(toDelete) + } catch (err) { + if (!isFileNotExistError(err)) throw err + } + } + + this.status.set(this.statusKey(ctx), 'full') + return { ms: Date.now() - started } + } + + async getRestoreStatus(ctx: LocalWorkspaceSyncContext): Promise { + return this.status.get(this.statusKey(ctx)) ?? null + } + + private async getManager(): Promise { + if (this.manager) return this.manager + const mod = await this.requireManagerNode() + const Ctor = ((mod as { default?: unknown }).default ?? mod) as ManagerCtor + if (typeof Ctor !== 'function') { + throw new ResourceError('@cloudbase/manager-node loaded but default export is not a constructor.') + } + this.manager = new Ctor({ + secretId: this.credentials.secretId, + secretKey: this.credentials.secretKey, + envId: this.credentials.envId, + ...(this.credentials.sessionToken ? { token: this.credentials.sessionToken } : {}), + region: this.credentials.region ?? 'ap-shanghai', + }) + return this.manager + } + + private async requireManagerNode(): Promise { + try { + const dynamicImport = new Function('p', 'return import(p)') as (p: string) => Promise + return await dynamicImport('@cloudbase/manager-node') + } catch { + throw new ResourceError('CloudBaseCosLocalWorkspaceStore failed to load @cloudbase/manager-node.') + } + } + + private resolvePrefix(ctx: LocalWorkspaceSyncContext): string { + return `${this.prefix}/${safeSegment(ctx.userId)}/${safeSegment(ctx.conversationId)}/` + } + + private statusKey(ctx: LocalWorkspaceSyncContext): string { + return `${ctx.envId}/${ctx.userId}/${ctx.conversationId}` + } +} + +function normalizePrefix(prefix: string): string { + return prefix.replace(/^\/+|\/+$/g, '') || DEFAULT_PREFIX +} + +function isFileNotExistError(err: unknown): boolean { + if (!err || typeof err !== 'object') return false + const e = err as { code?: string; name?: string; message?: string; statusCode?: number } + if (e.code && DELETE_NOT_EXIST_CODES.has(e.code)) return true + if (e.name && DELETE_NOT_EXIST_CODES.has(e.name)) return true + if (e.statusCode === 404) return true + if (typeof e.message === 'string' && /no such key|file.*not.*exist|nonexist/i.test(e.message)) return true + return false +} diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/engine.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/engine.ts new file mode 100644 index 0000000..3247635 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/engine.ts @@ -0,0 +1,40 @@ +import type { SandboxInstance } from '../types.js' +import type { Restored, SyncStatus } from '../workspace-snapshot/index.js' +import { SandboxRestoreFailed } from '../workspace-snapshot/index.js' +import type { LocalWorkspaceSyncEngineOptions } from './types.js' + +export class LocalWorkspaceSyncEngine { + private readonly opts: LocalWorkspaceSyncEngineOptions + private lastStatus: Restored | null = null + + constructor(opts: LocalWorkspaceSyncEngineOptions) { + this.opts = opts + } + + async bootstrap(inst: SandboxInstance, _args?: { credentials: Record }): Promise { + const workspaceRoot = inst.workspaceRoot + if (!workspaceRoot) { + throw new SandboxRestoreFailed('local workspace restore requires SandboxInstance.workspaceRoot') + } + const status = await this.opts.store.restore(this.opts.ctx, workspaceRoot) + this.lastStatus = status.restored + if (status.restored === 'failed') { + throw new SandboxRestoreFailed('local workspace restore failed', { note: status.note }) + } + return status + } + + async snapshot(inst: SandboxInstance): Promise<{ ms: number }> { + const workspaceRoot = inst.workspaceRoot + if (!workspaceRoot) { + throw new SandboxRestoreFailed('local workspace snapshot requires SandboxInstance.workspaceRoot') + } + return this.opts.store.snapshot(this.opts.ctx, workspaceRoot) + } + + async getRestoreStatus(inst: SandboxInstance): Promise { + const workspaceRoot = inst.workspaceRoot + if (!workspaceRoot) return this.lastStatus + return (await this.opts.store.getRestoreStatus?.(this.opts.ctx, workspaceRoot)) ?? this.lastStatus + } +} diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/file-utils.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/file-utils.ts new file mode 100644 index 0000000..f9ef33a --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/file-utils.ts @@ -0,0 +1,67 @@ +import * as fs from 'node:fs/promises' +import * as path from 'node:path' + +const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '.oak']) +const EXCLUDED_FILES = new Set(['.DS_Store', '.restore-in-status.json']) + +export function safeSegment(input: string): string { + const normalized = input.replace(/[^a-zA-Z0-9._-]/g, '-') + return normalized || 'default' +} + +export function assertSafeRelativePath(relPath: string): void { + if (!relPath || relPath.startsWith('/') || relPath.includes('\\')) { + throw new Error('relative path is not safe') + } + for (const segment of relPath.split('/')) { + if (!segment || segment === '.' || segment === '..') { + throw new Error('relative path contains unsafe segment') + } + } +} + +export async function listWorkspaceFiles(root: string): Promise { + const out: string[] = [] + await walk(root, '', out) + out.sort() + return out +} + +export async function copyTree(srcRoot: string, dstRoot: string): Promise { + const files = await listWorkspaceFiles(srcRoot) + await fs.mkdir(dstRoot, { recursive: true }) + for (const relPath of files) { + assertSafeRelativePath(relPath) + const src = path.join(srcRoot, relPath) + const dst = path.join(dstRoot, relPath) + await fs.mkdir(path.dirname(dst), { recursive: true }) + await fs.copyFile(src, dst) + } +} + +export async function replaceTree(srcRoot: string, dstRoot: string): Promise { + await fs.rm(dstRoot, { recursive: true, force: true }) + await fs.mkdir(dstRoot, { recursive: true }) + await copyTree(srcRoot, dstRoot) +} + +async function walk(absDir: string, relPrefix: string, out: string[]): Promise { + let entries: import('node:fs').Dirent[] + try { + entries = await fs.readdir(absDir, { withFileTypes: true }) + } catch { + return + } + + for (const entry of entries) { + const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name + const absPath = path.join(absDir, entry.name) + if (entry.isDirectory()) { + if (EXCLUDED_DIRS.has(entry.name)) continue + await walk(absPath, relPath, out) + } else if (entry.isFile()) { + if (EXCLUDED_FILES.has(entry.name)) continue + out.push(relPath) + } + } +} diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/filesystem-store.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/filesystem-store.ts new file mode 100644 index 0000000..23baef1 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/filesystem-store.ts @@ -0,0 +1,67 @@ +import * as fs from 'node:fs/promises' +import * as path from 'node:path' +import type { Restored, SyncStatus } from '../workspace-snapshot/index.js' +import { copyTree, replaceTree, safeSegment } from './file-utils.js' +import type { LocalWorkspaceSyncContext, LocalWorkspaceSyncStore } from './types.js' + +export interface FileSystemLocalWorkspaceStoreOptions { + root: string +} + +export class FileSystemLocalWorkspaceStore implements LocalWorkspaceSyncStore { + private readonly root: string + private readonly status = new Map() + + constructor(opts: FileSystemLocalWorkspaceStoreOptions) { + this.root = path.resolve(opts.root) + } + + async restore(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise { + const started = Date.now() + const remoteDir = this.resolveRemoteDir(ctx) + await fs.mkdir(workspaceRoot, { recursive: true }) + + const hasSnapshot = await exists(remoteDir) + if (hasSnapshot) { + await copyTree(remoteDir, workspaceRoot) + } + + const restored: Restored = hasSnapshot ? 'full' : 'fresh' + this.status.set(this.statusKey(ctx), restored) + return { + restored, + restoredAt: new Date().toISOString(), + restoreMs: Date.now() - started, + source: hasSnapshot ? 'cos' : 'none', + } + } + + async snapshot(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise<{ ms: number }> { + const started = Date.now() + const remoteDir = this.resolveRemoteDir(ctx) + await replaceTree(workspaceRoot, remoteDir) + this.status.set(this.statusKey(ctx), 'full') + return { ms: Date.now() - started } + } + + async getRestoreStatus(ctx: LocalWorkspaceSyncContext): Promise { + return this.status.get(this.statusKey(ctx)) ?? null + } + + private resolveRemoteDir(ctx: LocalWorkspaceSyncContext): string { + return path.join(this.root, safeSegment(ctx.envId), safeSegment(ctx.userId), safeSegment(ctx.conversationId)) + } + + private statusKey(ctx: LocalWorkspaceSyncContext): string { + return `${ctx.envId}/${ctx.userId}/${ctx.conversationId}` + } +} + +async function exists(absPath: string): Promise { + try { + await fs.access(absPath) + return true + } catch { + return false + } +} diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/index.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/index.ts new file mode 100644 index 0000000..e9632a6 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/index.ts @@ -0,0 +1,4 @@ +export { LocalWorkspaceSyncEngine } from './engine.js' +export { FileSystemLocalWorkspaceStore, type FileSystemLocalWorkspaceStoreOptions } from './filesystem-store.js' +export { CloudBaseCosLocalWorkspaceStore, type CloudBaseCosLocalWorkspaceStoreOptions } from './cloudbase-cos-store.js' +export type { LocalWorkspaceSyncContext, LocalWorkspaceSyncStore, LocalWorkspaceSyncEngineOptions } from './types.js' diff --git a/packages/open-agent-kernel/src/sandbox/local-workspace-sync/types.ts b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/types.ts new file mode 100644 index 0000000..c1c4398 --- /dev/null +++ b/packages/open-agent-kernel/src/sandbox/local-workspace-sync/types.ts @@ -0,0 +1,18 @@ +import type { Restored, SyncStatus } from '../workspace-snapshot/index.js' + +export interface LocalWorkspaceSyncContext { + envId: string + userId: string + conversationId: string +} + +export interface LocalWorkspaceSyncStore { + restore(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise + snapshot(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise<{ ms: number }> + getRestoreStatus?(ctx: LocalWorkspaceSyncContext, workspaceRoot: string): Promise +} + +export interface LocalWorkspaceSyncEngineOptions { + store: LocalWorkspaceSyncStore + ctx: LocalWorkspaceSyncContext +} diff --git a/packages/open-agent-kernel/src/sandbox/types.ts b/packages/open-agent-kernel/src/sandbox/types.ts index db81389..c33a55e 100644 --- a/packages/open-agent-kernel/src/sandbox/types.ts +++ b/packages/open-agent-kernel/src/sandbox/types.ts @@ -20,6 +20,10 @@ import type { PlatformCredentials } from '../public/types.js' export interface SandboxInstance { /** 实例唯一 ID(例:AGS InstanceId) */ readonly id: string + /** 实例后端类型(local / ags-stateful 等)。未提供时由 runtime.backend 判定。 */ + readonly backend?: string + /** local runtime 的实际工作目录。AGS 等远程 runtime 不需要提供。 */ + readonly workspaceRoot?: string /** * 在沙箱内调用一次 HTTP 接口(数据面)。 diff --git a/packages/open-agent-kernel/src/sandbox/workspace-snapshot/index.ts b/packages/open-agent-kernel/src/sandbox/workspace-snapshot/index.ts index 1e6d77d..417021d 100644 --- a/packages/open-agent-kernel/src/sandbox/workspace-snapshot/index.ts +++ b/packages/open-agent-kernel/src/sandbox/workspace-snapshot/index.ts @@ -5,4 +5,4 @@ export { SandboxRestoreTimeout, SandboxUnavailableError, } from './errors.js' -export type { SyncStatus, Restored } from './types.js' +export type { SyncStatus, Restored, WorkspaceSnapshotRuntime } from './types.js' diff --git a/packages/open-agent-kernel/src/sandbox/workspace-snapshot/types.ts b/packages/open-agent-kernel/src/sandbox/workspace-snapshot/types.ts index c96b1c5..8bb830e 100644 --- a/packages/open-agent-kernel/src/sandbox/workspace-snapshot/types.ts +++ b/packages/open-agent-kernel/src/sandbox/workspace-snapshot/types.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import type { SandboxInstance } from '../types.js' /** restored 取值,直接对应 tcb-remote-workspace cos-sync.ts:135 SyncStatus */ export const restoredEnum = z.enum(['full', 'partial', 'fresh', 'failed']) @@ -31,6 +32,12 @@ export const healthResponseSchema = z .passthrough() export type HealthResponse = z.infer +export interface WorkspaceSnapshotRuntime { + bootstrap(inst: SandboxInstance, args: { credentials: Record }): Promise + snapshot(inst: SandboxInstance): Promise<{ ms: number }> + getRestoreStatus(inst: SandboxInstance): Promise +} + /** * `POST /api/workspace/init` 真实成功响应(见 routes/api.ts:240-300)。 * 注意:**body 不含 restoreStatus** — 那个只在 `/health` 上读。 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efd50f8..684e32e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -286,15 +286,45 @@ importers: '@anthropic-ai/sdk': specifier: ^0.93.0 version: 0.93.0(zod@4.3.6) + '@cloudbase/cals': + specifier: ^1.2.25 + version: 1.2.25 + '@cloudbase/cloudbase-mcp': + specifier: ^2.21.1 + version: 2.21.1 '@cloudbase/manager-node': specifier: ^4.10.6 version: 4.10.6 '@cloudbase/node-sdk': specifier: ^3.18.1 version: 3.18.1 + '@cloudbase/toolbox': + specifier: ^0.7.19 + version: 0.7.19 '@modelcontextprotocol/sdk': specifier: ^1.29.0 version: 1.29.0(zod@4.3.6) + adm-zip: + specifier: ^0.5.17 + version: 0.5.17 + express: + specifier: ^5.2.1 + version: 5.2.1 + lockfile: + specifier: ^1.0.4 + version: 1.0.4 + open: + specifier: ^11.0.0 + version: 11.0.0 + winston: + specifier: ^3.19.0 + version: 3.19.0 + winston-daily-rotate-file: + specifier: ^5.0.0 + version: 5.0.0(winston@3.19.0) + ws: + specifier: ^8.21.0 + version: 8.21.0 zod: specifier: ^4.3.6 version: 4.3.6 @@ -438,7 +468,7 @@ importers: dependencies: '@cloudbase/js-sdk': specifier: ^2.27.2 - version: 2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.20.0) + version: 2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.21.0) '@coder/chat-core': specifier: workspace:* version: link:../chat-core @@ -786,9 +816,16 @@ packages: '@cloudbase/auth@2.27.2': resolution: {integrity: sha512-zSxRtWH5HsVi8nIyT8XByL0aOivGgqZB0YS3IO9zw6THzwUR/w/fdOrFAUDtX271htfb/dPrIRV7B/ByfUwdYQ==} + '@cloudbase/cals@1.2.25': + resolution: {integrity: sha512-jYRb6L0iJyEevl3zxl09SyQt9WLQJmI2QsmkMTsokGX2Ldgj9PZYwhdlcT7sZ6j91hmFI5OOIJsTxz55ba0UFg==} + '@cloudbase/cloud-api@0.5.5': resolution: {integrity: sha512-skNsOirrXPmQjL5NZpwfhM+kgEljYQQVetJRxOnhjeA3HVadNdJMejeXoGjOP9zua6b1YFPguVkLv+cYjw5jFg==} + '@cloudbase/cloudbase-mcp@2.21.1': + resolution: {integrity: sha512-blhgLx/gI1KJO142C58wb0yovzn71umLOXOKxkw5slB/bA4lBq2d/JTPDbXj16TfQgVWcs4Or4/Zblu4ielXxg==} + hasBin: true + '@cloudbase/cloudrun@2.27.2': resolution: {integrity: sha512-v+hnv0XN5BEfscEmCfbi6JZtFomZb7ELnyINie4OJnWLd1pHDTwYezcSja3hZUwegoLxC24pidvBAP5bI9rvtg==} @@ -861,6 +898,13 @@ packages: '@cloudbase/wx-cloud-client-sdk@1.7.1': resolution: {integrity: sha512-NzLOoYDAeoRh2lRjCtvr6yXu4KPVHqZtPxyv70e+3b2+dZIDE5S5gtU8daE7xS5u4TwPqKB/E2vmgRTYOsCS9w==} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -2459,6 +2503,9 @@ packages: resolution: {integrity: sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==} engines: {node: '>=22'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@swc/helpers@0.5.21': resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} @@ -2765,6 +2812,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2847,6 +2897,10 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + engines: {node: '>=12.0'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3075,6 +3129,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3215,9 +3273,25 @@ packages: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3570,6 +3644,14 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -3577,6 +3659,10 @@ packages: 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'} + del@5.1.0: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} engines: {node: '>=8'} @@ -3750,6 +3836,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -3974,6 +4063,9 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -3981,6 +4073,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-stream-rotator@0.6.1: + resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} + file-type@3.9.0: resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} engines: {node: '>=0.10.0'} @@ -4033,6 +4128,9 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -4400,6 +4498,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -4419,6 +4522,15 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -4479,6 +4591,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -4552,6 +4668,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-easy-filter@0.3.1: + resolution: {integrity: sha512-E+bsfF+/hgWO2+njl97thG8AAC7azX/S8OSU4VH6vzP5qQxUze39N9OUzcAn+WHT5hZwDLBUMUShU7XyDnKA5g==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -4635,6 +4754,9 @@ packages: klaw-sync@6.0.0: resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + langium@4.2.1: resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} @@ -4746,6 +4868,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lockfile@1.0.4: + resolution: {integrity: sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==} + lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -4804,6 +4929,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -5101,6 +5230,9 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} @@ -5188,6 +5320,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -5203,6 +5339,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -5213,6 +5352,10 @@ packages: oniguruma-to-es@4.3.5: resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + open@7.1.0: resolution: {integrity: sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==} engines: {node: '>=8'} @@ -5444,6 +5587,12 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-less@6.0.0: + resolution: {integrity: sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==} + engines: {node: '>=12'} + peerDependencies: + postcss: ^8.3.5 + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -5469,6 +5618,10 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -5765,6 +5918,10 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5780,6 +5937,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -5915,6 +6076,9 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -6043,6 +6207,9 @@ packages: text-encoding-shim@1.0.5: resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -6056,6 +6223,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -6109,6 +6279,10 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -6459,6 +6633,20 @@ packages: engines: {node: '>=8'} hasBin: true + winston-daily-rotate-file@5.0.0: + resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==} + engines: {node: '>=8'} + peerDependencies: + winston: ^3 + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -6486,8 +6674,8 @@ packages: utf-8-validate: optional: true - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6498,6 +6686,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -6819,7 +7011,7 @@ snapshots: '@cloudbase/types': 2.23.0 '@cloudbase/utilities': 2.23.0 - '@cloudbase/app@2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.20.0)': + '@cloudbase/app@2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.21.0)': dependencies: '@cloudbase/adapter-interface': 0.7.1 '@cloudbase/adapter-wx_mp': 1.3.1 @@ -6828,7 +7020,7 @@ snapshots: optionalDependencies: '@cloudbase/signature-nodejs': 2.2.0 jsonwebtoken: 9.0.3 - ws: 8.20.0 + ws: 8.21.0 '@cloudbase/auth@2.27.2': dependencies: @@ -6836,6 +7028,16 @@ snapshots: '@cloudbase/oauth': 2.27.2(@cloudbase/utilities@2.27.2) '@cloudbase/utilities': 2.27.2 + '@cloudbase/cals@1.2.25': + dependencies: + acorn: 8.16.0 + csstype: 3.2.3 + json-easy-filter: 0.3.1 + lodash: 4.18.1 + postcss: 8.5.8 + postcss-less: 6.0.0(postcss@8.5.8) + tinycolor2: 1.6.0 + '@cloudbase/cloud-api@0.5.5': dependencies: '@cloudbase/signature-nodejs': 2.0.0 @@ -6846,6 +7048,8 @@ snapshots: - encoding - supports-color + '@cloudbase/cloudbase-mcp@2.21.1': {} + '@cloudbase/cloudrun@2.27.2': dependencies: '@cloudbase/adapter-interface': 0.7.1 @@ -6880,13 +7084,13 @@ snapshots: '@cloudbase/types': 2.27.2 '@cloudbase/utilities': 2.27.2 - '@cloudbase/js-sdk@2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.20.0)': + '@cloudbase/js-sdk@2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.21.0)': dependencies: '@cloudbase/adapter-interface': 0.7.1 '@cloudbase/ai': 2.27.2 '@cloudbase/analytics': 2.27.2 '@cloudbase/apis': 2.27.2 - '@cloudbase/app': 2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.20.0) + '@cloudbase/app': 2.27.2(@cloudbase/signature-nodejs@2.2.0)(jsonwebtoken@9.0.3)(ws@8.21.0) '@cloudbase/auth': 2.27.2 '@cloudbase/cloudrun': 2.27.2 '@cloudbase/database': 0.11.0 @@ -6951,7 +7155,7 @@ snapshots: jsonwebtoken: 9.0.3 retry: 0.13.1 web-streams-polyfill: 4.2.0 - ws: 8.20.0 + ws: 8.21.0 xml2js: 0.6.2 transitivePeerDependencies: - bufferutil @@ -7052,6 +7256,14 @@ snapshots: dependencies: core-js-pure: 3.49.0 + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + '@drizzle-team/brocli@0.10.2': {} '@epic-web/invariant@1.0.0': {} @@ -8357,6 +8569,11 @@ snapshots: '@sindresorhus/is@8.1.0': {} + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + '@swc/helpers@0.5.21': dependencies: tslib: 2.8.1 @@ -8680,6 +8897,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': optional: true @@ -8773,6 +8992,8 @@ snapshots: address@1.2.2: {} + adm-zip@0.5.17: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -9046,6 +9267,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -9177,8 +9402,23 @@ snapshots: dependencies: color-name: 1.1.4 + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + color-name@1.1.4: {} + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -9565,6 +9805,13 @@ snapshots: deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -9575,6 +9822,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + del@5.1.0: dependencies: globby: 10.0.2 @@ -9669,6 +9918,8 @@ snapshots: emoji-regex@8.0.0: {} + enabled@2.0.0: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: @@ -10005,12 +10256,18 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fecha@4.2.3: {} + fflate@0.8.2: {} file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-stream-rotator@0.6.1: + dependencies: + moment: 2.30.1 + file-type@3.9.0: {} file-type@5.2.0: {} @@ -10067,6 +10324,8 @@ snapshots: flatted@3.4.2: {} + fn.name@1.1.0: {} + follow-redirects@1.16.0: {} for-each@0.3.5: @@ -10512,6 +10771,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extendable@0.1.1: {} is-extglob@2.1.1: {} @@ -10524,6 +10785,12 @@ snapshots: is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@1.0.0: {} is-natural-number@4.0.1: {} @@ -10560,6 +10827,10 @@ snapshots: dependencies: is-docker: 2.2.1 + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -10606,6 +10877,8 @@ snapshots: json-buffer@3.0.1: {} + json-easy-filter@0.3.1: {} + json-parse-even-better-errors@2.3.1: {} json-schema-to-ts@3.1.1: @@ -10700,6 +10973,8 @@ snapshots: dependencies: graceful-fs: 4.2.11 + kuler@2.0.0: {} + langium@4.2.1: dependencies: chevrotain: 11.1.2 @@ -10789,6 +11064,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lockfile@1.0.4: + dependencies: + signal-exit: 3.0.7 + lodash-es@4.17.23: {} lodash-es@4.18.1: {} @@ -10830,6 +11109,15 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + longest-streak@3.1.0: {} loupe@3.2.1: {} @@ -11360,6 +11648,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + moment@2.30.1: {} + monaco-editor@0.55.1: dependencies: dompurify: 3.2.7 @@ -11423,6 +11713,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -11435,6 +11727,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -11447,6 +11743,15 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + open@7.1.0: dependencies: is-docker: 2.2.1 @@ -11669,6 +11974,10 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-less@6.0.0(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 @@ -11686,6 +11995,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + powershell-utils@0.1.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -12061,6 +12372,8 @@ snapshots: transitivePeerDependencies: - supports-color + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -12075,6 +12388,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.6.0: {} @@ -12231,6 +12546,8 @@ snapshots: safer-buffer: 2.1.2 tweetnacl: 0.14.5 + stack-trace@0.0.10: {} + stackback@0.0.2: {} state-local@1.0.7: {} @@ -12406,6 +12723,8 @@ snapshots: text-encoding-shim@1.0.5: {} + text-hex@1.0.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -12418,6 +12737,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} tinyexec@1.0.4: {} @@ -12458,6 +12779,8 @@ snapshots: trim-lines@3.0.1: {} + triple-beam@1.4.1: {} + trough@2.2.0: {} ts-algebra@2.0.0: {} @@ -12821,6 +13144,34 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + winston-daily-rotate-file@5.0.0(winston@3.19.0): + dependencies: + file-stream-rotator: 0.6.1 + object-hash: 3.0.0 + triple-beam: 1.4.1 + winston: 3.19.0 + winston-transport: 4.9.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: @@ -12839,7 +13190,12 @@ snapshots: ws@7.5.10: {} - ws@8.20.0: {} + ws@8.21.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 xml2js@0.6.2: dependencies: