diff --git a/package.json b/package.json index e7db8fa4..ebf0696f 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "minimatch@9.0.3": "9.0.7", "undici": "6.24.0", "lodash": "4.18.0", - "brace-expansion": "1.1.13", + "brace-expansion@^1": "1.1.13", "@slack/bolt/path-to-regexp": "8.4.0", "handlebars": "4.7.9" } diff --git a/packages/api/package.json b/packages/api/package.json index 112fdc1c..19fd905f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -35,6 +35,8 @@ "dependencies": { "@anthropic-ai/sdk": "^0.71.2", "@inklabs/shared": "workspace:*", + "@mariozechner/pi-agent-core": "0.71.1", + "@mariozechner/pi-coding-agent": "0.71.1", "@modelcontextprotocol/sdk": "^1.26.0", "@slack/bolt": "^4.6.0", "@supabase/supabase-js": "^2.39.3", diff --git a/packages/api/src/agent/adapters/index.ts b/packages/api/src/agent/adapters/index.ts index d9dc09a7..fd65d413 100644 --- a/packages/api/src/agent/adapters/index.ts +++ b/packages/api/src/agent/adapters/index.ts @@ -45,7 +45,7 @@ export function getAdapterRegistry(): AdapterRegistry { registryInstance.register(getClaudeCodeAdapter()); // Future adapters would be registered here: - // registryInstance.register(getDirectApiAdapter()); + // registryInstance.register(getInkAdapter()); // registryInstance.register(getOpenAiAdapter()); } return registryInstance; diff --git a/packages/api/src/agent/adapters/types.ts b/packages/api/src/agent/adapters/types.ts index 9c7ce284..6ed219f2 100644 --- a/packages/api/src/agent/adapters/types.ts +++ b/packages/api/src/agent/adapters/types.ts @@ -30,7 +30,7 @@ export interface BackendPermissionConfig { * Implement this for each backend to translate PCP permissions */ export interface PermissionAdapter { - /** Backend identifier (e.g., 'claude-code', 'direct-api') */ + /** Backend identifier (e.g., 'claude-code', 'ink') */ readonly backendId: string; /** diff --git a/packages/api/src/agent/backend-manager.ts b/packages/api/src/agent/backend-manager.ts index 0abab7a4..3c77cedc 100644 --- a/packages/api/src/agent/backend-manager.ts +++ b/packages/api/src/agent/backend-manager.ts @@ -16,9 +16,9 @@ import type { ResponseHandler, } from './types'; import type { ClaudeCodeConfig } from './backends/claude-code.backend'; -import type { DirectApiConfig } from './backends/direct-api.backend'; +import type { InkConfig } from './backends/ink.backend'; import { ClaudeCodeBackend, createClaudeCodeBackend } from './backends/claude-code.backend'; -import { DirectApiBackend, createDirectApiBackend } from './backends/direct-api.backend'; +import { InkBackend, createInkBackend } from './backends/ink.backend'; export interface BackendManagerConfig { /** Primary backend to use */ @@ -28,7 +28,7 @@ export interface BackendManagerConfig { /** Backend-specific configurations */ backends: { 'claude-code'?: Partial; - 'direct-api'?: Partial; + ink?: Partial; }; /** Enable automatic failover */ enableFailover?: boolean; @@ -138,10 +138,9 @@ export class BackendManager extends EventEmitter { setResponseHandler(handler: ResponseHandler): void { this.responseHandler = handler; - // Pass to direct API backend if it exists - const directApi = this.backends.get('direct-api') as DirectApiBackend | undefined; - if (directApi) { - directApi.setResponseHandler(handler); + const inkBackend = this.backends.get('ink') as InkBackend | undefined; + if (inkBackend) { + inkBackend.setResponseHandler(handler); } } @@ -228,14 +227,14 @@ export class BackendManager extends EventEmitter { this.backends.set('claude-code', backend); } - // Create Direct API backend - if (this.config.backends['direct-api'] || this.config.primaryBackend === 'direct-api') { - const backend = createDirectApiBackend(this.config.backends['direct-api']); + // Create Ink backend + if (this.config.backends.ink || this.config.primaryBackend === 'ink') { + const backend = createInkBackend(this.config.backends.ink); this.setupBackendEvents(backend); if (this.responseHandler) { backend.setResponseHandler(this.responseHandler); } - this.backends.set('direct-api', backend); + this.backends.set('ink', backend); } } diff --git a/packages/api/src/agent/backends/direct-api.backend.ts b/packages/api/src/agent/backends/ink.backend.ts similarity index 84% rename from packages/api/src/agent/backends/direct-api.backend.ts rename to packages/api/src/agent/backends/ink.backend.ts index 72abe0f9..3938af74 100644 --- a/packages/api/src/agent/backends/direct-api.backend.ts +++ b/packages/api/src/agent/backends/ink.backend.ts @@ -1,5 +1,5 @@ /** - * Direct API Backend + * Ink Backend * * Uses the Anthropic API directly instead of Claude Code CLI. * Useful for cloud deployments where CLI isn't available. @@ -17,8 +17,8 @@ import type { ResponseHandler, } from '../types'; -export interface DirectApiConfig extends BackendConfig { - type: 'direct-api'; +export interface InkConfig extends BackendConfig { + type: 'ink'; apiKey?: string; model?: string; maxTokens?: number; @@ -30,15 +30,15 @@ interface ConversationMessage { content: string; } -const DEFAULT_CONFIG: Partial = { +const DEFAULT_CONFIG: Partial = { model: 'claude-sonnet-4-20250514', maxTokens: 4096, }; -export class DirectApiBackend extends EventEmitter implements AgentBackend { - readonly type: BackendType = 'direct-api'; +export class InkBackend extends EventEmitter implements AgentBackend { + readonly type: BackendType = 'ink'; - private config: DirectApiConfig; + private config: InkConfig; private client: Anthropic | null = null; private ready = false; private messageCount = 0; @@ -55,9 +55,9 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { // MCP tools definition (simplified for API calls) private tools: Anthropic.Tool[] = []; - constructor(config: Partial = {}) { + constructor(config: Partial = {}) { super(); - this.config = { ...DEFAULT_CONFIG, ...config, type: 'direct-api' } as DirectApiConfig; + this.config = { ...DEFAULT_CONFIG, ...config, type: 'ink' } as InkConfig; } /** @@ -76,28 +76,28 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { async initialize(): Promise { if (this.client) { - logger.warn('Direct API backend already initialized'); + logger.warn('Ink backend already initialized'); return; } - logger.info('Initializing Direct API backend...'); + logger.info('Initializing Ink backend...'); const apiKey = this.config.apiKey || process.env.ANTHROPIC_API_KEY; if (!apiKey) { - throw new Error('ANTHROPIC_API_KEY is required for Direct API backend'); + throw new Error('ANTHROPIC_API_KEY is required for Ink backend'); } this.client = new Anthropic({ apiKey }); - this.sessionId = `direct-api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + this.sessionId = `ink-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; this.startTime = new Date(); this.ready = true; this.emit('ready'); - logger.info('Direct API backend ready', { sessionId: this.sessionId }); + logger.info('Ink backend ready', { sessionId: this.sessionId }); } async shutdown(): Promise { - logger.info('Shutting down Direct API backend...'); + logger.info('Shutting down Ink backend...'); this.client = null; this.ready = false; this.conversationHistory.clear(); @@ -106,7 +106,7 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { async sendMessage(message: AgentMessage): Promise { if (!this.client || !this.ready) { - throw new Error('Direct API backend not ready'); + throw new Error('Ink backend not ready'); } this.messageCount++; @@ -121,7 +121,7 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { }); logger.info( - `Sending message via Direct API [${message.channel}]: ${message.content.substring(0, 100)}...` + `Sending message via Ink [${message.channel}]: ${message.content.substring(0, 100)}...` ); try { @@ -138,7 +138,7 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { await this.processResponse(response, message); } catch (error) { this.lastError = error instanceof Error ? error.message : 'Unknown error'; - logger.error('Direct API error:', error); + logger.error('Ink error:', error); this.emit('error', error); throw error; } @@ -164,7 +164,7 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { } async resumeSession(sessionId: string): Promise { - // Direct API doesn't have true session resumption + // Ink doesn't have true session resumption // but we can set the session ID for tracking this.sessionId = sessionId; return true; @@ -293,8 +293,8 @@ export class DirectApiBackend extends EventEmitter implements AgentBackend { } /** - * Create a Direct API backend instance + * Create a Ink backend instance */ -export function createDirectApiBackend(config?: Partial): DirectApiBackend { - return new DirectApiBackend(config); +export function createInkBackend(config?: Partial): InkBackend { + return new InkBackend(config); } diff --git a/packages/api/src/agent/index.ts b/packages/api/src/agent/index.ts index 1099c1cc..e08fc47d 100644 --- a/packages/api/src/agent/index.ts +++ b/packages/api/src/agent/index.ts @@ -11,8 +11,16 @@ export * from './types'; export { ClaudeCodeBackend, createClaudeCodeBackend } from './backends/claude-code.backend'; export type { ClaudeCodeConfig } from './backends/claude-code.backend'; -export { DirectApiBackend, createDirectApiBackend } from './backends/direct-api.backend'; -export type { DirectApiConfig } from './backends/direct-api.backend'; +export { InkBackend, createInkBackend } from './backends/ink.backend'; +export type { InkConfig } from './backends/ink.backend'; + +// Tools +export { + createInkCodingTools, + getPiToolSchemas, + createPiToolExecutor, +} from './tools/pi-coding-tools'; +export type { InkToolDefinition, PiCodingToolsConfig } from './tools/pi-coding-tools'; // Manager export { BackendManager, createBackendManager } from './backend-manager'; diff --git a/packages/api/src/agent/tools/bash-guard.test.ts b/packages/api/src/agent/tools/bash-guard.test.ts new file mode 100644 index 00000000..dc5080de --- /dev/null +++ b/packages/api/src/agent/tools/bash-guard.test.ts @@ -0,0 +1,451 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + analyzeCommand, + ProcessRegistry, + extractBackgroundPids, + guardBashCommand, + getProcessRegistry, + resetProcessRegistry, +} from './bash-guard'; + +vi.mock('../../utils/logger', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, +})); + +describe('Bash Guard', () => { + describe('analyzeCommand', () => { + describe('fork bombs', () => { + it('blocks classic fork bomb', () => { + const result = analyzeCommand(':(){ :|:& };:'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('fork bomb'); + }); + + it('blocks named fork bomb', () => { + const result = analyzeCommand('bomb(){ bomb|bomb& };bomb'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('fork bomb'); + }); + + it('blocks fork bomb with spaces', () => { + const result = analyzeCommand('x(){ x | x & };x'); + expect(result.blocked).toBe(true); + }); + + it('allows function definitions that are not fork bombs', () => { + const result = analyzeCommand('greet(){ echo "hello"; }'); + expect(result.blocked).toBe(false); + }); + }); + + describe('recursive root delete', () => { + it('blocks rm -rf /', () => { + expect(analyzeCommand('rm -rf /').blocked).toBe(true); + expect(analyzeCommand('rm -rf /').reason).toContain('recursive delete'); + }); + + it('blocks rm -r -f /', () => { + expect(analyzeCommand('rm -r -f /').blocked).toBe(true); + }); + + it('blocks rm --recursive /', () => { + expect(analyzeCommand('rm --recursive /').blocked).toBe(true); + }); + + it('blocks rm -rf /*', () => { + expect(analyzeCommand('rm -rf /*').blocked).toBe(true); + }); + + it('blocks sudo rm -rf /', () => { + expect(analyzeCommand('sudo rm -rf /').blocked).toBe(true); + }); + + it('blocks rm -rf / after semicolon', () => { + expect(analyzeCommand('echo hi; rm -rf /').blocked).toBe(true); + }); + + it('allows rm -rf /tmp/foo', () => { + expect(analyzeCommand('rm -rf /tmp/foo').blocked).toBe(false); + }); + + it('allows rm without recursive flag', () => { + expect(analyzeCommand('rm /tmp/file.txt').blocked).toBe(false); + }); + + it('allows rm -r on non-root paths', () => { + expect(analyzeCommand('rm -r ./build').blocked).toBe(false); + }); + }); + + describe('dangerous patterns', () => { + it('blocks mkfs', () => { + expect(analyzeCommand('mkfs.ext4 /dev/sda1').blocked).toBe(true); + expect(analyzeCommand('mkfs /dev/sda').blocked).toBe(true); + }); + + it('blocks dd to block devices', () => { + expect(analyzeCommand('dd if=/dev/zero of=/dev/sda').blocked).toBe(true); + expect(analyzeCommand('dd if=/dev/zero of=/dev/nvme0n1').blocked).toBe(true); + }); + + it('allows dd to regular files', () => { + expect(analyzeCommand('dd if=/dev/zero of=/tmp/image.iso bs=1M count=100').blocked).toBe( + false + ); + }); + + it('blocks shutdown', () => { + expect(analyzeCommand('shutdown -h now').blocked).toBe(true); + expect(analyzeCommand('shutdown').blocked).toBe(true); + }); + + it('blocks reboot', () => { + expect(analyzeCommand('reboot').blocked).toBe(true); + }); + + it('blocks poweroff', () => { + expect(analyzeCommand('poweroff').blocked).toBe(true); + }); + + it('blocks init 0 and init 6', () => { + expect(analyzeCommand('init 0').blocked).toBe(true); + expect(analyzeCommand('init 6').blocked).toBe(true); + }); + + it('allows init with other arguments', () => { + expect(analyzeCommand('init 3').blocked).toBe(false); + }); + + it('blocks nsenter', () => { + expect(analyzeCommand('nsenter --target 1 --mount').blocked).toBe(true); + }); + }); + + describe('kill commands', () => { + it('detects kill as a kill command', () => { + const result = analyzeCommand('kill 1234'); + expect(result.isKillCommand).toBe(true); + expect(result.killPidTargets).toEqual([1234]); + }); + + it('extracts multiple PID targets', () => { + const result = analyzeCommand('kill -9 1234 5678'); + expect(result.killPidTargets).toEqual([1234, 5678]); + }); + + it('blocks kill -1 (all processes)', () => { + const result = analyzeCommand('kill -1'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('all processes'); + }); + + it('blocks kill targeting PID 1', () => { + const result = analyzeCommand('kill 1'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('PID 1'); + }); + + it('blocks kill -9 1', () => { + const result = analyzeCommand('kill -9 1'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('PID 1'); + }); + + it('does not block kill of high PIDs', () => { + const result = analyzeCommand('kill -9 12345'); + expect(result.blocked).toBe(false); + expect(result.isKillCommand).toBe(true); + expect(result.killPidTargets).toEqual([12345]); + }); + + it('handles kill after -- separator', () => { + const result = analyzeCommand('kill -- 4567'); + expect(result.killPidTargets).toEqual([4567]); + }); + + it('blocks pkill (name-based, cannot verify ownership)', () => { + const result = analyzeCommand('pkill -f node'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('pkill/killall'); + }); + + it('blocks killall (name-based, cannot verify ownership)', () => { + const result = analyzeCommand('killall node'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('pkill/killall'); + }); + + it('handles multiple kill commands in one line', () => { + const result = analyzeCommand('kill 100; kill 200'); + expect(result.killPidTargets).toContain(100); + expect(result.killPidTargets).toContain(200); + }); + + it('blocks kill 0 (current process group)', () => { + const result = analyzeCommand('kill 0'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('process group'); + }); + + it('blocks kill with negative PID (process group target)', () => { + const result = analyzeCommand('kill -- -1234'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('process group'); + }); + + it('blocks kill $PPID (unresolvable variable target)', () => { + const result = analyzeCommand('kill $PPID'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('variable/dynamic'); + }); + + it('blocks kill $(cat /tmp/pid) (unresolvable command substitution)', () => { + const result = analyzeCommand('kill $(cat /tmp/pid)'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('variable/dynamic'); + }); + + it('blocks kill with backtick substitution', () => { + const result = analyzeCommand('kill `pgrep node`'); + expect(result.blocked).toBe(true); + expect(result.reason).toContain('variable/dynamic'); + }); + }); + + describe('safe commands', () => { + it('allows echo', () => { + expect(analyzeCommand('echo hello').blocked).toBe(false); + expect(analyzeCommand('echo hello').isKillCommand).toBe(false); + }); + + it('allows ls', () => { + expect(analyzeCommand('ls -la').blocked).toBe(false); + }); + + it('allows cat', () => { + expect(analyzeCommand('cat /etc/hostname').blocked).toBe(false); + }); + + it('allows git commands', () => { + expect(analyzeCommand('git status').blocked).toBe(false); + expect(analyzeCommand('git commit -m "fix"').blocked).toBe(false); + }); + + it('allows npm/yarn', () => { + expect(analyzeCommand('npm install').blocked).toBe(false); + expect(analyzeCommand('yarn build').blocked).toBe(false); + }); + + it('allows piped commands', () => { + expect(analyzeCommand('ps aux | grep node').blocked).toBe(false); + }); + + it('allows compound commands', () => { + expect(analyzeCommand('cd /tmp && ls -la').blocked).toBe(false); + }); + }); + }); + + describe('ProcessRegistry', () => { + let registry: ProcessRegistry; + + beforeEach(() => { + registry = new ProcessRegistry(); + }); + + it('registers and retrieves PIDs', () => { + registry.register('wren', 1234, 'sleep 100'); + expect(registry.has(1234)).toBe(true); + expect(registry.isOwned('wren', 1234)).toBe(true); + expect(registry.getOwner(1234)).toBe('wren'); + }); + + it('tracks PIDs per agent', () => { + registry.register('wren', 100, 'sleep 100'); + registry.register('wren', 200, 'node server.js'); + registry.register('lumen', 300, 'python main.py'); + + expect(registry.getAgentPids('wren')).toEqual([100, 200]); + expect(registry.getAgentPids('lumen')).toEqual([300]); + }); + + it('rejects ownership checks for wrong agent', () => { + registry.register('wren', 1234, 'sleep 100'); + expect(registry.isOwned('lumen', 1234)).toBe(false); + }); + + it('removes entries', () => { + registry.register('wren', 1234, 'sleep 100'); + registry.remove(1234); + expect(registry.has(1234)).toBe(false); + expect(registry.size).toBe(0); + }); + + it('clears all entries', () => { + registry.register('wren', 100, 'a'); + registry.register('lumen', 200, 'b'); + registry.clear(); + expect(registry.size).toBe(0); + }); + + it('removes dead processes on cleanup', () => { + // PID 99999999 almost certainly doesn't exist + registry.register('wren', 99999999, 'ghost'); + expect(registry.size).toBe(1); + registry.cleanup(); + expect(registry.size).toBe(0); + }); + + it('keeps alive processes on cleanup', () => { + // Current process is alive + registry.register('wren', process.pid, 'self'); + registry.cleanup(); + expect(registry.has(process.pid)).toBe(true); + }); + + it('removes expired entries on cleanup', () => { + const shortTtl = new ProcessRegistry(1); // 1ms TTL + shortTtl.register('wren', process.pid, 'self'); + + // Wait for TTL to expire + const start = Date.now(); + while (Date.now() - start < 5) { + /* spin */ + } + + shortTtl.cleanup(); + expect(shortTtl.size).toBe(0); + }); + }); + + describe('extractBackgroundPids', () => { + it('extracts PID from bash background output', () => { + expect(extractBackgroundPids('[1] 12345')).toEqual([12345]); + }); + + it('extracts multiple PIDs', () => { + const output = '[1] 12345\n[2] 67890'; + expect(extractBackgroundPids(output)).toEqual([12345, 67890]); + }); + + it('handles PIDs mixed with other output', () => { + const output = 'Starting server...\n[1] 54321\nListening on port 3000'; + expect(extractBackgroundPids(output)).toEqual([54321]); + }); + + it('returns empty for no background PIDs', () => { + expect(extractBackgroundPids('hello world')).toEqual([]); + expect(extractBackgroundPids('')).toEqual([]); + }); + + it('handles job completion messages', () => { + const output = '[1] 12345\n[1]+ Done sleep 1'; + expect(extractBackgroundPids(output)).toEqual([12345]); + }); + }); + + describe('guardBashCommand', () => { + beforeEach(() => { + resetProcessRegistry(); + }); + + afterEach(() => { + resetProcessRegistry(); + }); + + describe('dangerous command blocking', () => { + it('blocks fork bombs', () => { + const result = guardBashCommand(':(){ :|:& };:', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('fork bomb'); + }); + + it('blocks rm -rf /', () => { + const result = guardBashCommand('rm -rf /', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + }); + + it('allows safe commands', () => { + const result = guardBashCommand('echo hello', { agentId: 'wren' }); + expect(result.allowed).toBe(true); + }); + + it('can be disabled', () => { + const result = guardBashCommand(':(){ :|:& };:', { + agentId: 'wren', + blockDangerousCommands: false, + }); + expect(result.allowed).toBe(true); + }); + }); + + describe('kill scope enforcement', () => { + it('blocks kill targeting unregistered PIDs', () => { + const result = guardBashCommand('kill 1234', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('not owned by this agent'); + }); + + it('allows kill targeting own PIDs', () => { + const registry = getProcessRegistry(); + registry.register('wren', 1234, 'sleep 100'); + + const result = guardBashCommand('kill 1234', { agentId: 'wren' }); + expect(result.allowed).toBe(true); + }); + + it('blocks kill targeting another agent PIDs', () => { + const registry = getProcessRegistry(); + registry.register('lumen', 1234, 'sleep 100'); + + const result = guardBashCommand('kill 1234', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('1234'); + }); + + it('allows kill with mixed PIDs when all are owned', () => { + const registry = getProcessRegistry(); + registry.register('wren', 100, 'a'); + registry.register('wren', 200, 'b'); + + const result = guardBashCommand('kill 100 200', { agentId: 'wren' }); + expect(result.allowed).toBe(true); + }); + + it('blocks when any target PID is not owned', () => { + const registry = getProcessRegistry(); + registry.register('wren', 100, 'a'); + + const result = guardBashCommand('kill 100 200', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('200'); + }); + + it('blocks pkill (name-based, fail-closed)', () => { + const result = guardBashCommand('pkill -f "old-server"', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('pkill/killall'); + }); + + it('blocks kill with variable expansion', () => { + const result = guardBashCommand('kill $PPID', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('variable/dynamic'); + }); + + it('blocks kill 0 (process group)', () => { + const result = guardBashCommand('kill 0', { agentId: 'wren' }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain('process group'); + }); + + it('can be disabled', () => { + const result = guardBashCommand('kill 1234', { + agentId: 'wren', + enforceKillScope: false, + }); + expect(result.allowed).toBe(true); + }); + }); + }); +}); diff --git a/packages/api/src/agent/tools/bash-guard.ts b/packages/api/src/agent/tools/bash-guard.ts new file mode 100644 index 00000000..f12e3eda --- /dev/null +++ b/packages/api/src/agent/tools/bash-guard.ts @@ -0,0 +1,339 @@ +/** + * Bash Process Guard + * + * Defense-in-depth for bash command execution. NOT a substitute for + * container isolation — catches common dangerous patterns and provides + * multi-agent process boundaries. + * + * Three layers: + * 1. Static analysis — block catastrophic commands pre-execution + * 2. Kill scope enforcement — prevent cross-agent process termination + * 3. Process registry — track background PIDs per agent + */ + +import { logger } from '../../utils/logger'; + +// --- Types --- + +export interface CommandAnalysis { + blocked: boolean; + reason?: string; + isKillCommand: boolean; + killPidTargets: number[]; +} + +export interface BashGuardConfig { + agentId: string; + /** Block catastrophic commands before execution (default: true) */ + blockDangerousCommands?: boolean; + /** Enforce kill targeting only agent-owned PIDs (default: true) */ + enforceKillScope?: boolean; +} + +export interface GuardResult { + allowed: boolean; + reason?: string; +} + +// --- Dangerous command detection --- + +// Fork bomb: function that pipes to itself with backgrounding +// Catches :(){ :|:& };: and named variants like bomb(){ bomb|bomb& };bomb +const FORK_BOMB_RE = /([\w:.]+)\(\)\s*\{\s*\1\s*\|\s*\1\s*&/; + +interface DangerousPattern { + pattern: RegExp; + reason: string; +} + +const DANGEROUS_PATTERNS: DangerousPattern[] = [ + { pattern: FORK_BOMB_RE, reason: 'fork bomb detected' }, + { pattern: /\bmkfs(?:\.\w+)?\s/, reason: 'filesystem format' }, + { pattern: /\bdd\b[^;|&]*\bof=\/dev\/(?:sd|hd|nvme|vd)/, reason: 'raw disk write' }, + { pattern: /\bshutdown\b/, reason: 'system shutdown' }, + { pattern: /\breboot\b/, reason: 'system reboot' }, + { pattern: /\bpoweroff\b/, reason: 'system poweroff' }, + { pattern: /\binit\s+[06]\b/, reason: 'init level change' }, + { pattern: /\bnsenter\s/, reason: 'namespace entry' }, +]; + +function isRecursiveRootDelete(command: string): boolean { + const segments = command.split(/[;|&]+/); + for (const segment of segments) { + if (!/\brm\b/.test(segment)) continue; + if (!/-[a-zA-Z]*r/.test(segment) && !/--recursive/.test(segment)) continue; + const tokens = segment.trim().split(/\s+/); + const rmIdx = tokens.findIndex((t) => t === 'rm'); + if (rmIdx === -1) continue; + const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith('-')); + if (pathArgs.some((t) => t === '/' || t === '/*')) return true; + } + return false; +} + +// --- Kill command analysis --- + +function analyzeKillCommands(command: string): { + found: boolean; + allProcesses: boolean; + initProcess: boolean; + processGroup: boolean; + hasUnresolvableTargets: boolean; + pids: number[]; +} { + const result = { + found: false, + allProcesses: false, + initProcess: false, + processGroup: false, + hasUnresolvableTargets: false, + pids: [] as number[], + }; + + const matches = [...command.matchAll(/\bkill\s+([^;|&\n]+)/g)]; + if (matches.length === 0) return result; + result.found = true; + + for (const m of matches) { + const args = m[1].trim(); + + // -1 as a PID argument targets all processes + if (/(?:^|\s)-1(?:\s|$)/.test(args)) { + result.allProcesses = true; + } + + // Extract numeric PID targets (non-flag tokens after --) + // or non-flag tokens that are pure numbers + const tokens = args.split(/\s+/); + const dashDashIdx = tokens.indexOf('--'); + const candidates = + dashDashIdx >= 0 ? tokens.slice(dashDashIdx + 1) : tokens.filter((t) => !t.startsWith('-')); + + for (const t of candidates) { + // Variable expansion or command substitution — can't resolve statically + if (/\$/.test(t) || /`/.test(t)) { + result.hasUnresolvableTargets = true; + continue; + } + const n = parseInt(t, 10); + if (isNaN(n)) continue; + if (n === 0) { + result.processGroup = true; + } else if (n < 0) { + result.processGroup = true; + } else { + result.pids.push(n); + } + } + + // Also check for negative PIDs after -- (process group targets) + if (dashDashIdx >= 0) { + for (const t of tokens.slice(dashDashIdx + 1)) { + if (/^-\d+$/.test(t) && t !== '-1') { + result.processGroup = true; + } + } + } + } + + result.initProcess = result.pids.includes(1); + return result; +} + +// --- Public API --- + +export function analyzeCommand(command: string): CommandAnalysis { + const base: CommandAnalysis = { blocked: false, isKillCommand: false, killPidTargets: [] }; + + if (isRecursiveRootDelete(command)) { + return { ...base, blocked: true, reason: 'Blocked: recursive delete at filesystem root' }; + } + + for (const { pattern, reason } of DANGEROUS_PATTERNS) { + if (pattern.test(command)) { + return { ...base, blocked: true, reason: `Blocked: ${reason}` }; + } + } + + const kill = analyzeKillCommands(command); + const hasPkillKillall = /\b(pkill|killall)\b/.test(command); + + if (kill.allProcesses) { + return { + blocked: true, + reason: 'Blocked: kill -1 targets all processes', + isKillCommand: true, + killPidTargets: kill.pids, + }; + } + + if (kill.initProcess) { + return { + blocked: true, + reason: 'Blocked: cannot signal PID 1 (init process)', + isKillCommand: true, + killPidTargets: kill.pids, + }; + } + + if (kill.processGroup) { + return { + blocked: true, + reason: 'Blocked: kill targeting process group (PID 0 or negative PID)', + isKillCommand: true, + killPidTargets: kill.pids, + }; + } + + if (kill.hasUnresolvableTargets) { + return { + blocked: true, + reason: 'Blocked: kill with variable/dynamic PID target — cannot verify ownership', + isKillCommand: true, + killPidTargets: kill.pids, + }; + } + + if (hasPkillKillall) { + return { + blocked: true, + reason: 'Blocked: pkill/killall target by name — cannot verify process ownership', + isKillCommand: true, + killPidTargets: [], + }; + } + + return { + blocked: false, + isKillCommand: kill.found, + killPidTargets: kill.pids, + }; +} + +// --- Process Registry --- + +interface ProcessEntry { + pid: number; + agentId: string; + command: string; + registeredAt: number; +} + +const DEFAULT_TTL_MS = 30 * 60 * 1000; + +export class ProcessRegistry { + private entries = new Map(); + private ttlMs: number; + + constructor(ttlMs = DEFAULT_TTL_MS) { + this.ttlMs = ttlMs; + } + + register(agentId: string, pid: number, command: string): void { + this.entries.set(pid, { pid, agentId, command, registeredAt: Date.now() }); + } + + isOwned(agentId: string, pid: number): boolean { + const entry = this.entries.get(pid); + return !!entry && entry.agentId === agentId; + } + + getOwner(pid: number): string | undefined { + return this.entries.get(pid)?.agentId; + } + + getAgentPids(agentId: string): number[] { + return [...this.entries.values()].filter((e) => e.agentId === agentId).map((e) => e.pid); + } + + has(pid: number): boolean { + return this.entries.has(pid); + } + + remove(pid: number): void { + this.entries.delete(pid); + } + + cleanup(): void { + const now = Date.now(); + for (const [pid, entry] of this.entries) { + if (now - entry.registeredAt > this.ttlMs) { + this.entries.delete(pid); + continue; + } + try { + process.kill(pid, 0); + } catch { + this.entries.delete(pid); + } + } + } + + get size(): number { + return this.entries.size; + } + + clear(): void { + this.entries.clear(); + } +} + +let _registry: ProcessRegistry | undefined; + +export function getProcessRegistry(): ProcessRegistry { + if (!_registry) _registry = new ProcessRegistry(); + return _registry; +} + +export function resetProcessRegistry(): void { + _registry = undefined; +} + +// --- Background PID extraction --- + +export function extractBackgroundPids(output: string): number[] { + const re = /\[\d+\]\s+(\d+)/g; + const pids: number[] = []; + let match; + while ((match = re.exec(output)) !== null) { + const pid = parseInt(match[1], 10); + if (pid > 0) pids.push(pid); + } + return pids; +} + +// --- Guard --- + +export function guardBashCommand(command: string, config: BashGuardConfig): GuardResult { + const analysis = analyzeCommand(command); + + if (config.blockDangerousCommands !== false && analysis.blocked) { + logger.warn('Bash guard blocked command', { + agentId: config.agentId, + command: command.substring(0, 200), + reason: analysis.reason, + }); + return { allowed: false, reason: analysis.reason }; + } + + if ( + config.enforceKillScope !== false && + analysis.isKillCommand && + analysis.killPidTargets.length > 0 + ) { + const registry = getProcessRegistry(); + const unauthorized = analysis.killPidTargets.filter( + (pid) => !registry.isOwned(config.agentId, pid) + ); + if (unauthorized.length > 0) { + const reason = `Blocked: cannot signal PIDs not owned by this agent: [${unauthorized.join(', ')}]`; + logger.warn('Bash guard blocked kill', { + agentId: config.agentId, + unauthorizedPids: unauthorized, + }); + return { allowed: false, reason }; + } + } + + return { allowed: true }; +} diff --git a/packages/api/src/agent/tools/pi-coding-tools.test.ts b/packages/api/src/agent/tools/pi-coding-tools.test.ts new file mode 100644 index 00000000..fbd30026 --- /dev/null +++ b/packages/api/src/agent/tools/pi-coding-tools.test.ts @@ -0,0 +1,260 @@ +import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'; +import path from 'path'; +import { mkdtempSync, writeFileSync, mkdirSync, readFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { createInkCodingTools, type InkToolDefinition } from './pi-coding-tools'; +import { resetProcessRegistry, getProcessRegistry } from './bash-guard'; + +vi.mock('../../utils/logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, +})); + +describe('Pi Coding Tools Adapter', () => { + let testDir: string; + let tools: InkToolDefinition[]; + + beforeAll(async () => { + testDir = mkdtempSync(path.join(tmpdir(), 'pi-tools-test-')); + writeFileSync(path.join(testDir, 'hello.txt'), 'Hello, world!\n'); + mkdirSync(path.join(testDir, 'subdir')); + writeFileSync(path.join(testDir, 'subdir', 'nested.txt'), 'nested content\n'); + + tools = await createInkCodingTools({ cwd: testDir }); + }); + + it('loads all 7 coding tools', () => { + const names = tools.map((t) => t.schema.name); + expect(names).toContain('read'); + expect(names).toContain('write'); + expect(names).toContain('edit'); + expect(names).toContain('bash'); + expect(names).toContain('grep'); + expect(names).toContain('find'); + expect(names).toContain('ls'); + expect(names.length).toBe(7); + }); + + it('generates valid Anthropic tool schemas', () => { + for (const tool of tools) { + expect(tool.schema.name).toBeTruthy(); + expect(tool.schema.input_schema).toBeDefined(); + expect(tool.schema.input_schema.type).toBe('object'); + } + }); + + describe('read tool', () => { + it('reads files within workspace', async () => { + const readTool = tools.find((t) => t.schema.name === 'read')!; + const result = await readTool.execute({ path: 'hello.txt' }); + expect(result).toContain('Hello, world!'); + }); + + it('reads nested files', async () => { + const readTool = tools.find((t) => t.schema.name === 'read')!; + const result = await readTool.execute({ path: 'subdir/nested.txt' }); + expect(result).toContain('nested content'); + }); + }); + + describe('write tool', () => { + it('writes a file within workspace', async () => { + const writeTool = tools.find((t) => t.schema.name === 'write')!; + await writeTool.execute({ path: 'output.txt', content: 'written by test\n' }); + const content = readFileSync(path.join(testDir, 'output.txt'), 'utf-8'); + expect(content).toBe('written by test\n'); + }); + }); + + describe('bash tool', () => { + it('runs safe commands', async () => { + const bashTool = tools.find((t) => t.schema.name === 'bash')!; + const result = await bashTool.execute({ command: 'echo hello' }); + expect(result).toContain('hello'); + }); + + it('can list files', async () => { + const bashTool = tools.find((t) => t.schema.name === 'bash')!; + const result = await bashTool.execute({ command: 'ls' }); + expect(result).toContain('hello.txt'); + expect(result).toContain('subdir'); + }); + + it('can read files with cat', async () => { + const bashTool = tools.find((t) => t.schema.name === 'bash')!; + const result = await bashTool.execute({ command: 'cat hello.txt' }); + expect(result).toContain('Hello, world!'); + }); + + it('reports pwd as the workspace directory', async () => { + const bashTool = tools.find((t) => t.schema.name === 'bash')!; + const result = await bashTool.execute({ command: 'pwd' }); + expect(result).toContain(testDir); + }); + }); + + describe('grep tool', () => { + it('finds text in files', async () => { + const grepTool = tools.find((t) => t.schema.name === 'grep')!; + const result = await grepTool.execute({ pattern: 'Hello', path: '.' }); + expect(result).toContain('hello.txt'); + }); + }); + + describe('find tool', () => { + it('finds files by name', async () => { + const findTool = tools.find((t) => t.schema.name === 'find')!; + const result = await findTool.execute({ pattern: '*.txt', path: '.' }); + expect(result).toContain('hello.txt'); + expect(result).toContain('nested.txt'); + }); + }); + + describe('ls tool', () => { + it('lists directory contents', async () => { + const lsTool = tools.find((t) => t.schema.name === 'ls')!; + const result = await lsTool.execute({ path: '.' }); + expect(result).toContain('hello.txt'); + expect(result).toContain('subdir'); + }); + }); + + describe('workspace root enforcement', () => { + it('blocks absolute path escape', async () => { + const readTool = tools.find((t) => t.schema.name === 'read')!; + const result = await readTool.execute({ path: '/etc/hostname' }); + expect(result).toContain('Access denied'); + expect(result).toContain('outside workspace root'); + }); + + it('blocks relative path traversal', async () => { + const readTool = tools.find((t) => t.schema.name === 'read')!; + const result = await readTool.execute({ path: '../../etc/hostname' }); + expect(result).toContain('Access denied'); + }); + + it('can be disabled', async () => { + const unenforced = await createInkCodingTools({ + cwd: testDir, + enforceWorkspaceRoot: false, + include: ['read'], + }); + const readTool = unenforced[0]; + const result = await readTool.execute({ path: '/tmp' }); + // Won't get "Access denied" — might get a different error (reading a dir), but not workspace enforcement + expect(result).not.toContain('Access denied'); + }); + }); + + describe('include/exclude filtering', () => { + it('respects include filter', async () => { + const filtered = await createInkCodingTools({ + cwd: testDir, + include: ['read', 'ls'], + }); + const names = filtered.map((t) => t.schema.name); + expect(names).toEqual(['read', 'ls']); + }); + + it('respects exclude filter', async () => { + const filtered = await createInkCodingTools({ + cwd: testDir, + exclude: ['bash'], + }); + const names = filtered.map((t) => t.schema.name); + expect(names).not.toContain('bash'); + expect(names.length).toBe(6); + }); + }); + + describe('bash guard integration', () => { + let guardedTools: InkToolDefinition[]; + + beforeAll(async () => { + guardedTools = await createInkCodingTools({ + cwd: testDir, + agentId: 'test-agent', + }); + }); + + afterEach(() => { + resetProcessRegistry(); + }); + + it('blocks fork bombs before execution', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: ':(){ :|:& };:' }); + expect(result).toContain('Error'); + expect(result).toContain('fork bomb'); + }); + + it('blocks rm -rf / before execution', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'rm -rf /' }); + expect(result).toContain('Error'); + expect(result).toContain('recursive delete'); + }); + + it('blocks shutdown before execution', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'shutdown -h now' }); + expect(result).toContain('Error'); + expect(result).toContain('shutdown'); + }); + + it('blocks kill targeting unregistered PIDs', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'kill 99999' }); + expect(result).toContain('Error'); + expect(result).toContain('not owned by this agent'); + }); + + it('allows kill targeting own registered PIDs', async () => { + const registry = getProcessRegistry(); + registry.register('test-agent', 99999999, 'sleep 100'); + + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + // The kill will execute but fail (PID doesn't exist) — that's fine, + // the point is the guard lets it through + const result = await bash.execute({ command: 'kill 99999999' }); + expect(result).not.toContain('not owned by this agent'); + }); + + it('allows safe commands with guard enabled', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'echo guarded-hello' }); + expect(result).toContain('guarded-hello'); + }); + + it('does not guard when agentId is not set', async () => { + // The original tools (no agentId) should work normally + const bash = tools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'echo unguarded' }); + expect(result).toContain('unguarded'); + }); + + it('blocks pkill (fail-closed, name-based)', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'pkill -f node' }); + expect(result).toContain('Error'); + expect(result).toContain('pkill/killall'); + }); + + it('blocks kill with variable target (fail-closed)', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'kill $PPID' }); + expect(result).toContain('Error'); + expect(result).toContain('variable/dynamic'); + }); + + it('guard does not interfere with non-bash tools', async () => { + const readTool = guardedTools.find((t) => t.schema.name === 'read')!; + const result = await readTool.execute({ path: 'hello.txt' }); + expect(result).toContain('Hello, world!'); + }); + }); +}); diff --git a/packages/api/src/agent/tools/pi-coding-tools.ts b/packages/api/src/agent/tools/pi-coding-tools.ts new file mode 100644 index 00000000..d0870652 --- /dev/null +++ b/packages/api/src/agent/tools/pi-coding-tools.ts @@ -0,0 +1,235 @@ +/** + * Pi Coding Tools Adapter + * + * Bridges @mariozechner/pi-coding-agent's tool factories into Ink's + * Ink backend tool format (Anthropic.Tool + execution). + * + * Pi packages are ESM-only, so we use dynamic import(). + */ + +import path from 'path'; +import type Anthropic from '@anthropic-ai/sdk'; +import { logger } from '../../utils/logger'; +import { guardBashCommand } from './bash-guard'; + +// Pi tool types — widened to accept TypeBox TObject schemas +interface PiAgentTool { + name: string; + label?: string; + description?: string; + parameters?: unknown; + execute: ( + callId: string, + params: unknown, + signal?: AbortSignal, + onUpdate?: unknown + ) => Promise; +} + +interface PiToolResult { + content?: Array<{ type: string; text?: string }>; +} + +export interface InkToolDefinition { + /** Anthropic API tool schema (for sending to the LLM) */ + schema: Anthropic.Tool; + /** Execute the tool and return a string result */ + execute: (params: Record, signal?: AbortSignal) => Promise; +} + +export interface PiCodingToolsConfig { + /** Working directory — scopes all filesystem tools to this path */ + cwd: string; + /** Tools to include (default: all) */ + include?: Array<'read' | 'write' | 'edit' | 'bash' | 'grep' | 'find' | 'ls'>; + /** Tools to exclude */ + exclude?: Array<'read' | 'write' | 'edit' | 'bash' | 'grep' | 'find' | 'ls'>; + /** Enforce workspace root boundary — blocks access outside cwd (default: true) */ + enforceWorkspaceRoot?: boolean; + /** Default bash timeout in seconds when model doesn't specify one (default: 120) */ + bashTimeoutSeconds?: number; + /** Agent identity — enables bash guard (dangerous command blocking + kill scope) */ + agentId?: string; + /** Bash guard options (requires agentId to be set) */ + bashGuard?: { + /** Block catastrophic commands before execution (default: true) */ + blockDangerousCommands?: boolean; + /** Enforce kill targeting only agent-owned PIDs (default: true) */ + enforceKillScope?: boolean; + }; +} + +const TOOLS_WITH_PATH_PARAM = new Set(['read', 'write', 'edit', 'grep', 'find', 'ls']); + +function isPathWithinWorkspace(filePath: string, cwd: string): boolean { + const resolved = path.resolve(cwd, filePath); + const normalizedCwd = path.resolve(cwd); + return resolved.startsWith(normalizedCwd + path.sep) || resolved === normalizedCwd; +} + +interface PiModuleExports { + createCodingTools: (cwd: string) => PiAgentTool[]; + createGrepTool: (cwd: string, ...args: unknown[]) => PiAgentTool; + createFindTool: (cwd: string, ...args: unknown[]) => PiAgentTool; + createLsTool: (cwd: string, ...args: unknown[]) => PiAgentTool; +} + +let piModule: PiModuleExports | null = null; + +async function loadPiModule(): Promise { + if (piModule) return piModule; + const mod = (await import('@mariozechner/pi-coding-agent')) as Record; + piModule = { + createCodingTools: mod.createCodingTools as PiModuleExports['createCodingTools'], + createGrepTool: mod.createGrepTool as PiModuleExports['createGrepTool'], + createFindTool: mod.createFindTool as PiModuleExports['createFindTool'], + createLsTool: mod.createLsTool as PiModuleExports['createLsTool'], + }; + return piModule; +} + +function piParametersToJsonSchema(params: unknown): Record { + if (!params) return { type: 'object', properties: {} }; + if (typeof params !== 'object') return { type: 'object', properties: {} }; + + const p = params as Record; + // Pi uses TypeBox schemas — they compile to standard JSON Schema + // The 'properties' and 'type' fields should already be present + if (p.type === 'object' && p.properties) { + return p; + } + + // Fallback: wrap in an object schema + return { type: 'object', properties: p }; +} + +function formatToolResult(result: unknown): string { + if (result === null || result === undefined) { + return '(no output)'; + } + + // Pi tools return { content: [{ type: 'text', text: '...' }] } + if (typeof result === 'object' && result !== null) { + const r = result as PiToolResult; + if (Array.isArray(r.content)) { + return r.content + .filter((c) => c.type === 'text' && c.text) + .map((c) => c.text) + .join('\n'); + } + } + + if (typeof result === 'string') return result; + return JSON.stringify(result); +} + +const DEFAULT_BASH_TIMEOUT_SECONDS = 120; + +/** + * Create Pi coding tools adapted for the Ink backend. + * + * Returns both the Anthropic.Tool schemas (for the API call) and + * execute functions (for handling tool_use responses). + */ +export async function createInkCodingTools( + config: PiCodingToolsConfig +): Promise { + const pi = await loadPiModule(); + + // createCodingTools gives us: read, bash, edit, write + // Add grep, find, ls individually for the full coding toolset + const rawTools: PiAgentTool[] = [ + ...pi.createCodingTools(config.cwd), + pi.createGrepTool(config.cwd), + pi.createFindTool(config.cwd), + pi.createLsTool(config.cwd), + ]; + + // Filter tools based on include/exclude + let tools = rawTools; + if (config.include) { + const includeSet = new Set(config.include); + tools = tools.filter((t) => includeSet.has(t.name as any)); + } + if (config.exclude) { + const excludeSet = new Set(config.exclude); + tools = tools.filter((t) => !excludeSet.has(t.name as any)); + } + + logger.info('Pi coding tools loaded', { + cwd: config.cwd, + tools: tools.map((t) => t.name), + }); + + const enforceRoot = config.enforceWorkspaceRoot !== false; + const bashTimeout = config.bashTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS; + const agentId = config.agentId; + + return tools.map((tool) => ({ + schema: { + name: tool.name, + description: tool.description || `${tool.name} tool`, + input_schema: piParametersToJsonSchema(tool.parameters) as Anthropic.Tool.InputSchema, + }, + execute: async (params: Record, signal?: AbortSignal): Promise => { + // Workspace root enforcement for file-based tools + if (enforceRoot && TOOLS_WITH_PATH_PARAM.has(tool.name)) { + const filePath = (params.path as string) || ''; + if (filePath && !isPathWithinWorkspace(filePath, config.cwd)) { + return `Error: Access denied — path "${filePath}" is outside workspace root "${config.cwd}"`; + } + } + + // Bash guard: block dangerous commands and enforce kill scope + if (tool.name === 'bash' && agentId) { + const command = params.command as string; + if (command) { + const guard = guardBashCommand(command, { + agentId, + ...config.bashGuard, + }); + if (!guard.allowed) { + return `Error: ${guard.reason}`; + } + } + } + + // Inject default bash timeout if the model doesn't specify one + if (tool.name === 'bash' && !params.timeout) { + params = { ...params, timeout: bashTimeout }; + } + + const callId = `ink-${tool.name}-${Date.now()}`; + try { + const result = await tool.execute(callId, params, signal); + return formatToolResult(result); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + logger.error(`Pi tool ${tool.name} failed`, { error: message, params }); + return `Error: ${message}`; + } + }, + })); +} + +/** + * Convenience: get just the Anthropic.Tool schemas (for setTools). + */ +export async function getPiToolSchemas(config: PiCodingToolsConfig): Promise { + const tools = await createInkCodingTools(config); + return tools.map((t) => t.schema); +} + +/** + * Create a tool executor map for handling tool_use responses. + */ +export async function createPiToolExecutor( + config: PiCodingToolsConfig +): Promise> { + const tools = await createInkCodingTools(config); + const map = new Map(); + for (const tool of tools) { + map.set(tool.schema.name, tool.execute); + } + return map; +} diff --git a/packages/api/src/agent/types.ts b/packages/api/src/agent/types.ts index fe228a9f..774cdb34 100644 --- a/packages/api/src/agent/types.ts +++ b/packages/api/src/agent/types.ts @@ -18,7 +18,7 @@ export type ChannelType = | 'agent' | 'heartbeat' | 'web'; -export type BackendType = 'claude-code' | 'direct-api'; +export type BackendType = 'claude-code' | 'ink'; export type ResponseFormat = 'text' | 'markdown' | 'code' | 'json'; /** diff --git a/packages/api/src/data/repositories/task-groups.repository.ts b/packages/api/src/data/repositories/task-groups.repository.ts index 880fb4c4..be126276 100644 --- a/packages/api/src/data/repositories/task-groups.repository.ts +++ b/packages/api/src/data/repositories/task-groups.repository.ts @@ -68,6 +68,12 @@ export interface StrategyConfig { watchdogIntervalMinutes?: number; /** Supervisor agent identity ID — gets check-in notifications and a final audit on completion */ supervisorId?: string; + /** Run the strategy in a sandboxed Docker container */ + sandbox?: boolean; + /** Sandbox failure policy: 'required' fails the strategy if sandbox can't start, 'preferred' falls back to host (default: 'required') */ + sandboxPolicy?: 'required' | 'preferred'; + /** Backend auth dirs to mount in the sandbox (default: ['claude']) */ + sandboxBackendAuth?: Array<'claude' | 'codex' | 'gemini'>; } export interface CreateTaskGroupInput { diff --git a/packages/api/src/mcp/tools/strategy-handlers.ts b/packages/api/src/mcp/tools/strategy-handlers.ts index 2234dbe9..07101bc5 100644 --- a/packages/api/src/mcp/tools/strategy-handlers.ts +++ b/packages/api/src/mcp/tools/strategy-handlers.ts @@ -12,6 +12,7 @@ import type { VerificationMode, } from '../../data/repositories/task-groups.repository'; import { StrategyService } from '../../services/strategy.service'; +import { getOrchestrator } from '../../services/sandbox/index.js'; import { resolveUser, type UserIdentifier } from '../../services/user-resolver'; import { getEffectiveAgentId } from '../../auth/enforce-identity'; @@ -102,6 +103,17 @@ export const startStrategySchema = z.object({ .describe( 'Supervisor agent identity ID (UUID). Gets check-in notifications and a final audit on completion.' ), + sandbox: z.boolean().optional().describe('Run the strategy in a sandboxed Docker container'), + sandboxPolicy: z + .enum(['required', 'preferred']) + .optional() + .describe( + "Sandbox failure policy: 'required' aborts if sandbox can't start (default), 'preferred' falls back to host" + ), + sandboxBackendAuth: z + .array(z.enum(['claude', 'codex', 'gemini'])) + .optional() + .describe("Backend auth dirs to mount in the sandbox (default: ['claude'])"), }); export async function handleStartStrategy( @@ -116,7 +128,7 @@ export async function handleStartStrategy( const agentId = getEffectiveAgentId(args.ownerAgentId); - const service = new StrategyService(dataComposer); + const service = new StrategyService(dataComposer, getOrchestrator()); const result = await service.startStrategy({ groupId: args.groupId, userId: resolved.user.id, @@ -133,6 +145,9 @@ export async function handleStartStrategy( contextSummaryInterval: args.contextSummaryInterval, verificationGates: args.verificationGates, supervisorId: args.supervisorId, + sandbox: args.sandbox, + sandboxPolicy: args.sandboxPolicy, + sandboxBackendAuth: args.sandboxBackendAuth, }, }); @@ -179,7 +194,7 @@ export async function handlePauseStrategy( return mcpResponse({ success: false, error: 'User not found' }, true); } - const service = new StrategyService(dataComposer); + const service = new StrategyService(dataComposer, getOrchestrator()); const group = await service.pauseStrategy(args.groupId, resolved.user.id); return mcpResponse({ @@ -220,7 +235,7 @@ export async function handleResumeStrategy( return mcpResponse({ success: false, error: 'User not found' }, true); } - const service = new StrategyService(dataComposer); + const service = new StrategyService(dataComposer, getOrchestrator()); const result = await service.resumeStrategy(args.groupId, resolved.user.id); return mcpResponse({ @@ -271,7 +286,7 @@ export async function handleCancelStrategy( return mcpResponse({ success: false, error: 'User not found' }, true); } - const service = new StrategyService(dataComposer); + const service = new StrategyService(dataComposer, getOrchestrator()); const group = await service.cancelStrategy(args.groupId, resolved.user.id, args.reason); return mcpResponse({ @@ -312,7 +327,7 @@ export async function handleGetStrategyStatus( return mcpResponse({ success: false, error: 'User not found' }, true); } - const service = new StrategyService(dataComposer); + const service = new StrategyService(dataComposer, getOrchestrator()); const status = await service.getStrategyStatus(args.groupId, resolved.user.id); return mcpResponse({ success: true, ...status }); diff --git a/packages/api/src/mcp/tools/task-handlers.ts b/packages/api/src/mcp/tools/task-handlers.ts index d4b7f9fd..0180ccc0 100644 --- a/packages/api/src/mcp/tools/task-handlers.ts +++ b/packages/api/src/mcp/tools/task-handlers.ts @@ -9,6 +9,7 @@ import { z } from 'zod'; import type { DataComposer } from '../../data/composer'; import type { TaskStatus, TaskPriority } from '../../data/repositories/project-tasks.repository'; import { StrategyService } from '../../services/strategy.service'; +import { getOrchestrator } from '../../services/sandbox/index.js'; import { resolveUser, type UserIdentifier } from '../../services/user-resolver'; import { getEffectiveAgentId } from '../../auth/enforce-identity'; import { getRequestContext } from '../../utils/request-context'; @@ -412,7 +413,7 @@ export async function handleCompleteTask( try { const group = await dataComposer.repositories.taskGroups.findById(task.task_group_id); if (group && group.strategy && group.status === 'active') { - const strategyService = new StrategyService(dataComposer); + const strategyService = new StrategyService(dataComposer, getOrchestrator()); strategyResult = await strategyService.advanceStrategy( task.task_group_id, task.id, diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 9cff9e12..0dd1ddda 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -40,6 +40,7 @@ import { type DueReminder, } from './services/heartbeat'; import { StrategyService } from './services/strategy.service'; +import { getOrchestrator } from './services/sandbox/index.js'; import { setResponseCallback, hasExplicitResponse } from './mcp/tools/response-handlers'; import { getAgentGateway, type AgentTriggerPayload } from './channels/agent-gateway'; import { resolveRouteAgentId } from './services/routing/resolve-route'; @@ -466,7 +467,7 @@ async function startServer(config: ServerConfig = {}): Promise { return false; } try { - const strategyService = new StrategyService(dataComposer); + const strategyService = new StrategyService(dataComposer, getOrchestrator()); const fired = await strategyService.triggerWatchdog(groupId); if (fired) { logger.info( @@ -898,6 +899,11 @@ When you complete a task_request, mark it as completed using update_inbox_messag payload.metadata && typeof payload.metadata.groupId === 'string' ? payload.metadata.groupId : undefined, + // Forward sandbox container name so the session service routes CLI execution into it + ...(payload.metadata?.sandboxContainerName && + typeof payload.metadata.sandboxContainerName === 'string' + ? { sandboxContainerName: payload.metadata.sandboxContainerName } + : {}), }, }; diff --git a/packages/api/src/services/sandbox/index.ts b/packages/api/src/services/sandbox/index.ts new file mode 100644 index 00000000..9b5395c9 --- /dev/null +++ b/packages/api/src/services/sandbox/index.ts @@ -0,0 +1,25 @@ +export { + SandboxOrchestrator, + buildContainerName, + buildEnvVars, + buildDockerRunArgs, + buildMounts, + stageClaudeDir, + stageCodexDir, + type SandboxSpinUpRequest, + type SandboxSpinUpResult, + type SandboxStatusResult, + type BackendAuthName, + type SandboxMount, +} from './orchestrator.js'; + +import { SandboxOrchestrator } from './orchestrator.js'; + +let _instance: SandboxOrchestrator | undefined; + +export function getOrchestrator(): SandboxOrchestrator { + if (!_instance) { + _instance = new SandboxOrchestrator(); + } + return _instance; +} diff --git a/packages/api/src/services/sandbox/orchestrator.integration.test.ts b/packages/api/src/services/sandbox/orchestrator.integration.test.ts new file mode 100644 index 00000000..5a86f765 --- /dev/null +++ b/packages/api/src/services/sandbox/orchestrator.integration.test.ts @@ -0,0 +1,363 @@ +/** + * Integration tests for SandboxOrchestrator. + * + * These tests spin up real Docker containers. They require: + * - Docker daemon running + * - The inkwell:studio-sandbox image built (`ink studio sandbox build`) + * + * Container reuse: + * A shared fixture container (ink-test-sandbox-integ) is spun up once + * and reused across capability tests. Set INK_PERSIST_TEST_CONTAINER=1 + * to keep it alive after the run — saves ~20s on re-runs. + * + * To tear it down manually: docker rm -f ink-test-sandbox-integ + * + * Run with: npx vitest run --config vitest.integration.config.ts src/services/sandbox/orchestrator.integration.test.ts + */ + +import { describe, it, expect, afterAll, afterEach, beforeAll } from 'vitest'; +import { execFileSync, spawnSync } from 'child_process'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readFileSync } from 'fs'; +import { homedir, tmpdir } from 'os'; +import { join } from 'path'; +import { SandboxOrchestrator, buildContainerName, type SandboxSpinUpRequest } from './orchestrator'; +import { createInkCodingTools, type InkToolDefinition } from '../../agent/tools/pi-coding-tools'; +import { getProcessRegistry, resetProcessRegistry } from '../../agent/tools/bash-guard'; + +function dockerAvailable(): boolean { + try { + execFileSync('docker', ['info'], { stdio: 'ignore', timeout: 5_000 }); + return true; + } catch { + return false; + } +} + +function imageExists(image: string): boolean { + const result = spawnSync('docker', ['image', 'inspect', image], { stdio: 'ignore' }); + return result.status === 0; +} + +const SKIP = !dockerAvailable() || !imageExists('inkwell:studio-sandbox'); +const PERSIST = process.env.INK_PERSIST_TEST_CONTAINER === '1'; +const FIXTURE_NAME = 'ink-test-sandbox-integ'; +// Stable path so persisted containers' mounts match across runs +const FIXTURE_DIR = join(homedir(), '.ink', 'test-fixtures', 'sandbox-integ'); + +describe.skipIf(SKIP)('SandboxOrchestrator (integration)', () => { + let orchestrator: SandboxOrchestrator; + let testDir: string; + const ephemeralContainers: string[] = []; + let fixtureWasPreExisting = false; + + beforeAll(async () => { + orchestrator = new SandboxOrchestrator(); + + // Use a stable dir for fixture containers, temp dir for ephemeral + testDir = FIXTURE_DIR; + mkdirSync(testDir, { recursive: true }); + // Refresh test files each run (idempotent) + writeFileSync(join(testDir, 'hello.txt'), 'Integration test file\n'); + mkdirSync(join(testDir, 'src'), { recursive: true }); + writeFileSync(join(testDir, 'src', 'index.ts'), 'console.log("hello");\n'); + // Clean up any output files from prior runs + try { + rmSync(join(testDir, 'fixture-output.txt')); + } catch {} + + // Spin up or reuse the shared fixture container + fixtureWasPreExisting = await orchestrator.isRunning(FIXTURE_NAME); + if (!fixtureWasPreExisting) { + const result = await orchestrator.spinUp({ + userId: 'test-user', + agentId: 'test-agent', + studioId: 'studio-integ-fixture', + studioSlug: 'integ', + worktreePath: testDir, + repoRoot: testDir, + containerName: FIXTURE_NAME, + }); + expect(result.success).toBe(true); + } + }); + + afterAll(async () => { + // Always clean up ephemeral containers + for (const name of ephemeralContainers) { + await orchestrator.stop(name).catch(() => {}); + } + // Only tear down fixture if not persisting and we created it this run + if (!PERSIST && !fixtureWasPreExisting) { + await orchestrator.stop(FIXTURE_NAME).catch(() => {}); + } + }); + + function makeRequest(overrides: Partial = {}): SandboxSpinUpRequest { + return { + userId: 'test-user', + agentId: 'test-agent', + studioId: `studio-${Date.now()}`, + studioSlug: 'test', + worktreePath: testDir, + repoRoot: testDir, + ...overrides, + }; + } + + // ── Shared fixture tests (fast — reuse one container) ─────────────── + + describe('shared fixture', () => { + it('can read files at /studio', async () => { + const { stdout } = await orchestrator.exec(FIXTURE_NAME, ['cat', '/studio/hello.txt']); + expect(stdout.trim()).toBe('Integration test file'); + }, 10_000); + + it('has node and claude cli available', async () => { + const { stdout: nodeVersion } = await orchestrator.exec(FIXTURE_NAME, ['node', '--version']); + expect(nodeVersion.trim()).toMatch(/^v22\./); + + const { stdout: claudePath } = await orchestrator.exec(FIXTURE_NAME, ['which', 'claude']); + expect(claudePath.trim()).toBeTruthy(); + }, 10_000); + + it('has correct workdir', async () => { + const { stdout } = await orchestrator.exec(FIXTURE_NAME, ['pwd']); + expect(stdout.trim()).toBe('/studio'); + }, 10_000); + + it('can write files visible on the host', async () => { + await orchestrator.exec(FIXTURE_NAME, [ + 'bash', + '-c', + 'echo "from-fixture" > /studio/fixture-output.txt', + ]); + const { readFileSync } = await import('fs'); + const content = readFileSync(join(testDir, 'fixture-output.txt'), 'utf-8'); + expect(content.trim()).toBe('from-fixture'); + }, 10_000); + }); + + // ── Per-container lifecycle tests (each spins up its own) ─────────── + + describe('container lifecycle', () => { + it('spins up a container and verifies it is running', async () => { + const request = makeRequest({ studioSlug: 'integ-spinup' }); + const containerName = buildContainerName(request); + ephemeralContainers.push(containerName); + + const result = await orchestrator.spinUp(request); + expect(result.success).toBe(true); + expect(result.containerName).toBe(containerName); + + const running = await orchestrator.isRunning(containerName); + expect(running).toBe(true); + }, 30_000); + + it('returns alreadyRunning when container exists', async () => { + const request = makeRequest({ studioSlug: 'integ-already' }); + const containerName = buildContainerName(request); + ephemeralContainers.push(containerName); + + const first = await orchestrator.spinUp(request); + expect(first.success).toBe(true); + + const second = await orchestrator.spinUp(request); + expect(second.success).toBe(true); + expect(second.alreadyRunning).toBe(true); + }, 30_000); + + it('passes env vars into the container', async () => { + const request = makeRequest({ + studioSlug: 'integ-env', + taskGroupId: 'tg-test-123', + taskGroupTitle: 'Test Group', + }); + const containerName = buildContainerName(request); + ephemeralContainers.push(containerName); + + await orchestrator.spinUp(request); + + const { stdout: agentId } = await orchestrator.exec(containerName, [ + 'bash', + '-c', + 'echo $AGENT_ID', + ]); + expect(agentId.trim()).toBe('test-agent'); + + const { stdout: tgId } = await orchestrator.exec(containerName, [ + 'bash', + '-c', + 'echo $INK_TASK_GROUP_ID', + ]); + expect(tgId.trim()).toBe('tg-test-123'); + + const { stdout: sandbox } = await orchestrator.exec(containerName, [ + 'bash', + '-c', + 'echo $INK_SANDBOX', + ]); + expect(sandbox.trim()).toBe('docker'); + }, 30_000); + + it('stops a running container', async () => { + const request = makeRequest({ studioSlug: 'integ-stop' }); + const containerName = buildContainerName(request); + + await orchestrator.spinUp(request); + expect(await orchestrator.isRunning(containerName)).toBe(true); + + const stopped = await orchestrator.stop(containerName); + expect(stopped).toBe(true); + expect(await orchestrator.isRunning(containerName)).toBe(false); + }, 30_000); + + it('gets container status with labels', async () => { + const request = makeRequest({ + studioSlug: 'integ-status', + taskGroupId: 'tg-status-test', + }); + const containerName = buildContainerName(request); + ephemeralContainers.push(containerName); + + await orchestrator.spinUp(request); + + const status = await orchestrator.getStatus(containerName); + expect(status.running).toBe(true); + expect(status.labels?.['ink.sandbox']).toBe('true'); + expect(status.labels?.['ink.agent-id']).toBe('test-agent'); + expect(status.labels?.['ink.task-group-id']).toBe('tg-status-test'); + }, 30_000); + + it('lists active sandboxes', async () => { + const request = makeRequest({ studioSlug: 'integ-list' }); + const containerName = buildContainerName(request); + ephemeralContainers.push(containerName); + + await orchestrator.spinUp(request); + + const sandboxes = await orchestrator.listSandboxes(); + const found = sandboxes.find((s) => s.containerName === containerName); + expect(found).toBeDefined(); + expect(found?.running).toBe(true); + }, 30_000); + + it('container name includes task group context', async () => { + const request = makeRequest({ + studioSlug: 'integ-naming', + taskGroupId: 'tg-naming-test', + taskGroupTitle: 'Auth Migration', + }); + const containerName = buildContainerName(request); + ephemeralContainers.push(containerName); + + expect(containerName).toContain('integ-naming'); + expect(containerName).toContain('auth-migration'); + + await orchestrator.spinUp(request); + const running = await orchestrator.isRunning(containerName); + expect(running).toBe(true); + }, 30_000); + + it('respects containerName override', async () => { + const customName = 'ink-test-custom-name'; + const request = makeRequest({ containerName: customName }); + ephemeralContainers.push(customName); + + const result = await orchestrator.spinUp(request); + expect(result.containerName).toBe(customName); + expect(await orchestrator.isRunning(customName)).toBe(true); + }, 30_000); + }); + + // ── Bash guard in container context ──────────────────────────────── + + describe('bash guard (container context)', () => { + let guardedTools: InkToolDefinition[]; + + beforeAll(async () => { + guardedTools = await createInkCodingTools({ + cwd: testDir, + agentId: 'integ-guard-agent', + }); + }); + + afterEach(() => { + resetProcessRegistry(); + }); + + it('blocks fork bomb — container stays healthy', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: ':(){ :|:& };:' }); + expect(result).toContain('Blocked'); + expect(result).toContain('fork bomb'); + + // Container is still alive — the fork bomb never executed + const { stdout } = await orchestrator.exec(FIXTURE_NAME, ['echo', 'still-alive']); + expect(stdout.trim()).toBe('still-alive'); + }, 10_000); + + it('blocks rm -rf / — workspace files intact', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'rm -rf /' }); + expect(result).toContain('Blocked'); + expect(result).toContain('recursive delete'); + + // Workspace files still exist inside the container + const { stdout } = await orchestrator.exec(FIXTURE_NAME, ['cat', '/studio/hello.txt']); + expect(stdout).toContain('Integration test file'); + }, 10_000); + + it('blocks shutdown — container unaffected', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'shutdown -h now' }); + expect(result).toContain('Blocked'); + expect(result).toContain('shutdown'); + + const { stdout } = await orchestrator.exec(FIXTURE_NAME, ['echo', 'not-shut-down']); + expect(stdout.trim()).toBe('not-shut-down'); + }, 10_000); + + it('blocks kill of unregistered PIDs', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'kill 12345' }); + expect(result).toContain('Blocked'); + expect(result).toContain('not owned by this agent'); + }, 10_000); + + it('allows safe commands — result visible in container', async () => { + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ + command: 'echo "guard-allowed" > guard-test-output.txt', + }); + // No error from guard + expect(result).not.toContain('Blocked'); + + // File created on host is visible inside the container + const { stdout } = await orchestrator.exec(FIXTURE_NAME, [ + 'cat', + '/studio/guard-test-output.txt', + ]); + expect(stdout.trim()).toBe('guard-allowed'); + }, 10_000); + + it('allows kill of agent-owned PIDs', async () => { + const registry = getProcessRegistry(); + registry.register('integ-guard-agent', 99999999, 'test-process'); + + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + // Guard lets it through — actual kill fails (PID doesn't exist) but that's expected + const result = await bash.execute({ command: 'kill 99999999' }); + expect(result).not.toContain('not owned by this agent'); + }, 10_000); + + it('cross-agent kill blocked — other agent PID protected', async () => { + const registry = getProcessRegistry(); + registry.register('other-agent', 88888, 'other-process'); + + const bash = guardedTools.find((t) => t.schema.name === 'bash')!; + const result = await bash.execute({ command: 'kill 88888' }); + expect(result).toContain('Blocked'); + expect(result).toContain('88888'); + }, 10_000); + }); +}); diff --git a/packages/api/src/services/sandbox/orchestrator.live.integration.test.ts b/packages/api/src/services/sandbox/orchestrator.live.integration.test.ts new file mode 100644 index 00000000..c03c1736 --- /dev/null +++ b/packages/api/src/services/sandbox/orchestrator.live.integration.test.ts @@ -0,0 +1,241 @@ +/** + * Live tests for SandboxOrchestrator. + * + * These tests spin up real Docker containers and make real LLM API calls. + * They require: + * - Docker daemon running + * - The inkwell:studio-sandbox image built + * - Active Claude Code session (OAuth tokens staged from macOS keychain) + * - Inkwell server running on localhost:3001 (for MCP connectivity test) + * + * Container reuse: + * A shared fixture container (ink-test-sandbox-live) is spun up once + * with credentials mounted and reused across all tests. Set + * INK_PERSIST_TEST_CONTAINER=1 to keep it alive after the run. + * + * To tear it down manually: docker rm -f ink-test-sandbox-live + * + * Run with: INK_LIVE_TESTS=1 npx vitest run --config vitest.integration.config.ts src/services/sandbox/orchestrator.live.integration.test.ts + */ + +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import { execFileSync, spawnSync } from 'child_process'; +import { writeFileSync, mkdirSync, readFileSync, existsSync, rmSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import { SandboxOrchestrator, type SandboxSpinUpRequest } from './orchestrator'; + +function dockerAvailable(): boolean { + try { + execFileSync('docker', ['info'], { stdio: 'ignore', timeout: 5_000 }); + return true; + } catch { + return false; + } +} + +function imageExists(image: string): boolean { + const result = spawnSync('docker', ['image', 'inspect', image], { stdio: 'ignore' }); + return result.status === 0; +} + +function claudeCredentialsAvailable(): boolean { + const credFile = join(homedir(), '.claude', '.credentials.json'); + if (existsSync(credFile)) return true; + if (process.platform === 'darwin') { + try { + execFileSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'], { + encoding: 'utf-8', + timeout: 5_000, + stdio: ['pipe', 'pipe', 'pipe'], + }); + return true; + } catch { + return false; + } + } + return false; +} + +function inkwellReachable(): boolean { + try { + execFileSync('curl', ['-sf', '-o', '/dev/null', 'http://localhost:3001/health'], { + timeout: 3_000, + }); + return true; + } catch { + return false; + } +} + +const SKIP = + process.env.INK_LIVE_TESTS !== '1' || + !dockerAvailable() || + !imageExists('inkwell:studio-sandbox'); + +const PERSIST = process.env.INK_PERSIST_TEST_CONTAINER === '1'; +const FIXTURE_NAME = 'ink-test-sandbox-live'; +// Stable path so persisted containers' mounts match across runs +const FIXTURE_DIR = join(homedir(), '.ink', 'test-fixtures', 'sandbox-live'); + +describe.skipIf(SKIP)('SandboxOrchestrator (live)', () => { + let orchestrator: SandboxOrchestrator; + let testDir: string; + let fixtureWasPreExisting = false; + + beforeAll(async () => { + orchestrator = new SandboxOrchestrator(); + testDir = FIXTURE_DIR; + mkdirSync(testDir, { recursive: true }); + + // Refresh test files each run (idempotent) + writeFileSync(join(testDir, 'README.md'), '# Test Project\n\nThis is a sandbox live test.\n'); + mkdirSync(join(testDir, 'src'), { recursive: true }); + writeFileSync( + join(testDir, 'src', 'hello.ts'), + 'export function greet() { return "hello"; }\n' + ); + // Clean up output files from prior runs + try { + rmSync(join(testDir, 'sandbox-output.txt')); + } catch {} + + // Create .mcp.json with inkwell (HTTP) + a stdio server to verify stripping + writeFileSync( + join(testDir, '.mcp.json'), + JSON.stringify( + { + mcpServers: { + inkwell: { type: 'http', url: 'http://localhost:3001/mcp' }, + playwright: { type: 'stdio', command: 'npx', args: ['@playwright/mcp'] }, + }, + }, + null, + 2 + ) + ); + + // Spin up or reuse the shared fixture container (with credentials) + fixtureWasPreExisting = await orchestrator.isRunning(FIXTURE_NAME); + if (!fixtureWasPreExisting) { + const result = await orchestrator.spinUp({ + userId: 'live-test-user', + agentId: 'live-test-agent', + studioId: 'studio-live-fixture', + studioSlug: 'live', + worktreePath: testDir, + repoRoot: testDir, + backendAuth: ['claude'], + containerName: FIXTURE_NAME, + }); + expect(result.success).toBe(true); + } + }); + + afterAll(async () => { + if (!PERSIST && !fixtureWasPreExisting) { + await orchestrator.stop(FIXTURE_NAME).catch(() => {}); + } + }); + + describe('worktree manipulation', () => { + it('agent can read and write files in the mounted studio', async () => { + const { stdout: readResult } = await orchestrator.exec(FIXTURE_NAME, [ + 'cat', + '/studio/README.md', + ]); + expect(readResult).toContain('Test Project'); + + await orchestrator.exec(FIXTURE_NAME, [ + 'bash', + '-c', + 'echo "created by sandbox" > /studio/sandbox-output.txt', + ]); + + const hostPath = join(testDir, 'sandbox-output.txt'); + expect(existsSync(hostPath)).toBe(true); + expect(readFileSync(hostPath, 'utf-8').trim()).toBe('created by sandbox'); + }, 10_000); + + it('agent can modify existing source files', async () => { + await orchestrator.exec(FIXTURE_NAME, [ + 'bash', + '-c', + 'echo \'export function farewell() { return "goodbye"; }\' >> /studio/src/hello.ts', + ]); + + const content = readFileSync(join(testDir, 'src', 'hello.ts'), 'utf-8'); + expect(content).toContain('farewell'); + expect(content).toContain('greet'); + }, 10_000); + }); + + describe('MCP config patching', () => { + it('patched config contains only HTTP servers with rewritten URLs', async () => { + const { stdout } = await orchestrator.exec(FIXTURE_NAME, ['cat', '/studio/.mcp.json']); + const config = JSON.parse(stdout); + + expect(config.mcpServers.inkwell).toBeDefined(); + expect(config.mcpServers.inkwell.url).toContain('host.docker.internal'); + expect(config.mcpServers.playwright).toBeUndefined(); + }, 10_000); + }); + + describe('LLM response', () => { + const SKIP_LLM = !claudeCredentialsAvailable(); + + it.skipIf(SKIP_LLM)( + 'gets a live Claude response inside the container', + async () => { + const { stdout } = await orchestrator.exec(FIXTURE_NAME, [ + 'claude', + '--print', + '--model', + 'claude-haiku-4-5-20251001', + 'What is 2+2? Reply with just the number.', + ]); + + expect(stdout.trim().length).toBeGreaterThan(0); + expect(stdout).toContain('4'); + }, + 120_000 + ); + + it.skipIf(SKIP_LLM)( + 'Claude can read workspace files via coding tools', + async () => { + const { stdout } = await orchestrator.exec(FIXTURE_NAME, [ + 'claude', + '--print', + '--model', + 'claude-haiku-4-5-20251001', + '--allowedTools', + 'Read', + '-p', + 'Read the file src/hello.ts and tell me what function it exports. Reply with just the function name.', + ]); + + expect(stdout.toLowerCase()).toContain('greet'); + }, + 120_000 + ); + }); + + describe('Inkwell MCP access', () => { + const SKIP_INKWELL = !inkwellReachable(); + + it.skipIf(SKIP_INKWELL)( + 'container can reach Inkwell server via host.docker.internal', + async () => { + const { stdout } = await orchestrator.exec(FIXTURE_NAME, [ + 'curl', + '-sf', + 'http://host.docker.internal:3001/health', + ]); + + expect(stdout.length).toBeGreaterThan(0); + }, + 10_000 + ); + }); +}); diff --git a/packages/api/src/services/sandbox/orchestrator.test.ts b/packages/api/src/services/sandbox/orchestrator.test.ts new file mode 100644 index 00000000..274a3f69 --- /dev/null +++ b/packages/api/src/services/sandbox/orchestrator.test.ts @@ -0,0 +1,483 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { existsSync, mkdtempSync, writeFileSync, readFileSync, mkdirSync } from 'fs'; +import { homedir, tmpdir } from 'os'; +import { join } from 'path'; +import { + buildContainerName, + buildEnvVars, + buildDockerRunArgs, + buildMounts, + getRunnerFilesDir, + patchMcpConfig, + stageClaudeDir, + stageCodexDir, + SandboxOrchestrator, + type SandboxSpinUpRequest, +} from './orchestrator'; + +vi.mock('../../utils/logger', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, +})); + +const baseRequest: SandboxSpinUpRequest = { + userId: 'user-123', + agentId: 'wren', + studioId: 'studio-abc', + studioSlug: 'wren', + worktreePath: '/tmp/test-studio', + repoRoot: '/tmp/test-repo', + branch: 'wren/feat/sandbox', +}; + +describe('buildContainerName', () => { + it('includes studio slug and digest', () => { + const name = buildContainerName(baseRequest); + expect(name).toMatch(/^ink-sandbox-wren-[a-f0-9]{8}$/); + }); + + it('includes task group slug when provided', () => { + const name = buildContainerName({ + ...baseRequest, + taskGroupId: 'tg-456', + taskGroupTitle: 'Auth Refactor', + }); + expect(name).toMatch(/^ink-sandbox-wren-auth-refactor-[a-f0-9]{8}$/); + }); + + it('falls back to agentId when no studioSlug', () => { + const name = buildContainerName({ ...baseRequest, studioSlug: undefined }); + expect(name).toMatch(/^ink-sandbox-wren-[a-f0-9]{8}$/); + }); + + it('sanitizes special characters', () => { + const name = buildContainerName({ + ...baseRequest, + studioSlug: 'My Studio!!!', + taskGroupId: 'tg-1', + taskGroupTitle: 'Fix: all the BUGS (urgent)', + }); + expect(name).not.toMatch(/[^a-z0-9-]/); + }); + + it('produces different names for different task groups on the same studio', () => { + const name1 = buildContainerName({ ...baseRequest, taskGroupId: 'tg-1', taskGroupTitle: 'A' }); + const name2 = buildContainerName({ ...baseRequest, taskGroupId: 'tg-2', taskGroupTitle: 'B' }); + expect(name1).not.toBe(name2); + }); + + it('truncates long slugs', () => { + const name = buildContainerName({ + ...baseRequest, + studioSlug: 'a-very-long-studio-name-that-goes-on-forever', + }); + // "ink-sandbox-" (12) + slug (max 24) + "-" (1) + digest (8) = max 45 chars + expect(name.length).toBeLessThanOrEqual(50); + }); + + it('uses containerName override when provided', () => { + const name = buildContainerName({ ...baseRequest, containerName: 'ink-test-my-fixture' }); + expect(name).toBe('ink-test-my-fixture'); + }); +}); + +describe('buildEnvVars', () => { + it('includes core env vars', () => { + const env = buildEnvVars(baseRequest); + expect(env.AGENT_ID).toBe('wren'); + expect(env.INK_STUDIO_ID).toBe('studio-abc'); + expect(env.INK_SANDBOX).toBe('docker'); + expect(env.INK_STUDIO_PATH).toBe('/studio'); + }); + + it('rewrites localhost to host.docker.internal', () => { + const env = buildEnvVars({ ...baseRequest, serverUrl: 'http://localhost:3001' }); + expect(env.INK_SERVER_URL).toBe('http://host.docker.internal:3001'); + }); + + it('preserves non-localhost URLs', () => { + const env = buildEnvVars({ ...baseRequest, serverUrl: 'https://api.example.com' }); + expect(env.INK_SERVER_URL).toBe('https://api.example.com'); + }); + + it('includes task group vars when provided', () => { + const env = buildEnvVars({ + ...baseRequest, + taskGroupId: 'tg-456', + taskGroupTitle: 'Auth Refactor', + taskGroupContext: 'Migrating session tokens', + taskGroupThreadKey: 'strategy:tg-456', + }); + expect(env.INK_TASK_GROUP_ID).toBe('tg-456'); + expect(env.INK_TASK_GROUP_TITLE).toBe('Auth Refactor'); + expect(env.INK_TASK_GROUP_CONTEXT).toBe('Migrating session tokens'); + expect(env.INK_TASK_GROUP_THREAD_KEY).toBe('strategy:tg-456'); + }); + + it('omits task group vars when not provided', () => { + const env = buildEnvVars(baseRequest); + expect(env.INK_TASK_GROUP_ID).toBeUndefined(); + expect(env.INK_TASK_GROUP_TITLE).toBeUndefined(); + }); + + it('merges extraEnv', () => { + const env = buildEnvVars({ ...baseRequest, extraEnv: { CUSTOM_VAR: 'value' } }); + expect(env.CUSTOM_VAR).toBe('value'); + }); + + it('includes branch when provided', () => { + const env = buildEnvVars(baseRequest); + expect(env.INK_BRANCH).toBe('wren/feat/sandbox'); + }); +}); + +describe('buildDockerRunArgs', () => { + it('includes required docker run flags', async () => { + const args = await buildDockerRunArgs(baseRequest); + expect(args[0]).toBe('run'); + expect(args).toContain('--rm'); + expect(args).toContain('-d'); + expect(args).toContain(DEFAULT_IMAGE_NAME()); + }); + + it('sets container name', async () => { + const args = await buildDockerRunArgs(baseRequest); + const nameIdx = args.indexOf('--name'); + expect(nameIdx).toBeGreaterThan(-1); + expect(args[nameIdx + 1]).toMatch(/^ink-sandbox-/); + }); + + it('sets workdir to /studio', async () => { + const args = await buildDockerRunArgs(baseRequest); + const idx = args.indexOf('--workdir'); + expect(args[idx + 1]).toBe('/studio'); + }); + + it('adds host.docker.internal mapping', async () => { + const args = await buildDockerRunArgs(baseRequest); + expect(args).toContain('--add-host'); + const idx = args.indexOf('--add-host'); + expect(args[idx + 1]).toBe('host.docker.internal:host-gateway'); + }); + + it('adds discovery labels', async () => { + const args = await buildDockerRunArgs(baseRequest); + expect(args).toContain('ink.sandbox=true'); + expect(args).toContain(`ink.agent-id=wren`); + expect(args).toContain(`ink.studio-id=studio-abc`); + }); + + it('adds task group label when provided', async () => { + const args = await buildDockerRunArgs({ ...baseRequest, taskGroupId: 'tg-456' }); + expect(args).toContain('ink.task-group-id=tg-456'); + }); + + it('sets network none when requested', async () => { + const args = await buildDockerRunArgs({ ...baseRequest, networkMode: 'none' }); + const idx = args.indexOf('--network'); + expect(idx).toBeGreaterThan(-1); + expect(args[idx + 1]).toBe('none'); + }); + + it('passes env vars as -e flags', async () => { + const args = await buildDockerRunArgs(baseRequest); + const envPairs = args.filter((_, i) => i > 0 && args[i - 1] === '-e'); + expect(envPairs.some((p) => p.startsWith('AGENT_ID=wren'))).toBe(true); + expect(envPairs.some((p) => p.startsWith('INK_SANDBOX=docker'))).toBe(true); + }); +}); + +describe('getRunnerFilesDir', () => { + it('returns expected path pattern under ~/.ink/runtime/sandbox//runner-files', () => { + const dir = getRunnerFilesDir('ink-sandbox-wren-abc12345'); + expect(dir).toBe( + join(homedir(), '.ink', 'runtime', 'sandbox', 'ink-sandbox-wren-abc12345', 'runner-files') + ); + }); + + it('returns distinct paths for different container names', () => { + const dir1 = getRunnerFilesDir('ink-sandbox-wren-aaaa1111'); + const dir2 = getRunnerFilesDir('ink-sandbox-lumen-bbbb2222'); + expect(dir1).not.toBe(dir2); + }); +}); + +describe('buildMounts', () => { + it('includes a mount with target /run/ink for runner temp files', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mount-ink-')); + const mounts = await buildMounts({ ...baseRequest, worktreePath: tmpDir, repoRoot: tmpDir }); + const runInkMount = mounts.find((m) => m.target === '/run/ink'); + expect(runInkMount).toBeDefined(); + expect(runInkMount!.readOnly).toBe(false); + // Source should end with 'runner-files' + expect(runInkMount!.source).toMatch(/runner-files$/); + }); + + it('returns empty array when worktree path does not exist', async () => { + const mounts = await buildMounts({ ...baseRequest, worktreePath: '/nonexistent/path' }); + expect(mounts.filter((m) => m.target === '/studio')).toHaveLength(0); + }); + + it('mounts worktree at /studio when path exists', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mount-test-')); + const mounts = await buildMounts({ ...baseRequest, worktreePath: tmpDir, repoRoot: tmpDir }); + expect(mounts.some((m) => m.target === '/studio' && m.source === tmpDir)).toBe(true); + }); + + it('resolves git worktree mounts when .git is a file', async () => { + const repoDir = mkdtempSync(join(tmpdir(), 'repo-')); + const worktreeDir = mkdtempSync(join(tmpdir(), 'worktree-')); + + // Create canonical .git structure + mkdirSync(join(repoDir, '.git', 'worktrees', 'my-branch'), { recursive: true }); + + // Create .git file (worktree marker) + writeFileSync(join(worktreeDir, '.git'), `gitdir: ${repoDir}/.git/worktrees/my-branch\n`); + + const mounts = await buildMounts({ + ...baseRequest, + worktreePath: worktreeDir, + repoRoot: repoDir, + }); + + // Should mount canonical .git dir + const gitDirMount = mounts.find((m) => m.target === '/repo/.git'); + expect(gitDirMount).toBeDefined(); + expect(gitDirMount!.source).toBe(join(repoDir, '.git')); + + // Should mount patched .git file + const gitFileMount = mounts.find((m) => m.target === '/studio/.git'); + expect(gitFileMount).toBeDefined(); + expect(gitFileMount!.readOnly).toBe(true); + + // Patched .git file should point to container path + const patchedContent = readFileSync(gitFileMount!.source, 'utf-8'); + expect(patchedContent).toBe('gitdir: /repo/.git/worktrees/my-branch\n'); + }); + + it('skips git worktree mounts when .git is a directory', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'repo-git-dir-')); + mkdirSync(join(tmpDir, '.git'), { recursive: true }); + + const mounts = await buildMounts({ + ...baseRequest, + worktreePath: tmpDir, + repoRoot: tmpDir, + }); + + // No /repo/.git mount needed — .git dir is inside the bind mount + expect(mounts.find((m) => m.target === '/repo/.git')).toBeUndefined(); + }); +}); + +describe('patchMcpConfig', () => { + it('rewrites localhost URLs to host.docker.internal', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mcp-patch-')); + writeFileSync( + join(tmpDir, '.mcp.json'), + JSON.stringify({ + mcpServers: { + inkwell: { type: 'http', url: 'http://localhost:3001/mcp' }, + }, + }) + ); + + const result = await patchMcpConfig(tmpDir); + expect(result).toBeTruthy(); + const patched = JSON.parse(readFileSync(result!, 'utf-8')); + expect(patched.mcpServers.inkwell.url).toBe('http://host.docker.internal:3001/mcp'); + }); + + it('strips stdio/command-based servers', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mcp-patch-')); + writeFileSync( + join(tmpDir, '.mcp.json'), + JSON.stringify({ + mcpServers: { + inkwell: { type: 'http', url: 'http://localhost:3001/mcp' }, + inkmail: { command: 'npx', args: ['tsx', 'packages/channel-plugin/index.ts'] }, + playwright: { type: 'stdio', command: 'npx', args: ['@playwright/mcp'] }, + }, + }) + ); + + const result = await patchMcpConfig(tmpDir); + expect(result).toBeTruthy(); + const patched = JSON.parse(readFileSync(result!, 'utf-8')); + expect(Object.keys(patched.mcpServers)).toEqual(['inkwell']); + expect(patched.mcpServers.inkmail).toBeUndefined(); + expect(patched.mcpServers.playwright).toBeUndefined(); + }); + + it('preserves remote HTTP servers without rewriting', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mcp-patch-')); + writeFileSync( + join(tmpDir, '.mcp.json'), + JSON.stringify({ + mcpServers: { + github: { + type: 'http', + url: 'https://api.githubcopilot.com/mcp/', + headers: { Authorization: 'Bearer token' }, + }, + inkwell: { type: 'http', url: 'http://localhost:3001/mcp' }, + }, + }) + ); + + const result = await patchMcpConfig(tmpDir); + expect(result).toBeTruthy(); + const patched = JSON.parse(readFileSync(result!, 'utf-8')); + expect(patched.mcpServers.github.url).toBe('https://api.githubcopilot.com/mcp/'); + expect(patched.mcpServers.github.headers).toEqual({ Authorization: 'Bearer token' }); + expect(patched.mcpServers.inkwell.url).toBe('http://host.docker.internal:3001/mcp'); + }); + + it('returns undefined when no HTTP servers exist', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mcp-patch-')); + writeFileSync( + join(tmpDir, '.mcp.json'), + JSON.stringify({ + mcpServers: { + playwright: { type: 'stdio', command: 'npx', args: ['@playwright/mcp'] }, + }, + }) + ); + + const result = await patchMcpConfig(tmpDir); + expect(result).toBeUndefined(); + }); + + it('returns undefined when .mcp.json does not exist', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'mcp-patch-')); + const result = await patchMcpConfig(tmpDir); + expect(result).toBeUndefined(); + }); + + it('writes to provided staging dir instead of worktree', async () => { + const studioDir = mkdtempSync(join(tmpdir(), 'studio-')); + const stagingDir = mkdtempSync(join(tmpdir(), 'staging-')); + writeFileSync( + join(studioDir, '.mcp.json'), + JSON.stringify({ + mcpServers: { + inkwell: { type: 'http', url: 'http://localhost:3001/mcp' }, + }, + }) + ); + + const result = await patchMcpConfig(studioDir, stagingDir); + expect(result).toBeTruthy(); + // Patched file should be in staging dir, not studio dir + expect(result!.startsWith(stagingDir)).toBe(true); + expect(existsSync(join(studioDir, '.ink'))).toBe(false); + }); +}); + +describe('stageClaudeDir', () => { + it('stages credentials from file when .credentials.json exists', async () => { + const tmpDir = mkdtempSync(join(tmpdir(), 'cred-stage-')); + // stageClaudeDir reads from the real homedir, so we test what it returns + const result = await stageClaudeDir(join(tmpDir, 'staging')); + // On this machine (macOS with active Claude session), should extract from keychain + // In CI or without credentials, returns undefined — both are valid + if (result) { + expect(result).toContain('claude-home'); + expect(existsSync(join(result, '.credentials.json'))).toBe(true); + const creds = JSON.parse(readFileSync(join(result, '.credentials.json'), 'utf-8')); + expect(creds.claudeAiOauth).toBeDefined(); + } + }); + + it('copies settings files when they exist', async () => { + const result = await stageClaudeDir(mkdtempSync(join(tmpdir(), 'cred-stage-'))); + if (result) { + // settings.json should be copied if it exists on the host + const hostSettings = join(homedir(), '.claude', 'settings.json'); + if (existsSync(hostSettings)) { + expect(existsSync(join(result, 'settings.json'))).toBe(true); + } + } + }); +}); + +describe('stageCodexDir', () => { + it('stages auth.json and patched config.toml when codex home exists', async () => { + const codexHome = join(homedir(), '.codex'); + if (!existsSync(join(codexHome, 'auth.json'))) return; // skip if no Codex auth + + const tmpDir = mkdtempSync(join(tmpdir(), 'codex-stage-')); + const result = await stageCodexDir(tmpDir); + expect(result).toBeDefined(); + expect(existsSync(join(result!, 'auth.json'))).toBe(true); + + const auth = JSON.parse(readFileSync(join(result!, 'auth.json'), 'utf-8')); + expect(auth.tokens).toBeDefined(); + }); + + it('rewrites loopback URLs in config.toml', async () => { + const codexHome = join(homedir(), '.codex'); + if (!existsSync(join(codexHome, 'config.toml'))) return; + + const tmpDir = mkdtempSync(join(tmpdir(), 'codex-stage-')); + const result = await stageCodexDir(tmpDir); + if (!result) return; + + const config = readFileSync(join(result, 'config.toml'), 'utf-8'); + // Loopback URLs should be rewritten + expect(config).not.toMatch(/url\s*=\s*"https?:\/\/localhost/); + if (config.includes('host.docker.internal')) { + expect(config).toContain('host.docker.internal'); + } + }); + + it('strips host-specific project paths and adds /studio', async () => { + const codexHome = join(homedir(), '.codex'); + if (!existsSync(join(codexHome, 'config.toml'))) return; + + const tmpDir = mkdtempSync(join(tmpdir(), 'codex-stage-')); + const result = await stageCodexDir(tmpDir); + if (!result) return; + + const config = readFileSync(join(result, 'config.toml'), 'utf-8'); + // Host paths stripped + expect(config).not.toContain('/Users/'); + // Container project added + expect(config).toContain('[projects."/studio"]'); + expect(config).toContain('trust_level = "trusted"'); + }); + + it('returns undefined when auth.json does not exist', async () => { + // stageCodexDir checks for ~/.codex/auth.json before creating staging dir + // On a machine without Codex auth, this returns undefined + const tmpDir = mkdtempSync(join(tmpdir(), 'codex-no-auth-')); + // We can't mock homedir easily, but verify the function doesn't throw + const result = await stageCodexDir(tmpDir); + // On this machine with Codex installed, it will succeed + expect(result === undefined || typeof result === 'string').toBe(true); + }); +}); + +describe('SandboxOrchestrator', () => { + beforeEach(() => { + vi.fn(); + }); + + describe('isRunning', () => { + it('returns true when docker inspect succeeds', async () => { + const orch = new SandboxOrchestrator({ dockerCommand: '/usr/bin/true' }); + // /usr/bin/true always exits 0 — simulates a found container + const result = await orch.isRunning('test-container'); + expect(result).toBe(true); + }); + + it('returns false when docker inspect fails', async () => { + const orch = new SandboxOrchestrator({ dockerCommand: '/usr/bin/false' }); + const result = await orch.isRunning('test-container'); + expect(result).toBe(false); + }); + }); +}); + +function DEFAULT_IMAGE_NAME(): string { + return 'inkwell:studio-sandbox'; +} diff --git a/packages/api/src/services/sandbox/orchestrator.ts b/packages/api/src/services/sandbox/orchestrator.ts new file mode 100644 index 00000000..43098d5b --- /dev/null +++ b/packages/api/src/services/sandbox/orchestrator.ts @@ -0,0 +1,701 @@ +/** + * Sandbox Orchestrator + * + * Manages Docker container lifecycle for sandboxed agent work. + * Called by the strategy service when an agent needs to be spun up + * in an isolated environment for autonomous task execution. + * + * Phase 1 (current): Prepares containers alongside the host-side agent + * session. The container is spun up with credentials and worktree mounted + * but strategy execution still runs on the host. Phase 2 will route + * strategy execution into the container itself. + * + * Design: builds docker run args from DB-sourced studio data (no filesystem + * dependency for planning). Shells out to Docker via child_process. + */ + +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { createHash } from 'crypto'; +import { readFile, writeFile, mkdir, mkdtemp, access, constants as fsConstants } from 'fs/promises'; +import { join } from 'path'; +import { homedir, platform, tmpdir } from 'os'; +import { logger } from '../../utils/logger.js'; + +const execFileAsync = promisify(execFile); + +const DEFAULT_IMAGE = 'inkwell:studio-sandbox'; +const CONTAINER_HOME = '/home/sb'; +const CONTAINER_RUNNER_FILES = '/run/ink'; +const CONTAINER_LABEL = 'ink.sandbox=true'; +const CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials'; + +export type BackendAuthName = 'claude' | 'codex' | 'gemini'; + +export interface SandboxMount { + source: string; + target: string; + readOnly: boolean; +} + +export interface SandboxSpinUpRequest { + userId: string; + agentId: string; + studioId: string; + studioSlug?: string; + worktreePath: string; + repoRoot: string; + branch?: string; + taskGroupId?: string; + taskGroupTitle?: string; + taskGroupContext?: string; + taskGroupThreadKey?: string; + serverUrl?: string; + image?: string; + backendAuth?: BackendAuthName[]; + networkMode?: 'default' | 'none'; + extraEnv?: Record; + /** Override the computed container name (useful for test fixtures, named containers) */ + containerName?: string; +} + +export interface SandboxSpinUpResult { + containerName: string; + success: boolean; + alreadyRunning?: boolean; + error?: string; +} + +export interface SandboxStatusResult { + containerName: string; + running: boolean; + image?: string; + startedAt?: string; + labels?: Record; +} + +/** + * Derive the host-side runner-files directory from a container name. + * Runners write temp files here; the orchestrator bind-mounts it as /run/ink. + */ +export function getRunnerFilesDir(containerName: string): string { + return join(homedir(), '.ink', 'runtime', 'sandbox', containerName, 'runner-files'); +} + +export function buildContainerName(request: SandboxSpinUpRequest): string { + if (request.containerName) return request.containerName; + const label = sanitizeSlug(request.studioSlug || request.agentId || 'studio'); + const parts = [request.worktreePath]; + if (request.taskGroupId) parts.push(request.taskGroupId); + const digest = createHash('sha256').update(parts.join(':')).digest('hex').slice(0, 8); + + if (request.taskGroupId && request.taskGroupTitle) { + const taskSlug = sanitizeSlug(request.taskGroupTitle); + return `ink-sandbox-${label}-${taskSlug}-${digest}`; + } + + return `ink-sandbox-${label}-${digest}`; +} + +function sanitizeSlug(value: string): string { + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 24); +} + +export function buildEnvVars(request: SandboxSpinUpRequest): Record { + const serverUrl = request.serverUrl || process.env.INK_SERVER_URL || 'http://localhost:3001'; + + const env: Record = { + HOME: CONTAINER_HOME, + AGENT_ID: request.agentId, + INK_SERVER_URL: rewriteLoopbackUrl(serverUrl), + INK_STUDIO_ID: request.studioId, + INK_SANDBOX: 'docker', + INK_STUDIO_PATH: '/studio', + INK_STUDIOS_PATH: '/studios', + }; + + if (request.taskGroupId) { + env.INK_TASK_GROUP_ID = request.taskGroupId; + } + if (request.taskGroupTitle) { + env.INK_TASK_GROUP_TITLE = request.taskGroupTitle; + } + if (request.taskGroupContext) { + env.INK_TASK_GROUP_CONTEXT = request.taskGroupContext; + } + if (request.taskGroupThreadKey) { + env.INK_TASK_GROUP_THREAD_KEY = request.taskGroupThreadKey; + } + if (request.branch) { + env.INK_BRANCH = request.branch; + } + + if (request.extraEnv) { + Object.assign(env, request.extraEnv); + } + + return env; +} + +function rewriteLoopbackUrl(rawUrl: string): string { + try { + const parsed = new URL(rawUrl); + if (['localhost', '127.0.0.1', '::1', '[::1]'].includes(parsed.hostname)) { + parsed.hostname = 'host.docker.internal'; + return parsed.toString().replace(/\/+$/, ''); + } + } catch { + // Not a URL — leave it + } + return rawUrl; +} + +// ============================================================================ +// Async helpers +// ============================================================================ + +async function fileExists(p: string): Promise { + try { + await access(p, fsConstants.F_OK); + return true; + } catch { + return false; + } +} + +// ============================================================================ +// Credential staging +// ============================================================================ + +/** + * Stage a Claude config directory for Docker mounting. + * + * Claude Code stores OAuth tokens in the macOS keychain (primary) with a + * file fallback at ~/.claude/.credentials.json. Docker containers can't + * access the keychain, so we build a staging directory with extracted + * credentials plus the config files Claude Code needs (settings, etc.). + * + * This replaces mounting ~/.claude directly — Docker can't overlay a file + * inside a read-only directory mount, so we stage everything into one dir. + * + * Returns the path to the staged directory, or undefined if credentials + * aren't available. + */ +export async function stageClaudeDir(stagingDir: string): Promise { + const home = homedir(); + const claudeHome = join(home, '.claude'); + const stagedDir = join(stagingDir, 'claude-home'); + await mkdir(stagedDir, { recursive: true }); + + // Copy config files Claude Code needs + const filesToCopy = ['settings.json', 'settings.local.json']; + for (const file of filesToCopy) { + const src = join(claudeHome, file); + if (await fileExists(src)) { + const content = await readFile(src, 'utf-8'); + await writeFile(join(stagedDir, file), content, { mode: 0o600 }); + } + } + + // Stage credentials: file fallback first, then keychain extraction + const credFile = join(claudeHome, '.credentials.json'); + if (await fileExists(credFile)) { + const content = await readFile(credFile, 'utf-8'); + await writeFile(join(stagedDir, '.credentials.json'), content, { mode: 0o600 }); + return stagedDir; + } + + if (platform() === 'darwin') { + try { + const { stdout: raw } = await execFileAsync( + 'security', + ['find-generic-password', '-s', CLAUDE_KEYCHAIN_SERVICE, '-w'], + { encoding: 'utf-8', timeout: 5_000 } + ); + + const data = JSON.parse(raw.trim()); + if (!data?.claudeAiOauth?.accessToken) return undefined; + + await writeFile(join(stagedDir, '.credentials.json'), JSON.stringify(data, null, 2) + '\n', { + mode: 0o600, + }); + return stagedDir; + } catch { + logger.debug('Could not extract Claude credentials from keychain'); + return undefined; + } + } + + return undefined; +} + +/** + * Stage a Codex config directory for Docker mounting. + * + * Codex stores credentials in ~/.codex/auth.json (file-based, no keychain + * on macOS by default). The config.toml contains host-specific project + * paths and MCP server URLs that need rewriting for container use. + * + * Returns the path to the staged directory, or undefined if no auth exists. + */ +export async function stageCodexDir(stagingDir: string): Promise { + const home = homedir(); + const codexHome = join(home, '.codex'); + if (!(await fileExists(codexHome))) return undefined; + + const authFile = join(codexHome, 'auth.json'); + if (!(await fileExists(authFile))) return undefined; + + const stagedDir = join(stagingDir, 'codex-home'); + await mkdir(stagedDir, { recursive: true }); + + // Copy auth.json as-is + const authContent = await readFile(authFile, 'utf-8'); + await writeFile(join(stagedDir, 'auth.json'), authContent, { mode: 0o600 }); + + // Patch config.toml: rewrite loopback MCP URLs, strip host-specific project paths + const configFile = join(codexHome, 'config.toml'); + if (await fileExists(configFile)) { + let config = await readFile(configFile, 'utf-8'); + + // Rewrite localhost/127.0.0.1 URLs to host.docker.internal + config = config.replace( + /url\s*=\s*"(https?:\/\/(?:localhost|127\.0\.0\.1|::1|\[::1\])(:\d+)?[^"]*)"/g, + (_match, url: string) => `url = "${rewriteLoopbackUrl(url)}"` + ); + + // Strip host-specific [projects.*] sections — they reference host paths + config = config.replace(/\[projects\."[^"]*"\]\s*\n(?:[^\[]*\n)*/g, ''); + + // Add container project as trusted + config += '\n[projects."/studio"]\ntrust_level = "trusted"\n'; + + await writeFile(join(stagedDir, 'config.toml'), config, { mode: 0o600 }); + } + + // Copy installation_id if present + const installId = join(codexHome, 'installation_id'); + if (await fileExists(installId)) { + const content = await readFile(installId, 'utf-8'); + await writeFile(join(stagedDir, 'installation_id'), content, { mode: 0o600 }); + } + + return stagedDir; +} + +// ============================================================================ +// MCP config patching +// ============================================================================ + +/** + * Patch .mcp.json for Docker: rewrite loopback URLs to host.docker.internal + * and strip stdio/command-based servers (they can't spawn inside the container). + * + * Writes the patched config to stagingDir (outside the worktree) to avoid + * exposing it inside the /studio mount. + */ +export async function patchMcpConfig( + studioPath: string, + stagingDir?: string +): Promise { + const sourcePath = join(studioPath, '.mcp.json'); + if (!(await fileExists(sourcePath))) return undefined; + + try { + const raw = await readFile(sourcePath, 'utf-8'); + const parsed = JSON.parse(raw) as { + mcpServers?: Record>; + }; + const servers = parsed.mcpServers; + if (!servers) return undefined; + + const patched: Record> = {}; + for (const [name, server] of Object.entries(servers)) { + // Only keep HTTP transport servers — stdio/command servers can't run in the container + if (server?.type === 'http' && typeof server.url === 'string') { + patched[name] = { ...server, url: rewriteLoopbackUrl(server.url) }; + } + } + + if (Object.keys(patched).length === 0) return undefined; + + const outDir = stagingDir || (await mkdtemp(join(tmpdir(), 'ink-mcp-'))); + await mkdir(outDir, { recursive: true }); + const targetPath = join(outDir, 'mcp.docker.json'); + await writeFile(targetPath, JSON.stringify({ mcpServers: patched }, null, 2) + '\n', 'utf-8'); + return targetPath; + } catch { + return undefined; + } +} + +// ============================================================================ +// Git worktree resolution +// ============================================================================ + +/** + * Resolve additional mounts needed for git worktrees. + * + * In a worktree, .git is a file containing "gitdir: " rather than + * a directory. The referenced path points to repoRoot/.git/worktrees/, + * which in turn references the shared objects/refs. Docker bind-mounting + * the worktree alone breaks git because the canonical .git dir isn't visible. + * + * Fix: mount the canonical .git dir and overlay a patched .git file that + * uses container-relative paths. + */ +async function resolveGitMounts( + worktreePath: string, + repoRoot: string, + stagingDir: string +): Promise { + const gitPath = join(worktreePath, '.git'); + + try { + await access(gitPath, fsConstants.F_OK); + } catch { + return []; + } + + try { + const content = await readFile(gitPath, 'utf-8'); + + // .git file format: "gitdir: \n" + const match = content.match(/^gitdir:\s*(.+)$/m); + if (!match) return []; + + const hostGitDir = match[1].trim(); + const worktreeMatch = hostGitDir.match(/\.git\/worktrees\/(.+)$/); + if (!worktreeMatch) return []; + + const worktreeName = worktreeMatch[1]; + const canonicalGitDir = join(repoRoot, '.git'); + + if (!(await fileExists(canonicalGitDir))) return []; + + const mounts: SandboxMount[] = [ + { source: canonicalGitDir, target: '/repo/.git', readOnly: false }, + ]; + + // Create a patched .git file pointing to the container-mapped path + const patchedGitFile = join(stagingDir, 'dotgit'); + await writeFile(patchedGitFile, `gitdir: /repo/.git/worktrees/${worktreeName}\n`); + mounts.push({ source: patchedGitFile, target: '/studio/.git', readOnly: true }); + + return mounts; + } catch { + return []; + } +} + +// ============================================================================ +// Mount + arg builders +// ============================================================================ + +export async function buildMounts( + request: SandboxSpinUpRequest, + stagingDir?: string +): Promise { + const effectiveDir = stagingDir || (await mkdtemp(join(tmpdir(), 'ink-sandbox-'))); + const mounts: SandboxMount[] = []; + + if (await fileExists(request.worktreePath)) { + mounts.push({ source: request.worktreePath, target: '/studio', readOnly: false }); + } + + // Runner temp files — runners write here on the host, visible at /run/ink inside the container + const runnerFilesDir = join(effectiveDir, 'runner-files'); + await mkdir(runnerFilesDir, { recursive: true }); + mounts.push({ source: runnerFilesDir, target: CONTAINER_RUNNER_FILES, readOnly: false }); + + // Mount patched MCP config if it exists + const patchedMcpPath = await patchMcpConfig(request.worktreePath, effectiveDir); + if (patchedMcpPath) { + mounts.push({ source: patchedMcpPath, target: '/studio/.mcp.json', readOnly: true }); + } + + // Resolve git worktree mounts (canonical .git dir + patched .git file) + const gitMounts = await resolveGitMounts(request.worktreePath, request.repoRoot, effectiveDir); + mounts.push(...gitMounts); + + // Backend auth dirs (read-only) + const home = homedir(); + const authDirs: Record = { + claude: join(home, '.claude'), + codex: join(home, '.codex'), + gemini: join(home, '.gemini'), + }; + + for (const backend of request.backendAuth || []) { + if (backend === 'claude') { + // Stage ~/.claude with credentials extracted from keychain. + // We can't mount ~/.claude read-only and then overlay a file inside it, + // so we stage everything into one directory. + const stagedClaudeHome = await stageClaudeDir(effectiveDir); + if (stagedClaudeHome) { + mounts.push({ + source: stagedClaudeHome, + target: `${CONTAINER_HOME}/.claude`, + readOnly: true, + }); + } else { + // Fallback: mount ~/.claude directly (no credentials, but settings work) + const sourceDir = authDirs[backend]; + if (await fileExists(sourceDir)) { + mounts.push({ + source: sourceDir, + target: `${CONTAINER_HOME}/.claude`, + readOnly: true, + }); + } + } + + // Claude Code also needs ~/.claude.json (config separate from ~/.claude/ dir) + const claudeJson = join(home, '.claude.json'); + if (await fileExists(claudeJson)) { + mounts.push({ + source: claudeJson, + target: `${CONTAINER_HOME}/.claude.json`, + readOnly: true, + }); + } + } else if (backend === 'codex') { + // Stage ~/.codex with patched config.toml (rewrite loopback URLs, strip host paths) + const stagedCodexHome = await stageCodexDir(effectiveDir); + if (stagedCodexHome) { + mounts.push({ + source: stagedCodexHome, + target: `${CONTAINER_HOME}/.codex`, + readOnly: true, + }); + } else { + const sourceDir = authDirs[backend]; + if (await fileExists(sourceDir)) { + mounts.push({ + source: sourceDir, + target: `${CONTAINER_HOME}/.codex`, + readOnly: true, + }); + } + } + } else { + const sourceDir = authDirs[backend]; + if (await fileExists(sourceDir)) { + mounts.push({ + source: sourceDir, + target: `${CONTAINER_HOME}/.${backend}`, + readOnly: true, + }); + } + } + } + + return mounts; +} + +export async function buildDockerRunArgs(request: SandboxSpinUpRequest): Promise { + const containerName = buildContainerName(request); + const image = request.image || DEFAULT_IMAGE; + const env = buildEnvVars(request); + + // Staging dir outside the worktree — credentials and config land here + const stagingDir = join(homedir(), '.ink', 'runtime', 'sandbox', containerName); + await mkdir(stagingDir, { recursive: true }); + + const mounts = await buildMounts(request, stagingDir); + + const args = ['run', '--rm', '-d', '--name', containerName]; + args.push('--workdir', '/studio'); + args.push('--add-host', 'host.docker.internal:host-gateway'); + args.push('--hostname', containerName); + + // Labels for discovery and lifecycle management + args.push('--label', CONTAINER_LABEL); + args.push('--label', `ink.agent-id=${request.agentId}`); + args.push('--label', `ink.studio-id=${request.studioId}`); + if (request.taskGroupId) { + args.push('--label', `ink.task-group-id=${request.taskGroupId}`); + } + + // Preserve host user for file ownership + const uid = typeof process.getuid === 'function' ? process.getuid() : undefined; + const gid = typeof process.getgid === 'function' ? process.getgid() : undefined; + if (uid !== undefined && gid !== undefined) { + args.push('--user', `${uid}:${gid}`); + } + + if (request.networkMode === 'none') { + args.push('--network', 'none'); + } + + for (const [key, value] of Object.entries(env)) { + args.push('-e', `${key}=${value}`); + } + + for (const mount of mounts) { + const ro = mount.readOnly ? ',readonly' : ''; + args.push('--mount', `type=bind,src=${mount.source},dst=${mount.target}${ro}`); + } + + args.push(image); + return args; +} + +// ============================================================================ +// Orchestrator Class +// ============================================================================ + +export class SandboxOrchestrator { + private dockerCommand: string; + + constructor(options: { dockerCommand?: string } = {}) { + this.dockerCommand = options.dockerCommand || 'docker'; + } + + async spinUp(request: SandboxSpinUpRequest): Promise { + const containerName = buildContainerName(request); + + // Check if already running + const alreadyRunning = await this.isRunning(containerName); + if (alreadyRunning) { + logger.info(`Sandbox already running: ${containerName}`); + return { containerName, success: true, alreadyRunning: true }; + } + + const args = await buildDockerRunArgs(request); + + try { + await execFileAsync(this.dockerCommand, args, { timeout: 30_000 }); + + // Wait for the container to be ready to accept exec calls + const ready = await this.waitReady(containerName); + if (!ready) { + logger.warn('Sandbox started but readiness check timed out', { containerName }); + } + + logger.info('Sandbox container started', { + containerName, + agentId: request.agentId, + studioId: request.studioId, + taskGroupId: request.taskGroupId, + }); + return { containerName, success: true }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + logger.error('Sandbox spin-up failed', { containerName, error: message }); + return { containerName, success: false, error: message }; + } + } + + /** + * Poll until the container can accept exec calls. Returns false on timeout. + */ + async waitReady(containerName: string, timeoutMs = 5_000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + await execFileAsync(this.dockerCommand, ['exec', containerName, 'true'], { + timeout: 2_000, + }); + return true; + } catch { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + return false; + } + + async stop(containerName: string): Promise { + try { + await execFileAsync(this.dockerCommand, ['rm', '-f', containerName], { timeout: 15_000 }); + logger.info(`Sandbox stopped: ${containerName}`); + return true; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + logger.warn(`Sandbox stop failed: ${containerName}`, { error: message }); + return false; + } + } + + async isRunning(containerName: string): Promise { + try { + await execFileAsync(this.dockerCommand, ['container', 'inspect', containerName], { + timeout: 5_000, + }); + return true; + } catch { + return false; + } + } + + async getStatus(containerName: string): Promise { + try { + const { stdout } = await execFileAsync( + this.dockerCommand, + ['container', 'inspect', '--format', '{{json .}}', containerName], + { timeout: 5_000 } + ); + const info = JSON.parse(stdout.trim()); + return { + containerName, + running: info.State?.Running === true, + image: info.Config?.Image, + startedAt: info.State?.StartedAt, + labels: info.Config?.Labels, + }; + } catch { + return { containerName, running: false }; + } + } + + async listSandboxes(): Promise { + try { + const { stdout } = await execFileAsync( + this.dockerCommand, + ['ps', '--filter', 'label=ink.sandbox=true', '--format', '{{json .}}'], + { timeout: 10_000 } + ); + + if (!stdout.trim()) return []; + + return stdout + .trim() + .split('\n') + .map((line) => { + const info = JSON.parse(line); + return { + containerName: info.Names, + running: info.State === 'running', + image: info.Image, + labels: info.Labels ? parseLabelString(info.Labels) : undefined, + }; + }); + } catch { + return []; + } + } + + async exec( + containerName: string, + command: string[] + ): Promise<{ stdout: string; stderr: string }> { + return execFileAsync(this.dockerCommand, ['exec', containerName, ...command], { + timeout: 60_000, + }); + } +} + +function parseLabelString(labels: string): Record { + const result: Record = {}; + for (const pair of labels.split(',')) { + const eqIdx = pair.indexOf('='); + if (eqIdx > 0) { + result[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1); + } + } + return result; +} diff --git a/packages/api/src/services/sessions/claude-runner.ts b/packages/api/src/services/sessions/claude-runner.ts index f898b976..48992d9a 100644 --- a/packages/api/src/services/sessions/claude-runner.ts +++ b/packages/api/src/services/sessions/claude-runner.ts @@ -19,7 +19,13 @@ import type { import { formatInjectedContext } from './context-builder.js'; import { logger } from '../../utils/logger.js'; import { resolveBinaryPath, buildSpawnPath } from './resolve-binary.js'; -import { injectSessionHeaders, buildSessionEnv, writeRuntimeSessionHint } from '@inklabs/shared'; +import { + injectSessionHeaders, + buildSessionEnv, + writeRuntimeSessionHint, + resolveSpawnTarget, + CONTAINER_RUNNER_FILES, +} from '@inklabs/shared'; import { ensureStudioSettings, applyPermissionOverlay } from '../studio-settings.js'; /** Maximum time (ms) to wait for a Claude Code subprocess before killing it. @@ -193,6 +199,7 @@ export class ClaudeRunner implements IRunner { pcpSessionId: config.pcpSessionId, studioId: config.studioId, accessToken: config.pcpAccessToken, + outputDir: config.container?.runtimeDir, }) : null; @@ -226,11 +233,17 @@ export class ClaudeRunner implements IRunner { } } - // If headers were injected, patch the --mcp-config arg to point to the temp file + // If headers were injected, patch the --mcp-config arg to point to the temp file. + // When containerized, translate host path to the container-side mount point. if (mcpInjection?.modified) { const mcpIdx = args.indexOf('--mcp-config'); if (mcpIdx !== -1 && args[mcpIdx + 1]) { - args[mcpIdx + 1] = mcpInjection.mcpConfigPath; + if (config.container?.runtimeDir) { + const filename = mcpInjection.mcpConfigPath.split('/').pop()!; + args[mcpIdx + 1] = `${CONTAINER_RUNNER_FILES}/${filename}`; + } else { + args[mcpIdx + 1] = mcpInjection.mcpConfigPath; + } } } @@ -238,30 +251,38 @@ export class ClaudeRunner implements IRunner { // Strip CLAUDECODE to prevent "nested session" detection when PCP is // launched from inside a Claude Code session (e.g., via PM2). const { CLAUDECODE, ...cleanEnv } = process.env; - const proc = spawn(claudeBin, args, { + const spawnEnv: Record = { + // Ensure Claude Code uses correct paths + HOME: process.env.HOME || '', + PATH: buildSpawnPath(claudeBin), + // Agent identity — hooks resolve identity from $AGENT_ID. + ...(config.agentId ? { AGENT_ID: config.agentId } : {}), + // Session env vars + ...buildSessionEnv({ + pcpSessionId: config.pcpSessionId, + runtimeLinkId: config.pcpSessionId ? runtimeLinkId : undefined, + studioId: config.studioId, + accessToken: config.pcpAccessToken, + agentId: config.agentId, + runtime: 'claude', + repoRoot: config.repoRoot, + }), + }; + + // Route through container or host — resolveSpawnTarget handles the + // docker exec wrapping transparently. + const target = resolveSpawnTarget({ + binary: claudeBin, + args, cwd: config.workingDirectory, - env: { - ...cleanEnv, - // Ensure Claude Code uses correct paths - HOME: process.env.HOME, - PATH: buildSpawnPath(claudeBin), - // Agent identity — hooks resolve identity from $AGENT_ID. - // Without this, hooks in cross-agent studios (e.g., Myra triggered - // in Wren's worktree) fall back to .ink/identity.json and get the - // wrong agent ID. - ...(config.agentId ? { AGENT_ID: config.agentId } : {}), - // Session env vars: INK_SESSION_ID for ${VAR} interpolation in - // .mcp.json headers, INK_RUNTIME_LINK_ID for hook hint matching. - ...buildSessionEnv({ - pcpSessionId: config.pcpSessionId, - runtimeLinkId: config.pcpSessionId ? runtimeLinkId : undefined, - studioId: config.studioId, - accessToken: config.pcpAccessToken, - agentId: config.agentId, - runtime: 'claude', - repoRoot: config.repoRoot, - }), - }, + env: spawnEnv, + pipeStdin: true, + container: config.container, + }); + + const proc = spawn(target.binary, target.args, { + cwd: target.cwd, + env: config.container ? target.env : { ...cleanEnv, ...spawnEnv }, stdio: ['pipe', 'pipe', 'pipe'], }); diff --git a/packages/api/src/services/sessions/codex-runner.test.ts b/packages/api/src/services/sessions/codex-runner.test.ts index 131ecb29..873d47e4 100644 --- a/packages/api/src/services/sessions/codex-runner.test.ts +++ b/packages/api/src/services/sessions/codex-runner.test.ts @@ -1,5 +1,8 @@ -import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; import { EventEmitter } from 'events'; +import { mkdtempSync, existsSync, readdirSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; vi.mock('child_process', async (importOriginal) => { const actual = await importOriginal(); @@ -406,6 +409,124 @@ describe('CodexRunner', () => { expect(result.backendSessionId).toBe(codexSessionId); }); + describe('container path translation', () => { + let runtimeDir: string; + + beforeEach(() => { + runtimeDir = mkdtempSync(join(tmpdir(), 'codex-container-test-')); + }); + + afterEach(() => { + try { + rmSync(runtimeDir, { recursive: true, force: true }); + } catch { + // best effort + } + }); + + it('uses /run/ink/ container path when config.container.runtimeDir is set', async () => { + const mockProc = createMockProcess(); + (spawn as Mock).mockReturnValue(mockProc); + + const runner = new CodexRunner(); + const runPromise = runner.run('hello', { + config: { + workingDirectory: process.cwd(), + mcpConfigPath: '', + model: 'gpt-5-codex', + appendSystemPrompt: 'test identity prompt', + container: { + containerName: 'ink-sandbox-test-abc', + runtimeDir, + }, + }, + }); + + setTimeout(() => { + mockProc.stdout.emit('data', Buffer.from(`${JSON.stringify({ result: 'ok' })}\n`)); + mockProc.emit('close', 0); + }, 5); + + await runPromise; + + expect(spawn).toHaveBeenCalledTimes(1); + const [, args] = (spawn as Mock).mock.calls[0] as [string, string[]]; + + // The model_instructions_file arg should point to /run/ink/ (container path) + const configArg = args.find( + (a: string) => typeof a === 'string' && a.startsWith('model_instructions_file=') + ); + expect(configArg).toBeDefined(); + expect(configArg).toMatch(/^model_instructions_file=\/run\/ink\//); + }); + + it('writes the identity prompt file to runtimeDir on the host', async () => { + const mockProc = createMockProcess(); + (spawn as Mock).mockReturnValue(mockProc); + + const runner = new CodexRunner(); + const runPromise = runner.run('hello', { + config: { + workingDirectory: process.cwd(), + mcpConfigPath: '', + model: 'gpt-5-codex', + appendSystemPrompt: 'host-side identity content', + container: { + containerName: 'ink-sandbox-test-abc', + runtimeDir, + }, + }, + }); + + setTimeout(() => { + mockProc.stdout.emit('data', Buffer.from(`${JSON.stringify({ result: 'ok' })}\n`)); + mockProc.emit('close', 0); + }, 5); + + await runPromise; + + // Verify the file was written to the runtimeDir (host-side) + const files = readdirSync(runtimeDir); + const identityFile = files.find((f) => f.startsWith('identity-')); + expect(identityFile).toBeDefined(); + }); + + it('uses /tmp/ path when runtimeDir is NOT set', async () => { + const mockProc = createMockProcess(); + (spawn as Mock).mockReturnValue(mockProc); + + const runner = new CodexRunner(); + const runPromise = runner.run('hello', { + config: { + workingDirectory: process.cwd(), + mcpConfigPath: '', + model: 'gpt-5-codex', + appendSystemPrompt: 'test identity prompt', + // No container config + }, + }); + + setTimeout(() => { + mockProc.stdout.emit('data', Buffer.from(`${JSON.stringify({ result: 'ok' })}\n`)); + mockProc.emit('close', 0); + }, 5); + + await runPromise; + + expect(spawn).toHaveBeenCalledTimes(1); + const [, args] = (spawn as Mock).mock.calls[0] as [string, string[]]; + + // Without container, the model_instructions_file should NOT use /run/ink/ + const configArg = args.find( + (a: string) => typeof a === 'string' && a.startsWith('model_instructions_file=') + ); + expect(configArg).toBeDefined(); + expect(configArg).not.toMatch(/\/run\/ink\//); + // Should be a host-side tmp path + expect(configArg).toMatch(/model_instructions_file=\//); + }); + }); + it('includes parsed startup events in diagnostics when codex exits non-zero without stderr', async () => { const mockProc = createMockProcess(); (spawn as Mock).mockReturnValue(mockProc); diff --git a/packages/api/src/services/sessions/codex-runner.ts b/packages/api/src/services/sessions/codex-runner.ts index 82eb2641..2b70ef15 100644 --- a/packages/api/src/services/sessions/codex-runner.ts +++ b/packages/api/src/services/sessions/codex-runner.ts @@ -22,7 +22,12 @@ import type { import { formatInjectedContext } from './context-builder.js'; import { logger } from '../../utils/logger.js'; import { resolveBinaryPath, buildSpawnPath } from './resolve-binary.js'; -import { buildSessionEnv, writeRuntimeSessionHint } from '@inklabs/shared'; +import { + buildSessionEnv, + writeRuntimeSessionHint, + resolveSpawnTarget, + CONTAINER_RUNNER_FILES, +} from '@inklabs/shared'; /** Maximum time (ms) to wait for a Codex CLI subprocess before killing it. * Override with CODEX_PROCESS_TIMEOUT_MS env var. */ @@ -55,15 +60,22 @@ export class CodexRunner implements IRunner { fullMessage = `${contextBlock}\n\n---\n\n${message}`; } - const { promptPath, cleanup } = this.createIdentityPromptTempFile( - config.appendSystemPrompt || config.systemPrompt || '' + const { promptPath, containerPath, cleanup } = this.createIdentityPromptTempFile( + config.appendSystemPrompt || config.systemPrompt || '', + config.container?.runtimeDir ); try { // Only pass a session ID to buildArgs when resuming a known backend session. // For fresh runs, Codex assigns its own session UUID — we extract it from stdout. const argsSessionId = isResume ? backendSessionId! : undefined; - const args = this.buildArgs(argsSessionId, isResume, fullMessage, config, promptPath); + const args = this.buildArgs( + argsSessionId, + isResume, + fullMessage, + config, + containerPath || promptPath + ); logger.info('Spawning Codex CLI', { resumeSessionId: argsSessionId || null, isResume, @@ -184,23 +196,32 @@ export class CodexRunner implements IRunner { return new Promise((resolve, reject) => { // Strip CLAUDECODE to prevent env leaking into subprocess const { CLAUDECODE, ...cleanEnv } = process.env; - const proc = spawn(codexBin, args, { + const spawnEnv: Record = { + HOME: process.env.HOME || '', + PATH: buildSpawnPath(codexBin), + ...(config.agentId ? { AGENT_ID: config.agentId } : {}), + ...buildSessionEnv({ + pcpSessionId: config.pcpSessionId, + runtimeLinkId: config.pcpSessionId ? runtimeLinkId : undefined, + studioId: config.studioId, + accessToken: config.pcpAccessToken, + agentId: config.agentId, + runtime: 'codex', + repoRoot: config.repoRoot, + }), + }; + + const target = resolveSpawnTarget({ + binary: codexBin, + args, cwd: config.workingDirectory, - env: { - ...cleanEnv, - HOME: process.env.HOME, - PATH: buildSpawnPath(codexBin), - ...(config.agentId ? { AGENT_ID: config.agentId } : {}), - ...buildSessionEnv({ - pcpSessionId: config.pcpSessionId, - runtimeLinkId: config.pcpSessionId ? runtimeLinkId : undefined, - studioId: config.studioId, - accessToken: config.pcpAccessToken, - agentId: config.agentId, - runtime: 'codex', - repoRoot: config.repoRoot, - }), - }, + env: spawnEnv, + container: config.container, + }); + + const proc = spawn(target.binary, target.args, { + cwd: target.cwd, + env: config.container ? target.env : { ...cleanEnv, ...spawnEnv }, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -430,20 +451,28 @@ export class CodexRunner implements IRunner { } } - private createIdentityPromptTempFile(content: string): { + private createIdentityPromptTempFile( + content: string, + runtimeDir?: string + ): { promptPath: string; + containerPath?: string; cleanup: () => void; } { - const dir = mkdtempSync(join(tmpdir(), 'pcp-codex-')); - const promptPath = join(dir, 'identity.md'); + const dir = runtimeDir || mkdtempSync(join(tmpdir(), 'pcp-codex-')); + const filename = `identity-${process.pid}-${Date.now()}.md`; + const promptPath = join(dir, filename); writeFileSync(promptPath, content || 'Follow system identity instructions.'); return { promptPath, + containerPath: runtimeDir ? `${CONTAINER_RUNNER_FILES}/${filename}` : undefined, cleanup: () => { - try { - rmSync(dir, { recursive: true, force: true }); - } catch { - // ignore cleanup errors + if (!runtimeDir) { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // ignore cleanup errors + } } }, }; diff --git a/packages/api/src/services/sessions/gemini-runner.ts b/packages/api/src/services/sessions/gemini-runner.ts index d4c0aa38..b2b9b24e 100644 --- a/packages/api/src/services/sessions/gemini-runner.ts +++ b/packages/api/src/services/sessions/gemini-runner.ts @@ -26,7 +26,7 @@ import type { import { formatInjectedContext } from './context-builder.js'; import { logger } from '../../utils/logger.js'; import { resolveBinaryPath, buildSpawnPath } from './resolve-binary.js'; -import { buildSessionEnv } from '@inklabs/shared'; +import { buildSessionEnv, resolveSpawnTarget, CONTAINER_RUNNER_FILES } from '@inklabs/shared'; /** Maximum time (ms) to wait for a Gemini CLI subprocess before killing it. * Override with GEMINI_PROCESS_TIMEOUT_MS env var. */ @@ -62,8 +62,13 @@ export class GeminiRunner implements IRunner { } // Optionally write system prompt to a temp policy file - const { policyPath, cleanup } = this.createPolicyTempFile( - config.appendSystemPrompt || config.systemPrompt || '' + const { + policyPath, + containerPath: containerPolicyPath, + cleanup, + } = this.createPolicyTempFile( + config.appendSystemPrompt || config.systemPrompt || '', + config.container?.runtimeDir ); // Build Gemini system settings with PCP MCP server config (including auth). @@ -71,7 +76,8 @@ export class GeminiRunner implements IRunner { // We use GEMINI_CLI_SYSTEM_SETTINGS_PATH to point to a temp settings file // that overrides the mcpServers section. Other user settings (model, auth, // etc.) are preserved since system settings only override matching keys. - let geminiSettingsPath: string | undefined; + let geminiSettingsEnvPath: string | undefined; + let geminiSettingsHostPath: string | undefined; if (config.pcpAccessToken) { const mcpJsonPath = join(config.workingDirectory, '.mcp.json'); // Start from workspace .mcp.json servers (includes supabase, github, etc.) @@ -101,12 +107,16 @@ export class GeminiRunner implements IRunner { }, }; - const settingsDir = join(tmpdir(), 'sb-gemini'); + const settingsDir = config.container?.runtimeDir || join(tmpdir(), 'sb-gemini'); mkdirSync(settingsDir, { recursive: true }); - const settingsFile = join(settingsDir, `settings-${process.pid}-${Date.now()}.json`); + const settingsFilename = `settings-${process.pid}-${Date.now()}.json`; + const settingsFile = join(settingsDir, settingsFilename); try { writeFileSync(settingsFile, JSON.stringify({ mcpServers }, null, 2)); - geminiSettingsPath = settingsFile; + geminiSettingsHostPath = settingsFile; + geminiSettingsEnvPath = config.container?.runtimeDir + ? `${CONTAINER_RUNNER_FILES}/${settingsFilename}` + : settingsFile; } catch (err) { logger.warn('Failed to write Gemini system settings', { error: err instanceof Error ? err.message : String(err), @@ -115,20 +125,23 @@ export class GeminiRunner implements IRunner { } try { - const args = this.buildArgs(fullMessage, config, policyPath, backendSessionId); + const effectivePolicyPath = containerPolicyPath || policyPath; + const args = this.buildArgs(fullMessage, config, effectivePolicyPath, backendSessionId); logger.info('Spawning Gemini CLI', { isResume, backendSessionId: backendSessionId || '(new)', workingDirectory: config.workingDirectory, messageLength: fullMessage.length, hasPcpAccessToken: !!config.pcpAccessToken, - geminiSettingsOverride: !!geminiSettingsPath, + geminiSettingsOverride: !!geminiSettingsEnvPath, }); const result = await this.spawnProcess( args, config, - geminiSettingsPath ? { GEMINI_CLI_SYSTEM_SETTINGS_PATH: geminiSettingsPath } : undefined + geminiSettingsEnvPath + ? { GEMINI_CLI_SYSTEM_SETTINGS_PATH: geminiSettingsEnvPath } + : undefined ); // Use session ID from Gemini's init event, fall back to the one we passed in @@ -154,10 +167,10 @@ export class GeminiRunner implements IRunner { error: error instanceof Error ? error.message : 'Unknown error', }; } finally { - // Clean up temp Gemini settings file - if (geminiSettingsPath) { + // Clean up temp Gemini settings file (host path, not container path) + if (geminiSettingsHostPath && !config.container?.runtimeDir) { try { - rmSync(geminiSettingsPath, { force: true }); + rmSync(geminiSettingsHostPath, { force: true }); } catch { // best-effort cleanup } @@ -204,23 +217,32 @@ export class GeminiRunner implements IRunner { return new Promise((resolve, reject) => { // Strip CLAUDECODE to prevent env leaking into subprocess const { CLAUDECODE, ...cleanEnv } = process.env; - const proc = spawn(geminiBin, args, { + const spawnEnv: Record = { + HOME: process.env.HOME || '', + PATH: buildSpawnPath(geminiBin), + ...(config.agentId ? { AGENT_ID: config.agentId } : {}), + ...(extraEnv || {}), + ...buildSessionEnv({ + pcpSessionId: config.pcpSessionId, + studioId: config.studioId, + accessToken: config.pcpAccessToken, + agentId: config.agentId, + runtime: 'gemini', + repoRoot: config.repoRoot, + }), + }; + + const target = resolveSpawnTarget({ + binary: geminiBin, + args, cwd: config.workingDirectory, - env: { - ...cleanEnv, - HOME: process.env.HOME, - PATH: buildSpawnPath(geminiBin), - ...(config.agentId ? { AGENT_ID: config.agentId } : {}), - ...(extraEnv || {}), - ...buildSessionEnv({ - pcpSessionId: config.pcpSessionId, - studioId: config.studioId, - accessToken: config.pcpAccessToken, - agentId: config.agentId, - runtime: 'gemini', - repoRoot: config.repoRoot, - }), - }, + env: spawnEnv, + container: config.container, + }); + + const proc = spawn(target.binary, target.args, { + cwd: target.cwd, + env: config.container ? target.env : { ...cleanEnv, ...spawnEnv }, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -535,25 +557,33 @@ export class GeminiRunner implements IRunner { /** * Write system prompt / identity to a temp policy file for Gemini CLI. */ - private createPolicyTempFile(content: string): { + private createPolicyTempFile( + content: string, + runtimeDir?: string + ): { policyPath: string | undefined; + containerPath?: string; cleanup: () => void; } { if (!content) { return { policyPath: undefined, cleanup: () => {} }; } - const dir = mkdtempSync(join(tmpdir(), 'gemini-policy-')); - const policyPath = join(dir, 'identity-policy.md'); + const dir = runtimeDir || mkdtempSync(join(tmpdir(), 'gemini-policy-')); + const filename = `identity-policy-${process.pid}-${Date.now()}.md`; + const policyPath = join(dir, filename); writeFileSync(policyPath, content, 'utf-8'); return { policyPath, + containerPath: runtimeDir ? `${CONTAINER_RUNNER_FILES}/${filename}` : undefined, cleanup: () => { - try { - rmSync(dir, { recursive: true, force: true }); - } catch { - // Best effort cleanup + if (!runtimeDir) { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // Best effort cleanup + } } }, }; diff --git a/packages/api/src/services/sessions/index.ts b/packages/api/src/services/sessions/index.ts index 15ef59c7..71dfcef1 100644 --- a/packages/api/src/services/sessions/index.ts +++ b/packages/api/src/services/sessions/index.ts @@ -25,6 +25,7 @@ export { ContextBuilder, formatInjectedContext } from './context-builder.js'; export { ClaudeRunner, buildIdentityPrompt } from './claude-runner.js'; export { CodexRunner } from './codex-runner.js'; export { GeminiRunner } from './gemini-runner.js'; +export { InkRunner } from './ink-runner.js'; // Types export type { diff --git a/packages/api/src/services/sessions/ink-runner.test.ts b/packages/api/src/services/sessions/ink-runner.test.ts new file mode 100644 index 00000000..273fa26e --- /dev/null +++ b/packages/api/src/services/sessions/ink-runner.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi } from 'vitest'; +import { InkRunner } from './ink-runner'; +import type { InkToolDefinition } from '../../agent/tools/pi-coding-tools'; + +vi.mock('../../utils/logger', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, +})); + +describe('InkRunner', () => { + describe('agentId propagation', () => { + it('creates guarded bash tools when agentId is provided', async () => { + const runner = new InkRunner({ apiKey: 'test-key' }); + + // Access private getTools to verify agentId propagation + const tools: InkToolDefinition[] = await (runner as any).getTools('/tmp', 'wren'); + const bash = tools.find((t) => t.schema.name === 'bash')!; + + // If agentId propagated, the guard blocks dangerous commands + const result = await bash.execute({ command: ':(){ :|:& };:' }); + expect(result).toContain('fork bomb'); + }); + + it('creates unguarded bash tools when agentId is absent', async () => { + const runner = new InkRunner({ apiKey: 'test-key' }); + + const tools: InkToolDefinition[] = await (runner as any).getTools('/tmp'); + const bash = tools.find((t) => t.schema.name === 'bash')!; + + // Without agentId, guard is bypassed — command would execute + // (we test with a safe command to avoid actual execution of dangerous ones) + const result = await bash.execute({ command: 'echo hello' }); + expect(result).toContain('hello'); + }); + + it('caches tools by cwd+agentId combination', async () => { + const runner = new InkRunner({ apiKey: 'test-key' }); + + const tools1 = await (runner as any).getTools('/tmp', 'wren'); + const tools2 = await (runner as any).getTools('/tmp', 'wren'); + const tools3 = await (runner as any).getTools('/tmp', 'lumen'); + + // Same cwd+agentId returns cached instance + expect(tools1).toBe(tools2); + // Different agentId returns different instance + expect(tools1).not.toBe(tools3); + }); + }); +}); diff --git a/packages/api/src/services/sessions/ink-runner.ts b/packages/api/src/services/sessions/ink-runner.ts new file mode 100644 index 00000000..68eb0f1b --- /dev/null +++ b/packages/api/src/services/sessions/ink-runner.ts @@ -0,0 +1,269 @@ +/** + * Ink Runner + * + * Implements IRunner using the Anthropic API directly with Ink coding tools. + * Unlike CLI runners (Claude/Codex/Gemini), this calls the API in-process + * with a proper tool execution loop — tool results are fed back to continue + * the conversation until the model emits end_turn. + */ + +import Anthropic from '@anthropic-ai/sdk'; +import type { + InjectedContext, + ClaudeRunnerConfig, + RunnerResult, + ChannelResponse, + ChannelType, + IRunner, + ToolCall, +} from './types.js'; +import { formatInjectedContext } from './context-builder.js'; +import { buildIdentityPrompt } from './claude-runner.js'; +import { logger } from '../../utils/logger.js'; +import { + createInkCodingTools, + type InkToolDefinition, + type PiCodingToolsConfig, +} from '../../agent/tools/pi-coding-tools.js'; + +const MAX_TOOL_ITERATIONS = 50; +const DEFAULT_MODEL = 'claude-sonnet-4-20250514'; +const DEFAULT_MAX_TOKENS = 16384; + +export interface InkRunnerConfig { + apiKey?: string; + model?: string; + maxTokens?: number; + /** Pi coding tools config — if omitted, tools are loaded with cwd from runner config */ + piToolsConfig?: Partial; + /** Additional tools to register alongside Pi coding tools */ + extraTools?: Anthropic.Tool[]; +} + +export class InkRunner implements IRunner { + private client: Anthropic | null = null; + private runnerConfig: InkRunnerConfig; + private toolsCache = new Map(); + + constructor(config: InkRunnerConfig = {}) { + this.runnerConfig = config; + } + + async run( + message: string, + options: { + backendSessionId?: string; + injectedContext?: InjectedContext; + config: ClaudeRunnerConfig; + } + ): Promise { + const { injectedContext, config } = options; + + this.ensureClient(); + + // Build system prompt + const systemPrompt = this.buildSystemPrompt(config, injectedContext); + + // Build user message with context injection (first turn only) + let fullMessage = message; + if (injectedContext && !options.backendSessionId) { + const contextBlock = formatInjectedContext(injectedContext); + fullMessage = `${contextBlock}\n\n---\n\n${message}`; + } + + // Load Pi coding tools scoped to the working directory + const tools = await this.getTools(config.workingDirectory, config.agentId); + const toolSchemas: Anthropic.Tool[] = tools.map((t) => t.schema); + if (this.runnerConfig.extraTools) { + toolSchemas.push(...this.runnerConfig.extraTools); + } + + // Build executor map for fast lookup + const executorMap = new Map(); + for (const tool of tools) { + executorMap.set(tool.schema.name, tool.execute); + } + + // Run the agentic loop + const messages: Anthropic.MessageParam[] = [{ role: 'user', content: fullMessage }]; + const responses: ChannelResponse[] = []; + const toolCalls: ToolCall[] = []; + let totalInputTokens = 0; + let totalOutputTokens = 0; + let finalTextResponse = ''; + let backendSessionId = options.backendSessionId || `ink-${Date.now()}`; + + for (let iteration = 0; iteration < MAX_TOOL_ITERATIONS; iteration++) { + const response = await this.client!.messages.create({ + model: this.runnerConfig.model || config.model || DEFAULT_MODEL, + max_tokens: this.runnerConfig.maxTokens || DEFAULT_MAX_TOKENS, + system: systemPrompt, + messages, + tools: toolSchemas.length > 0 ? toolSchemas : undefined, + }); + + totalInputTokens += response.usage.input_tokens; + totalOutputTokens += response.usage.output_tokens; + + // Collect text and tool_use blocks + const textParts: string[] = []; + const toolUseBlocks: Anthropic.ToolUseBlock[] = []; + + for (const block of response.content) { + if (block.type === 'text') { + textParts.push(block.text); + } else if (block.type === 'tool_use') { + toolUseBlocks.push(block); + } + } + + if (textParts.length > 0) { + finalTextResponse = textParts.join(''); + } + + // Check for send_response in tool calls + for (const toolUse of toolUseBlocks) { + const input = toolUse.input as Record; + toolCalls.push({ + toolUseId: toolUse.id, + toolName: toolUse.name, + input, + }); + + if (toolUse.name === 'send_response' || toolUse.name === 'mcp__inkwell__send_response') { + const channel = (input.channel as ChannelType) || 'api'; + const conversationId = input.conversationId as string | undefined; + const content = input.content as string | undefined; + if (content) { + responses.push({ + channel, + conversationId: conversationId || '', + content, + format: input.format as ChannelResponse['format'], + }); + } + } + } + + // If no tool use, we're done + if (toolUseBlocks.length === 0 || response.stop_reason === 'end_turn') { + break; + } + + // Execute tools and build tool_result messages + // Add assistant turn with the full content (text + tool_use) + messages.push({ role: 'assistant', content: response.content }); + + const toolResults: Anthropic.ToolResultBlockParam[] = []; + for (const toolUse of toolUseBlocks) { + const executor = executorMap.get(toolUse.name); + let resultText: string; + + if (executor) { + try { + resultText = await executor(toolUse.input as Record); + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + logger.error(`Ink runner: tool ${toolUse.name} threw`, { error: errMsg }); + resultText = `Error: ${errMsg}`; + } + } else { + resultText = `Error: Tool "${toolUse.name}" not available in this runtime. Available tools: ${Array.from(executorMap.keys()).join(', ')}`; + logger.warn(`Ink runner: unknown tool "${toolUse.name}" requested`); + } + + toolResults.push({ + type: 'tool_result', + tool_use_id: toolUse.id, + content: resultText, + }); + } + + // Add tool results as user turn + messages.push({ role: 'user', content: toolResults }); + + logger.debug('Ink runner: tool iteration complete', { + iteration, + toolsExecuted: toolUseBlocks.map((t) => t.name), + }); + } + + return { + success: true, + backendSessionId, + responses, + usage: { + contextTokens: 0, + inputTokens: totalInputTokens, + outputTokens: totalOutputTokens, + }, + finalTextResponse: finalTextResponse || undefined, + toolCalls, + }; + } + + private ensureClient(): void { + if (this.client) return; + const apiKey = this.runnerConfig.apiKey || process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error('ANTHROPIC_API_KEY is required for Ink runner'); + } + this.client = new Anthropic({ apiKey }); + } + + private async getTools(cwd: string, agentId?: string): Promise { + const cacheKey = `${cwd}:${agentId ?? ''}`; + if (this.toolsCache.has(cacheKey)) { + return this.toolsCache.get(cacheKey)!; + } + + const piConfig: PiCodingToolsConfig = { + cwd, + ...this.runnerConfig.piToolsConfig, + ...(agentId && { agentId }), + }; + + const tools = await createInkCodingTools(piConfig); + this.toolsCache.set(cacheKey, tools); + return tools; + } + + private buildSystemPrompt(config: ClaudeRunnerConfig, context?: InjectedContext): string { + const parts: string[] = []; + + // Identity prompt (same as CLI runners) + if (config.agentId && context?.agent) { + parts.push( + buildIdentityPrompt( + config.agentId, + context.agent.name, + context.agent.soul, + context.user?.timezone, + context.agent.heartbeat, + { + pcpSessionId: config.pcpSessionId, + studioId: config.studioId, + } + ) + ); + } + + // System prompt from config + if (config.systemPrompt) { + parts.push(config.systemPrompt); + } + if (config.appendSystemPrompt) { + parts.push(config.appendSystemPrompt); + } + + // Coding tools context + parts.push(`## Coding Tools + +You have filesystem access scoped to: ${config.workingDirectory} + +Available tools: read, write, edit, bash, grep, find, ls +All file paths are resolved relative to the working directory. Access outside this directory is blocked.`); + + return parts.join('\n\n'); + } +} diff --git a/packages/api/src/services/sessions/session-service.ts b/packages/api/src/services/sessions/session-service.ts index a1d832b6..331d4f26 100644 --- a/packages/api/src/services/sessions/session-service.ts +++ b/packages/api/src/services/sessions/session-service.ts @@ -33,9 +33,11 @@ import { ContextBuilder } from './context-builder.js'; import { ClaudeRunner, buildIdentityPrompt } from './claude-runner.js'; import { CodexRunner } from './codex-runner.js'; import { GeminiRunner } from './gemini-runner.js'; +import { InkRunner } from './ink-runner.js'; import { ActivityStreamRepository } from '../../data/repositories/activity-stream.repository.js'; import { resolveIdentityId } from '../../auth/resolve-identity.js'; import { classifyError } from '@inklabs/shared'; +import { getRunnerFilesDir } from '../sandbox/orchestrator.js'; import { logger } from '../../utils/logger.js'; /** @@ -134,6 +136,7 @@ export class SessionService implements ISessionService { private claudeRunner: IRunner; private codexRunner: IRunner; private geminiRunner: IRunner; + private inkRunner: IRunner; private activityStream: IActivityStream; private config: SessionServiceConfig; private supabase: SupabaseClient | null; @@ -166,13 +169,15 @@ export class SessionService implements ISessionService { config: Partial = {}, codexRunner?: IRunner, supabase?: SupabaseClient, - geminiRunner?: IRunner + geminiRunner?: IRunner, + inkRunner?: IRunner ) { this.repository = repository; this.contextBuilder = contextBuilder; this.claudeRunner = claudeRunner; this.codexRunner = codexRunner || claudeRunner; this.geminiRunner = geminiRunner || claudeRunner; + this.inkRunner = inkRunner || new InkRunner(); this.activityStream = activityStream; this.config = { ...DEFAULT_CONFIG, ...config }; this.supabase = supabase || null; @@ -467,15 +472,36 @@ export class SessionService implements ISessionService { ...(permissionOverlay ? { permissionOverlay } : {}), // Propagate repo root so spawned backend's context token carries it repoRoot: resolvedWorkingDirectory.replace(/--[^/]+$/, ''), + // Route CLI execution into sandbox container when triggered by a sandboxed strategy + ...(metadata?.sandboxContainerName + ? { + container: { + containerName: metadata.sandboxContainerName, + runtimeDir: getRunnerFilesDir(metadata.sandboxContainerName), + }, + } + : {}), }; // 5. Run with selected backend + // Ink runner executes tools in-process against the host filesystem — + // it cannot route to a Docker container. Reject the combination so a + // sandboxed strategy doesn't silently bypass containment. + if (resolvedBackend === 'ink' && runnerConfig.container) { + throw new Error( + 'ink backend cannot run inside a sandbox container. ' + + 'Use a CLI backend (claude-code, codex-cli, gemini) for sandboxed strategies.' + ); + } + const runner = resolvedBackend === 'codex-cli' ? this.codexRunner : resolvedBackend === 'gemini' ? this.geminiRunner - : this.claudeRunner; + : resolvedBackend === 'ink' + ? this.inkRunner + : this.claudeRunner; // 5a. Log backend spawn to activity stream (fire-and-forget) const triggerSource = metadata?.triggerType as string | undefined; @@ -1251,7 +1277,9 @@ This session will continue with a fresh context after compaction. Your identity, ? this.codexRunner : runtimeBackend === 'gemini' ? this.geminiRunner - : this.claudeRunner; + : runtimeBackend === 'ink' + ? this.inkRunner + : this.claudeRunner; // Phase 1: Send compaction prompt — agent saves context, notifies users, ends session const result = await runner.run(compactionPrompt, { @@ -1290,10 +1318,14 @@ This session will continue with a fresh context after compaction. Your identity, /** * Normalize backend value to runtime backend IDs used by sessions. */ - private normalizeBackend(raw: string | null | undefined): 'claude-code' | 'codex-cli' | 'gemini' { + private normalizeBackend( + raw: string | null | undefined + ): 'claude-code' | 'codex-cli' | 'gemini' | 'ink' { const value = (raw || '').toLowerCase().trim(); if (value === 'codex' || value === 'codex-cli') return 'codex-cli'; if (value === 'gemini' || value === 'gemini-cli') return 'gemini'; + if (value === 'ink' || value === 'direct-api' || value === 'direct' || value === 'api') + return 'ink'; if (value === 'claude' || value === 'claude-code' || value === '') return 'claude-code'; logger.warn('Unknown backend configured, falling back to claude-code', { raw }); return 'claude-code'; @@ -1305,7 +1337,7 @@ This session will continue with a fresh context after compaction. Your identity, private async resolveAgentBackend( userId: string, agentId: string - ): Promise<'claude-code' | 'codex-cli' | 'gemini'> { + ): Promise<'claude-code' | 'codex-cli' | 'gemini' | 'ink'> { try { const identityBackend = await this.contextBuilder.getAgentBackend(userId, agentId); return this.normalizeBackend(identityBackend); @@ -1325,7 +1357,7 @@ This session will continue with a fresh context after compaction. Your identity, private resolveRuntimeBackend( sessionBackend: string | null | undefined, identityBackend: string | null | undefined - ): 'claude-code' | 'codex-cli' | 'gemini' { + ): 'claude-code' | 'codex-cli' | 'gemini' | 'ink' { if (sessionBackend) return this.normalizeBackend(sessionBackend); return this.normalizeBackend(identityBackend); } @@ -1596,6 +1628,7 @@ export function createSessionService( config, new CodexRunner(), supabase, - new GeminiRunner() + new GeminiRunner(), + new InkRunner() ); } diff --git a/packages/api/src/services/sessions/types.ts b/packages/api/src/services/sessions/types.ts index 91f5c099..11f0a7d8 100644 --- a/packages/api/src/services/sessions/types.ts +++ b/packages/api/src/services/sessions/types.ts @@ -138,6 +138,8 @@ export interface SessionRequest { repoRoot?: string; // Task group ID for strategy lifecycle correlation taskGroupId?: string; + // Docker container name for sandboxed strategy execution + sandboxContainerName?: string; }; } @@ -432,6 +434,13 @@ export interface ClaudeRunnerConfig { allow?: string[]; deny?: string[]; }; + /** Run the backend CLI inside a Docker container instead of on the host */ + container?: { + containerName: string; + dockerBinary?: string; + /** Host-side directory for runner temp files; bind-mounted as /run/ink inside the container */ + runtimeDir?: string; + }; } export interface RunnerResult { diff --git a/packages/api/src/services/strategy.service.test.ts b/packages/api/src/services/strategy.service.test.ts index 418d115d..fd856380 100644 --- a/packages/api/src/services/strategy.service.test.ts +++ b/packages/api/src/services/strategy.service.test.ts @@ -126,6 +126,9 @@ function createMockDataComposer() { activityStream: { logActivity: vi.fn().mockResolvedValue({ id: 'activity-1' }), }, + studios: { + findById: vi.fn().mockResolvedValue(null), + }, }, }; } @@ -1501,4 +1504,306 @@ describe('StrategyService', () => { expect(payload.content).toContain(pendingTask.title); }); }); + + describe('sandbox integration', () => { + it('does not spin up sandbox when config.sandbox is not set', async () => { + const group = createMockGroup({ strategy: null, status: 'active' }); + const task = createMockTask(); + const mockOrchestrator = { spinUp: vi.fn(), isRunning: vi.fn() }; + + dc.repositories.taskGroups.findById.mockResolvedValue(group); + dc.repositories.taskGroups.update.mockResolvedValue({ + ...group, + strategy: 'persistence', + status: 'active', + strategy_config: {}, + }); + + const mockClient = dc.getClient(); + mockClient.from.mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + in: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + single: vi.fn().mockResolvedValue({ data: task, error: null }), + }), + }), + }), + }), + }), + insert: vi.fn().mockResolvedValue({ data: null, error: null }), + update: vi.fn().mockReturnValue({ + contains: vi.fn().mockResolvedValue({ data: null, error: null }), + }), + }); + + const serviceWithSandbox = new StrategyService(dc as any, mockOrchestrator as any); + const result = await serviceWithSandbox.startStrategy({ + groupId: 'group-1', + userId: 'user-123', + strategy: 'persistence', + ownerAgentId: 'wren', + }); + + expect(mockOrchestrator.spinUp).not.toHaveBeenCalled(); + expect(result.sandbox).toBeUndefined(); + }); + + it('spins up sandbox when config.sandbox is true and studio exists', async () => { + const group = createMockGroup({ + strategy: null, + status: 'active', + metadata: { studioId: 'studio-abc' }, + }); + const task = createMockTask(); + const mockOrchestrator = { + spinUp: vi.fn().mockResolvedValue({ + containerName: 'ink-sandbox-wren-test-12345678', + success: true, + }), + isRunning: vi.fn(), + }; + + dc.repositories.taskGroups.findById.mockResolvedValue(group); + dc.repositories.taskGroups.update.mockResolvedValue({ + ...group, + strategy: 'persistence', + status: 'active', + strategy_config: { sandbox: true }, + }); + dc.repositories.studios.findById.mockResolvedValue({ + id: 'studio-abc', + userId: 'user-123', + agentId: 'wren', + worktreePath: '/tmp/test-studio', + repoRoot: '/tmp/test-repo', + branch: 'wren/feat/test', + slug: 'wren', + }); + + const mockClient = dc.getClient(); + mockClient.from.mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + in: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + single: vi.fn().mockResolvedValue({ data: task, error: null }), + }), + }), + }), + }), + }), + insert: vi.fn().mockResolvedValue({ data: null, error: null }), + update: vi.fn().mockReturnValue({ + contains: vi.fn().mockResolvedValue({ data: null, error: null }), + }), + }); + + const serviceWithSandbox = new StrategyService(dc as any, mockOrchestrator as any); + const result = await serviceWithSandbox.startStrategy({ + groupId: 'group-1', + userId: 'user-123', + strategy: 'persistence', + ownerAgentId: 'wren', + }); + + expect(mockOrchestrator.spinUp).toHaveBeenCalledWith( + expect.objectContaining({ + agentId: 'wren', + studioId: 'studio-abc', + worktreePath: '/tmp/test-studio', + taskGroupId: 'group-1', + taskGroupTitle: 'Test Strategy Group', + }) + ); + expect(result.sandbox).toBeDefined(); + expect(result.sandbox?.success).toBe(true); + }); + + it('aborts strategy when sandbox fails and policy is required (default)', async () => { + const group = createMockGroup({ + strategy: null, + status: 'active', + metadata: { studioId: 'studio-abc' }, + }); + const task = createMockTask(); + const mockOrchestrator = { + spinUp: vi.fn().mockResolvedValue({ + containerName: 'ink-sandbox-wren-test-12345678', + success: false, + error: 'Docker daemon not running', + }), + isRunning: vi.fn(), + }; + + dc.repositories.taskGroups.findById.mockResolvedValue(group); + dc.repositories.taskGroups.update.mockResolvedValue({ + ...group, + strategy: 'persistence', + status: 'active', + strategy_config: { sandbox: true }, + }); + dc.repositories.studios.findById.mockResolvedValue({ + id: 'studio-abc', + userId: 'user-123', + agentId: 'wren', + worktreePath: '/tmp/test-studio', + repoRoot: '/tmp/test-repo', + branch: 'wren/feat/test', + slug: 'wren', + }); + + const mockClient = dc.getClient(); + mockClient.from.mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + in: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + single: vi.fn().mockResolvedValue({ data: task, error: null }), + }), + }), + }), + }), + }), + insert: vi.fn().mockResolvedValue({ data: null, error: null }), + update: vi.fn().mockReturnValue({ + contains: vi.fn().mockResolvedValue({ data: null, error: null }), + }), + }); + + const serviceWithSandbox = new StrategyService(dc as any, mockOrchestrator as any); + const result = await serviceWithSandbox.startStrategy({ + groupId: 'group-1', + userId: 'user-123', + strategy: 'persistence', + ownerAgentId: 'wren', + }); + + // Fail-closed: strategy aborts, group is paused + expect(result.action).toBe('group_complete'); + expect(result.sandbox?.success).toBe(false); + expect(result.sandbox?.error).toContain('Docker daemon'); + expect(result.prompt).toContain('Sandbox spin-up failed'); + }); + + it('continues strategy when sandbox fails and policy is preferred', async () => { + const group = createMockGroup({ + strategy: null, + status: 'active', + metadata: { studioId: 'studio-abc' }, + }); + const task = createMockTask(); + const mockOrchestrator = { + spinUp: vi.fn().mockResolvedValue({ + containerName: 'ink-sandbox-wren-test-12345678', + success: false, + error: 'Docker daemon not running', + }), + isRunning: vi.fn(), + }; + + dc.repositories.taskGroups.findById.mockResolvedValue(group); + dc.repositories.taskGroups.update.mockResolvedValue({ + ...group, + strategy: 'persistence', + status: 'active', + strategy_config: { sandbox: true, sandboxPolicy: 'preferred' }, + }); + dc.repositories.studios.findById.mockResolvedValue({ + id: 'studio-abc', + userId: 'user-123', + agentId: 'wren', + worktreePath: '/tmp/test-studio', + repoRoot: '/tmp/test-repo', + branch: 'wren/feat/test', + slug: 'wren', + }); + + const mockClient = dc.getClient(); + mockClient.from.mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + in: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + single: vi.fn().mockResolvedValue({ data: task, error: null }), + }), + }), + }), + }), + }), + insert: vi.fn().mockResolvedValue({ data: null, error: null }), + update: vi.fn().mockReturnValue({ + contains: vi.fn().mockResolvedValue({ data: null, error: null }), + }), + }); + + const serviceWithSandbox = new StrategyService(dc as any, mockOrchestrator as any); + const result = await serviceWithSandbox.startStrategy({ + groupId: 'group-1', + userId: 'user-123', + strategy: 'persistence', + ownerAgentId: 'wren', + }); + + // Preferred: strategy continues on host despite sandbox failure + expect(result.action).toBe('next_task'); + expect(result.sandbox?.success).toBe(false); + expect(result.sandbox?.error).toContain('Docker daemon'); + }); + + it('aborts when sandbox requested but no studioId in metadata', async () => { + const group = createMockGroup({ + strategy: null, + status: 'active', + metadata: {}, + }); + const task = createMockTask(); + const mockOrchestrator = { spinUp: vi.fn(), isRunning: vi.fn() }; + + dc.repositories.taskGroups.findById.mockResolvedValue(group); + dc.repositories.taskGroups.update.mockResolvedValue({ + ...group, + strategy: 'persistence', + status: 'active', + strategy_config: { sandbox: true }, + }); + + const mockClient = dc.getClient(); + mockClient.from.mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + in: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + single: vi.fn().mockResolvedValue({ data: task, error: null }), + }), + }), + }), + }), + }), + insert: vi.fn().mockResolvedValue({ data: null, error: null }), + update: vi.fn().mockReturnValue({ + contains: vi.fn().mockResolvedValue({ data: null, error: null }), + }), + }); + + const serviceWithSandbox = new StrategyService(dc as any, mockOrchestrator as any); + const result = await serviceWithSandbox.startStrategy({ + groupId: 'group-1', + userId: 'user-123', + strategy: 'persistence', + ownerAgentId: 'wren', + }); + + // Fail-closed: no studioId means sandbox can't start + expect(mockOrchestrator.spinUp).not.toHaveBeenCalled(); + expect(result.action).toBe('group_complete'); + expect(result.sandbox?.success).toBe(false); + expect(result.prompt).toContain('Sandbox spin-up failed'); + }); + }); }); diff --git a/packages/api/src/services/strategy.service.ts b/packages/api/src/services/strategy.service.ts index 66ef0594..2b560412 100644 --- a/packages/api/src/services/strategy.service.ts +++ b/packages/api/src/services/strategy.service.ts @@ -23,6 +23,7 @@ import type { import type { ProjectTask, TaskAssignment } from '../data/repositories/project-tasks.repository'; import { handleSendToInbox } from '../mcp/tools/inbox-handlers'; import { logger } from '../utils/logger'; +import type { SandboxOrchestrator, SandboxSpinUpResult } from './sandbox/orchestrator'; // ============================================================================ // Types @@ -51,6 +52,8 @@ export interface StrategyAdvanceResult { notified?: boolean; /** Completion stats when group is done */ stats?: { total: number; completed: number }; + /** Sandbox container info (when sandbox mode is active) */ + sandbox?: SandboxSpinUpResult; } export interface StrategyStatus { @@ -158,7 +161,15 @@ const STRATEGY_PROMPTS: Record | null; @@ -221,12 +232,48 @@ export class StrategyService { // Create a watchdog reminder so the heartbeat checks progress periodically await this.createWatchdogReminder(updated, input.userId); - // Kick off the owner agent in the assigned studio. Without this trigger, - // start_strategy only returns a prompt to the caller's session — which - // is useless when the caller is delegating to a different studio/agent. - // The trigger spawns (or resumes) a session in the target studio so work - // actually begins, matching how heartbeats/reminders already deliver. - const triggered = await this.triggerOwnerAgent(updated, nextTask, 'strategy_kickoff'); + // Spin up sandbox BEFORE triggering the agent — if sandboxPolicy is + // 'required' (default), a failed sandbox aborts the strategy instead + // of silently degrading to host execution. + const config = updated.strategy_config as StrategyConfig; + const sandboxResult = await this.maybeSpinUpSandbox(updated); + const sandboxPolicy = config.sandboxPolicy || 'required'; + + if (config.sandbox && sandboxResult && !sandboxResult.success && sandboxPolicy === 'required') { + // Fail-closed: revert the strategy to paused and report the failure + await this.dataComposer.repositories.taskGroups.update(input.groupId, { + status: 'paused', + strategy_paused_at: new Date().toISOString(), + }); + await this.logStrategyEvent( + updated, + 'sandbox_failed', + `Strategy aborted: sandbox required but spin-up failed — ${sandboxResult.error}`, + { + containerName: sandboxResult.containerName, + error: sandboxResult.error, + policy: 'required', + } + ); + return { + action: 'group_complete', + stats: { total: 0, completed: 0 }, + prompt: `Sandbox spin-up failed (policy: required). Error: ${sandboxResult.error}. Strategy has been paused — fix the sandbox configuration and retry.`, + sandbox: sandboxResult, + }; + } + + // Trigger the owner agent in the assigned studio. The trigger spawns + // (or resumes) a session in the target studio so work actually begins. + // Pass the sandbox container name so the triggered session routes + // CLI execution into the container. + const sandboxContainer = sandboxResult?.success ? sandboxResult.containerName : undefined; + const triggered = await this.triggerOwnerAgent( + updated, + nextTask, + 'strategy_kickoff', + sandboxContainer + ); // Log strategy start await this.logStrategyEvent( @@ -237,6 +284,9 @@ export class StrategyService { firstTaskId: nextTask.id, firstTaskTitle: nextTask.title, ownerTriggered: triggered, + sandbox: sandboxResult + ? { containerName: sandboxResult.containerName, success: sandboxResult.success } + : undefined, } ); @@ -247,6 +297,7 @@ export class StrategyService { nextTask, prompt, notified: triggered, + sandbox: sandboxResult || undefined, }; } @@ -773,7 +824,8 @@ export class StrategyService { private async triggerOwnerAgent( group: TaskGroup, task: ProjectTask, - reason: 'strategy_kickoff' | 'watchdog' | 'manual_resume' + reason: 'strategy_kickoff' | 'watchdog' | 'manual_resume', + sandboxContainerName?: string ): Promise { if (!group.owner_agent_id) { logger.warn( @@ -819,6 +871,7 @@ export class StrategyService { groupId: group.id, taskId: task.id, strategy: group.strategy, + ...(sandboxContainerName ? { sandboxContainerName } : {}), }, }, this.dataComposer @@ -864,6 +917,75 @@ export class StrategyService { } } + /** + * Spin up a sandbox Docker container for the strategy's owner agent. + * Resolves the studio from DB metadata, builds a SandboxSpinUpRequest, + * and delegates to the orchestrator. Returns null if sandbox mode is + * not enabled or no orchestrator is configured. + */ + private async maybeSpinUpSandbox(group: TaskGroup): Promise { + const config = group.strategy_config as StrategyConfig; + if (!config.sandbox) return null; + + if (!this.sandboxOrchestrator) { + const msg = `Sandbox enabled but no SandboxOrchestrator configured`; + logger.warn(`Strategy group ${group.id}: ${msg}`); + return { containerName: '', success: false, error: msg }; + } + + const metadata = (group.metadata || {}) as Record; + const studioId = typeof metadata.studioId === 'string' ? metadata.studioId : undefined; + if (!studioId) { + const msg = `Sandbox requested but no studioId in metadata`; + logger.warn(`Strategy group ${group.id}: ${msg}`); + return { containerName: '', success: false, error: msg }; + } + + const studio = await this.dataComposer.repositories.studios.findById(studioId); + if (!studio) { + const msg = `Studio ${studioId} not found`; + logger.warn(`Strategy group ${group.id}: ${msg}`); + return { containerName: '', success: false, error: msg }; + } + + const result = await this.sandboxOrchestrator.spinUp({ + userId: group.user_id, + agentId: group.owner_agent_id || studio.agentId || 'unknown', + studioId: studio.id, + studioSlug: studio.slug || undefined, + worktreePath: studio.worktreePath, + repoRoot: studio.repoRoot, + branch: studio.branch, + taskGroupId: group.id, + taskGroupTitle: group.title, + taskGroupContext: group.context_summary || undefined, + taskGroupThreadKey: group.thread_key || `strategy:${group.id}`, + backendAuth: (config.sandboxBackendAuth as any) || ['claude'], + }); + + if (result.success) { + await this.logStrategyEvent( + group, + 'sandbox_started', + `Sandbox container started: ${result.containerName}`, + { + containerName: result.containerName, + studioId: studio.id, + alreadyRunning: result.alreadyRunning, + } + ); + } else { + await this.logStrategyEvent( + group, + 'sandbox_failed', + `Sandbox spin-up failed: ${result.error}`, + { containerName: result.containerName, error: result.error } + ); + } + + return result; + } + /** * Public entry point for watchdog-driven triggers. Called from the heartbeat * reminder-delivery path when a scheduled_reminder has @@ -931,7 +1053,35 @@ export class StrategyService { return false; } - return this.triggerOwnerAgent(group, currentTask, 'watchdog'); + // If the strategy uses a sandbox, spin up (or reuse) the container before + // triggering. The orchestrator short-circuits if the container is already + // running, so this is safe to call on every watchdog tick. + const config = group.strategy_config as StrategyConfig; + let sandboxContainerName: string | undefined; + if (config.sandbox) { + const sandboxResult = await this.maybeSpinUpSandbox(group); + const sandboxPolicy = config.sandboxPolicy || 'required'; + + if (sandboxResult && !sandboxResult.success && sandboxPolicy === 'required') { + await this.logStrategyEvent( + group, + 'sandbox_failed', + `Watchdog aborted: sandbox required but spin-up failed — ${sandboxResult.error}`, + { error: sandboxResult.error, policy: 'required', trigger: 'watchdog' } + ); + await this.dataComposer.repositories.taskGroups.update(groupId, { + status: 'paused', + strategy_paused_at: new Date().toISOString(), + }); + return false; + } + + if (sandboxResult?.success) { + sandboxContainerName = sandboxResult.containerName; + } + } + + return this.triggerOwnerAgent(group, currentTask, 'watchdog', sandboxContainerName); } /** diff --git a/packages/shared/src/runner/index.ts b/packages/shared/src/runner/index.ts index c4d816d3..b0ecc6ed 100644 --- a/packages/shared/src/runner/index.ts +++ b/packages/shared/src/runner/index.ts @@ -1,7 +1,10 @@ export { buildCleanEnv, + resolveSpawnTarget, spawnBackend, LineBuffer, + CONTAINER_RUNNER_FILES, + type ContainerTarget, type SpawnBackendOptions, type SpawnBackendResult, } from './spawn-backend.js'; diff --git a/packages/shared/src/runner/mcp-config.test.ts b/packages/shared/src/runner/mcp-config.test.ts index 01b8b278..a90f3ab3 100644 --- a/packages/shared/src/runner/mcp-config.test.ts +++ b/packages/shared/src/runner/mcp-config.test.ts @@ -104,6 +104,48 @@ describe('injectSessionHeaders', () => { unlinkSync(configPath); }); + it('writes modified config to outputDir when set', () => { + const configPath = writeTempConfig({ + mcpServers: { inkwell: { type: 'http', url: 'http://localhost:3001/mcp' } }, + }); + + const outputDir = join(testDir, `output-${Date.now()}`); + mkdirSync(outputDir, { recursive: true }); + + const result = injectSessionHeaders({ + mcpConfigPath: configPath, + pcpSessionId: 'test-session-id', + studioId: 'test-studio-id', + outputDir, + }); + + expect(result.modified).toBe(true); + // The returned mcpConfigPath should be inside the outputDir, not the default /tmp/sb-mcp + expect(result.mcpConfigPath.startsWith(outputDir)).toBe(true); + expect(existsSync(result.mcpConfigPath)).toBe(true); + + // Verify the config was written correctly + const config = JSON.parse(readFileSync(result.mcpConfigPath, 'utf-8')); + expect(config.mcpServers.inkwell.headers['x-ink-session-id']).toBe('${INK_SESSION_ID}'); + result.cleanup(); + }); + + it('uses default sb-mcp dir when outputDir is not set', () => { + const configPath = writeTempConfig({ + mcpServers: { inkwell: { type: 'http', url: 'http://localhost:3001/mcp' } }, + }); + + const result = injectSessionHeaders({ + mcpConfigPath: configPath, + pcpSessionId: 'test-session-id', + }); + + expect(result.modified).toBe(true); + // Without outputDir, should use the default tmpdir/sb-mcp path + expect(result.mcpConfigPath).toContain('sb-mcp'); + result.cleanup(); + }); + it('returns original path when headers already present', () => { const configPath = writeTempConfig({ mcpServers: { diff --git a/packages/shared/src/runner/mcp-config.ts b/packages/shared/src/runner/mcp-config.ts index 072042ee..3b4d0be1 100644 --- a/packages/shared/src/runner/mcp-config.ts +++ b/packages/shared/src/runner/mcp-config.ts @@ -38,6 +38,8 @@ export interface InjectSessionHeadersOptions { studioId?: string; /** Optional access token — injected as Authorization header for triggered sessions */ accessToken?: string; + /** Directory to write the modified config to (default: system tmpdir/sb-mcp). Use this for container execution where temp files must be in a mounted directory. */ + outputDir?: string; } export interface InjectSessionHeadersResult { @@ -137,8 +139,8 @@ export function injectSessionHeaders( return { mcpConfigPath, cleanup: () => {}, modified: false }; } - // Write modified config to temp file - const tmpDir = join(tmpdir(), 'sb-mcp'); + // Write modified config to temp file (or outputDir for container execution) + const tmpDir = options.outputDir || join(tmpdir(), 'sb-mcp'); mkdirSync(tmpDir, { recursive: true }); const tmpPath = join(tmpDir, `mcp-server-${process.pid}-${Date.now()}.json`); writeFileSync(tmpPath, JSON.stringify(config, null, 2)); diff --git a/packages/shared/src/runner/spawn-backend.test.ts b/packages/shared/src/runner/spawn-backend.test.ts index cb5c934c..1392e9ec 100644 --- a/packages/shared/src/runner/spawn-backend.test.ts +++ b/packages/shared/src/runner/spawn-backend.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildCleanEnv, spawnBackend, LineBuffer } from './spawn-backend.js'; +import { buildCleanEnv, spawnBackend, resolveSpawnTarget, LineBuffer } from './spawn-backend.js'; describe('buildCleanEnv', () => { it('strips CLAUDECODE from process.env', () => { @@ -107,6 +107,146 @@ describe('spawnBackend', () => { }); }); +describe('resolveSpawnTarget', () => { + it('passes through binary and args for host execution', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: ['--print', '--verbose'], + cwd: '/tmp/studio', + env: { AGENT_ID: 'wren' }, + }); + expect(target.binary).toBe('claude'); + expect(target.args).toEqual(['--print', '--verbose']); + expect(target.cwd).toBe('/tmp/studio'); + expect(target.env.AGENT_ID).toBe('wren'); + }); + + it('wraps binary in docker exec for container execution', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: ['--print', '--verbose'], + container: { containerName: 'ink-sandbox-wren-abc123' }, + }); + expect(target.binary).toBe('docker'); + expect(target.args[0]).toBe('exec'); + expect(target.args).toContain('ink-sandbox-wren-abc123'); + expect(target.args).toContain('claude'); + expect(target.args).toContain('--print'); + expect(target.args).toContain('--verbose'); + }); + + it('passes cwd as --workdir to docker exec', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + cwd: '/studio', + container: { containerName: 'test-container' }, + }); + const workdirIdx = target.args.indexOf('--workdir'); + expect(workdirIdx).toBeGreaterThan(-1); + expect(target.args[workdirIdx + 1]).toBe('/studio'); + // Host cwd should be undefined (cwd is inside the container) + expect(target.cwd).toBeUndefined(); + }); + + it('passes env vars as -e flags to docker exec', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + env: { AGENT_ID: 'wren', INK_SANDBOX: 'docker' }, + container: { containerName: 'test-container' }, + }); + expect(target.args).toContain('-e'); + expect(target.args).toContain('AGENT_ID=wren'); + expect(target.args).toContain('INK_SANDBOX=docker'); + }); + + it('adds -i flag when pipeStdin is true for container', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + pipeStdin: true, + container: { containerName: 'test-container' }, + }); + expect(target.args).toContain('-i'); + }); + + it('does not add -i flag when pipeStdin is false for container', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + pipeStdin: false, + container: { containerName: 'test-container' }, + }); + const execIdx = target.args.indexOf('exec'); + const containerIdx = target.args.indexOf('test-container'); + // No -i between exec and container name + const sliceBetween = target.args.slice(execIdx + 1, containerIdx); + expect(sliceBetween).not.toContain('-i'); + }); + + it('uses custom docker binary when specified', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + container: { containerName: 'test', dockerBinary: 'podman' }, + }); + expect(target.binary).toBe('podman'); + }); + + it('converts host binary path to basename for container execution', () => { + const target = resolveSpawnTarget({ + binary: '/Users/conor/.local/bin/claude', + args: ['--print'], + container: { containerName: 'test-container' }, + }); + expect(target.binary).toBe('docker'); + const containerIdx = target.args.indexOf('test-container'); + expect(target.args[containerIdx + 1]).toBe('claude'); + }); + + it('maps host cwd to container workDir (default /studio)', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + cwd: '/Users/conor/ws/pcp/personal-context-protocol--wren', + container: { containerName: 'test-container' }, + }); + const workdirIdx = target.args.indexOf('--workdir'); + expect(target.args[workdirIdx + 1]).toBe('/studio'); + expect(target.cwd).toBeUndefined(); + }); + + it('respects custom container workDir', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: [], + cwd: '/Users/conor/ws/project', + container: { containerName: 'test-container', workDir: '/workspace' }, + }); + const workdirIdx = target.args.indexOf('--workdir'); + expect(target.args[workdirIdx + 1]).toBe('/workspace'); + }); + + it('preserves argument order: docker exec [flags] container binary args', () => { + const target = resolveSpawnTarget({ + binary: 'claude', + args: ['--print', '-m', 'sonnet'], + cwd: '/studio', + env: { KEY: 'val' }, + pipeStdin: true, + container: { containerName: 'my-sandbox' }, + }); + // Structure: docker exec -i --workdir /studio -e KEY=val my-sandbox claude --print -m sonnet + const containerIdx = target.args.indexOf('my-sandbox'); + expect(containerIdx).toBeGreaterThan(0); + expect(target.args[containerIdx + 1]).toBe('claude'); + expect(target.args[containerIdx + 2]).toBe('--print'); + expect(target.args[containerIdx + 3]).toBe('-m'); + expect(target.args[containerIdx + 4]).toBe('sonnet'); + }); +}); + describe('LineBuffer', () => { it('splits complete lines', () => { const buf = new LineBuffer(); diff --git a/packages/shared/src/runner/spawn-backend.ts b/packages/shared/src/runner/spawn-backend.ts index dd2d4ee8..d494d801 100644 --- a/packages/shared/src/runner/spawn-backend.ts +++ b/packages/shared/src/runner/spawn-backend.ts @@ -13,9 +13,22 @@ */ import { spawn, type ChildProcess } from 'child_process'; +import path from 'path'; + +/** Container-side path for runner temp files, bind-mounted from the host staging dir */ +export const CONTAINER_RUNNER_FILES = '/run/ink'; // ─── Types ────────────────────────────────────────────────────── +export interface ContainerTarget { + /** Docker container name or ID to exec into */ + containerName: string; + /** Docker binary (default: 'docker') */ + dockerBinary?: string; + /** Working directory inside the container (default: '/studio') */ + workDir?: string; +} + export interface SpawnBackendOptions { /** Absolute or PATH-relative binary name */ binary: string; @@ -35,6 +48,8 @@ export interface SpawnBackendOptions { onStdout?: (chunk: string) => void; /** Called on each stderr chunk */ onStderr?: (chunk: string) => void; + /** Run the binary inside a Docker container instead of on the host */ + container?: ContainerTarget; } export interface SpawnBackendResult { @@ -67,23 +82,83 @@ export function buildCleanEnv( return { ...cleanEnv, ...extraEnv }; } +/** + * Build the actual binary + args for spawning, handling container routing. + * + * When `container` is set, wraps the command in `docker exec` so the + * binary runs inside the container. Env vars are passed via `-e` flags + * and cwd via `--workdir`. The caller sees the same interface regardless + * of execution target. + */ +export function resolveSpawnTarget(options: SpawnBackendOptions): { + binary: string; + args: string[]; + cwd?: string; + env: Record; +} { + if (!options.container) { + return { + binary: options.binary, + args: options.args, + cwd: options.cwd, + env: buildCleanEnv(options.env), + }; + } + + const docker = options.container.dockerBinary || 'docker'; + const execArgs = ['exec']; + + if (options.pipeStdin) { + execArgs.push('-i'); + } + + const containerWorkDir = options.container.workDir || '/studio'; + if (options.cwd) { + execArgs.push('--workdir', containerWorkDir); + } + + if (options.env) { + for (const [key, value] of Object.entries(options.env)) { + execArgs.push('-e', `${key}=${value}`); + } + } + + // Use basename — the container has CLI tools on its PATH, not at host-resolved absolute paths + const containerBinary = path.basename(options.binary); + execArgs.push(options.container.containerName, containerBinary, ...options.args); + + return { + binary: docker, + args: execArgs, + // cwd is inside the container (passed via --workdir), not on the host + cwd: undefined, + // Host env is clean but doesn't need the extra vars (they're inside the container) + env: buildCleanEnv(), + }; +} + /** * Spawn a backend process with timeout management, output accumulation, * and CLAUDECODE env stripping. * * This is the canonical spawn function for all backend process invocations. * Both API server runners and CLI backend-runner should use this. + * + * When `options.container` is set, the binary runs inside the specified + * Docker container via `docker exec`. The interface is identical — callers + * don't need to know whether they're targeting host or container. */ export function spawnBackend(options: SpawnBackendOptions): { child: ChildProcess; result: Promise; } { const started = Date.now(); + const target = resolveSpawnTarget(options); - const child = spawn(options.binary, options.args, { + const child = spawn(target.binary, target.args, { stdio: [options.pipeStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'], - cwd: options.cwd, - env: buildCleanEnv(options.env), + cwd: target.cwd, + env: target.env, }); let stdout = ''; diff --git a/yarn.lock b/yarn.lock index 890fb95a..8eaf0c47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,6 +38,615 @@ __metadata: languageName: node linkType: hard +"@anthropic-ai/sdk@npm:^0.91.1": + version: 0.91.1 + resolution: "@anthropic-ai/sdk@npm:0.91.1" + dependencies: + json-schema-to-ts: "npm:^3.1.1" + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + bin: + anthropic-ai-sdk: bin/cli + checksum: 10/3ac357233f237e038af32a0a2d820e3531e35dc2e3b39864639fc42570bad6d1036630b49b6a75e9961ef31a9bcd338a3f262fbde60e4d13014683c40265756a + languageName: node + linkType: hard + +"@aws-crypto/crc32@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10/1b0a56ad4cb44c9512d8b1668dcf9306ab541d3a73829f435ca97abaec8d56f3db953db03ad0d0698754fea16fcd803d11fa42e0889bc7b803c6a030b04c63de + languageName: node + linkType: hard + +"@aws-crypto/sha256-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-browser@npm:5.2.0" + dependencies: + "@aws-crypto/sha256-js": "npm:^5.2.0" + "@aws-crypto/supports-web-crypto": "npm:^5.2.0" + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + "@aws-sdk/util-locate-window": "npm:^3.0.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10/2b1b701ca6caa876333b4eb2b96e5187d71ebb51ebf8e2d632690dbcdedeff038202d23adcc97e023437ed42bb1963b7b463e343687edf0635fd4b98b2edad1a + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-js@npm:5.2.0" + dependencies: + "@aws-crypto/util": "npm:^5.2.0" + "@aws-sdk/types": "npm:^3.222.0" + tslib: "npm:^2.6.2" + checksum: 10/f46aace7b873c615be4e787ab0efd0148ef7de48f9f12c7d043e05c52e52b75bb0bf6dbcb9b2852d940d7724fab7b6d5ff1469160a3dd024efe7a68b5f70df8c + languageName: node + linkType: hard + +"@aws-crypto/supports-web-crypto@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/6ed0c7e17f4f6663d057630805c45edb35d5693380c24ab52d4c453ece303c6c8a6ade9ee93c97dda77d9f6cae376ffbb44467057161c513dffa3422250edaf5 + languageName: node + linkType: hard + +"@aws-crypto/util@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/util@npm:5.2.0" + dependencies: + "@aws-sdk/types": "npm:^3.222.0" + "@smithy/util-utf8": "npm:^2.0.0" + tslib: "npm:^2.6.2" + checksum: 10/f80a174c404e1ad4364741c942f440e75f834c08278fa754349fe23a6edc679d480ea9ced5820774aee58091ed270067022d8059ecf1a7ef452d58134ac7e9e1 + languageName: node + linkType: hard + +"@aws-sdk/client-bedrock-runtime@npm:^3.1030.0": + version: 3.1042.0 + resolution: "@aws-sdk/client-bedrock-runtime@npm:3.1042.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/credential-provider-node": "npm:^3.972.39" + "@aws-sdk/eventstream-handler-node": "npm:^3.972.14" + "@aws-sdk/middleware-eventstream": "npm:^3.972.10" + "@aws-sdk/middleware-host-header": "npm:^3.972.10" + "@aws-sdk/middleware-logger": "npm:^3.972.10" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.11" + "@aws-sdk/middleware-user-agent": "npm:^3.972.38" + "@aws-sdk/middleware-websocket": "npm:^3.972.16" + "@aws-sdk/region-config-resolver": "npm:^3.972.13" + "@aws-sdk/token-providers": "npm:3.1042.0" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-endpoints": "npm:^3.996.8" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.10" + "@aws-sdk/util-user-agent-node": "npm:^3.973.24" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/core": "npm:^3.23.17" + "@smithy/eventstream-serde-browser": "npm:^4.2.14" + "@smithy/eventstream-serde-config-resolver": "npm:^4.3.14" + "@smithy/eventstream-serde-node": "npm:^4.2.14" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/hash-node": "npm:^4.2.14" + "@smithy/invalid-dependency": "npm:^4.2.14" + "@smithy/middleware-content-length": "npm:^4.2.14" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/middleware-retry": "npm:^4.5.7" + "@smithy/middleware-serde": "npm:^4.2.20" + "@smithy/middleware-stack": "npm:^4.2.14" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.49" + "@smithy/util-defaults-mode-node": "npm:^4.2.54" + "@smithy/util-endpoints": "npm:^3.4.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.6" + "@smithy/util-stream": "npm:^4.5.25" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/714e225f988fa437a5622371bd9f0587160e3d4d632fb4921a2fd58bc71e33622a2f088c25a773565ae10f527393efff4b33391afe9d74e2a35add6a1c499667 + languageName: node + linkType: hard + +"@aws-sdk/core@npm:^3.974.8": + version: 3.974.8 + resolution: "@aws-sdk/core@npm:3.974.8" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/xml-builder": "npm:^3.972.22" + "@smithy/core": "npm:^3.23.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.6" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/7371738ba92353ebb9cdc753bf53240d7fec2486b8a87c191d11ff15386a19a4335ff7c28444f87825ad2643b1c41a491dfcb8dd9cd6e556eb352da5b7e05a9b + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:^3.972.34": + version: 3.972.34 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.34" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/764a8accd62fa11dffc02354c98c7b3f4ce91b31b1c0baaecde5e10101fb60fb0ab25125e9ef1dd52cf681928e57f7da6afd4e9c63c8ec13632d38406e8ae2cb + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-http@npm:^3.972.36": + version: 3.972.36 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.36" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-stream": "npm:^4.5.25" + tslib: "npm:^2.6.2" + checksum: 10/832699bb7075baca36530abe4fbcbde84e340ce3e24cfb4615be2bead459824944181644f625da576e92f25dc341cfabd713d8f3455cab55bc47be16eee2c1fe + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:^3.972.38": + version: 3.972.38 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.38" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/credential-provider-env": "npm:^3.972.34" + "@aws-sdk/credential-provider-http": "npm:^3.972.36" + "@aws-sdk/credential-provider-login": "npm:^3.972.38" + "@aws-sdk/credential-provider-process": "npm:^3.972.34" + "@aws-sdk/credential-provider-sso": "npm:^3.972.38" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.38" + "@aws-sdk/nested-clients": "npm:^3.997.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/credential-provider-imds": "npm:^4.2.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/49c7faa65d961450bc62e883553b278d3b525fd9a31c1e60e9ed927034c78603d1f2456b1bccce10ac478672eea8a0fb31b76ab0adf7b68dd0cb362e841b48aa + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-login@npm:^3.972.38": + version: 3.972.38 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.38" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/nested-clients": "npm:^3.997.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/b03c35546b1ebffb2ba01d54131fb8bc5438a59f8a484ad3af3ebb42761cb9912d2771764dd60ab1a1a65fc64d61446fd3a519766f21354d2b0fda3d874a492e + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:^3.972.39": + version: 3.972.39 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.39" + dependencies: + "@aws-sdk/credential-provider-env": "npm:^3.972.34" + "@aws-sdk/credential-provider-http": "npm:^3.972.36" + "@aws-sdk/credential-provider-ini": "npm:^3.972.38" + "@aws-sdk/credential-provider-process": "npm:^3.972.34" + "@aws-sdk/credential-provider-sso": "npm:^3.972.38" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.38" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/credential-provider-imds": "npm:^4.2.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/b577b77075d4957d46f5bbc190af6f70dfc0a4acff8c8db73425b94aa2e84133393cf305febcc7cacdc7c7896e67697d2f53cbc2a454bf7ba011a830e7dd067f + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:^3.972.34": + version: 3.972.34 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.34" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/184a83004066fe745e0aa36ec48480c9eadd13300f854100b6e71d7b14e3cc7254b16e28d5d7bcbc9c1a593ef3275d10b0accfba0b4c5f333f3e7be8850a5ccb + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:^3.972.38": + version: 3.972.38 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.38" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/nested-clients": "npm:^3.997.6" + "@aws-sdk/token-providers": "npm:3.1041.0" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/2f1bd86399d46487901e6eee881afaf607e0aac459c6db1fceb342c225f96f726d8d73349f9cf9d5b2c5a1be6a9a373902aaae6a3e448260d0ebc80d1fe8f9a1 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:^3.972.38": + version: 3.972.38 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.38" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/nested-clients": "npm:^3.997.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/9975bc20ab171365b73e2e4c9567bb07ffbace0bf5e0e2c6c052e5b99f06d5635c1ce6e0e5260bd62150eca933878ac5cbccb58b3f3058714ce93e3f93aa9649 + languageName: node + linkType: hard + +"@aws-sdk/eventstream-handler-node@npm:^3.972.14": + version: 3.972.14 + resolution: "@aws-sdk/eventstream-handler-node@npm:3.972.14" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/eventstream-codec": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/bf5038f4eccb933b6a9ae5230288278af337bc8988261cc68d261d0fd12994d3d81280b3d2a679c40eb1254457dd2d0eea771503ffbb6f0ffd5f4b89687c292b + languageName: node + linkType: hard + +"@aws-sdk/middleware-eventstream@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-eventstream@npm:3.972.10" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/dc8f8f6331ac1e5599035cab9198441768f7edbd46b03189390775ee0e0eb09a66b26f4e7b9959c852b8609f3238f68fb68e2a749c5bcb7303ad5fe7c74c98e2 + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-host-header@npm:3.972.10" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/4098fa5f6d519adcc62e39bcc59ac46cf1478b7608b5137ca9e1e4d64acd8123ecc699627edccb06d755db84aea5077e5e4b1d34501efb6043bd0fe51e3c4695 + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-logger@npm:3.972.10" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/a5ccf69d05be4288448d6285640b693c7c2182c63a8a8c4474c83b5c020011ae538ed6bf34928eea2ee9edae3a73b49f45b5db828d65f346e660609c8add739b + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:^3.972.11": + version: 3.972.11 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.972.11" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@aws/lambda-invoke-store": "npm:^0.2.2" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/3ce52895d9414d6d791114cea5a38bcc83659ec0bfa79234a032d8337df6248715bdcfcc5a0c601e71ce21f31967bc05eb84c22af455309bf9d995eb8a6ad309 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-s3@npm:^3.972.37": + version: 3.972.37 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.972.37" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-arn-parser": "npm:^3.972.3" + "@smithy/core": "npm:^3.23.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-config-provider": "npm:^4.2.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-stream": "npm:^4.5.25" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/8b5f71c1a62fb02f0acb70eb55592db2ffa8c5c0822fc03babe3d4321672cdcf13ad165d6b921f8312683014c3f1efcede0254790c0ec8bca3a3956fea4f67d8 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:^3.972.38": + version: 3.972.38 + resolution: "@aws-sdk/middleware-user-agent@npm:3.972.38" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-endpoints": "npm:^3.996.8" + "@smithy/core": "npm:^3.23.17" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-retry": "npm:^4.3.6" + tslib: "npm:^2.6.2" + checksum: 10/f1bfd65d5a49ac80d09c3309bf32e0c6c3362566232aaf5ab830044622efa89abeef0231ce6a03ab5f61c15a6ed22e62c6bf8a290dd4480e2330033eec6d9fef + languageName: node + linkType: hard + +"@aws-sdk/middleware-websocket@npm:^3.972.16": + version: 3.972.16 + resolution: "@aws-sdk/middleware-websocket@npm:3.972.16" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-format-url": "npm:^3.972.10" + "@smithy/eventstream-codec": "npm:^4.2.14" + "@smithy/eventstream-serde-browser": "npm:^4.2.14" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-hex-encoding": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/98803772fe93edab7cb26f6816e1f435148e74ac1a6f03eaa5f5ddbc7231b3ff7f6776f22a85948ce29093b809aa9d8f4483e9e2e93a4169251116f1f53000ab + languageName: node + linkType: hard + +"@aws-sdk/nested-clients@npm:^3.997.6": + version: 3.997.6 + resolution: "@aws-sdk/nested-clients@npm:3.997.6" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/middleware-host-header": "npm:^3.972.10" + "@aws-sdk/middleware-logger": "npm:^3.972.10" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.11" + "@aws-sdk/middleware-user-agent": "npm:^3.972.38" + "@aws-sdk/region-config-resolver": "npm:^3.972.13" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.25" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-endpoints": "npm:^3.996.8" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.10" + "@aws-sdk/util-user-agent-node": "npm:^3.973.24" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/core": "npm:^3.23.17" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/hash-node": "npm:^4.2.14" + "@smithy/invalid-dependency": "npm:^4.2.14" + "@smithy/middleware-content-length": "npm:^4.2.14" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/middleware-retry": "npm:^4.5.7" + "@smithy/middleware-serde": "npm:^4.2.20" + "@smithy/middleware-stack": "npm:^4.2.14" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-body-length-node": "npm:^4.2.3" + "@smithy/util-defaults-mode-browser": "npm:^4.3.49" + "@smithy/util-defaults-mode-node": "npm:^4.2.54" + "@smithy/util-endpoints": "npm:^3.4.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.6" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/8467df064e288a3cd2f39d33ef3a63193bb40ac5ef54ef2265dc1b1d8801dbc29c442b75c85a29e6d02e2e43e8f84924dbb7933db8642bb37788c97634c2e814 + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:^3.972.13": + version: 3.972.13 + resolution: "@aws-sdk/region-config-resolver@npm:3.972.13" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/f80c26ecd5d93c1f2c8e05c15ac8e6cff2411f6739f19995d8262331ebf75247b20ee932c4e48b13a19976b56706559b8ba814e2a5f245949987ac3cd1b97ab0 + languageName: node + linkType: hard + +"@aws-sdk/signature-v4-multi-region@npm:^3.996.25": + version: 3.996.25 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.25" + dependencies: + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.37" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/1139b78872c18b65c3f4a3e6d38ba78bd4196129fb969bf0246034b41ac3387b9516f45734ed62e38eb1d564dcd1861ae633a2a50945ff279d67e4aeac503fa6 + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.1041.0": + version: 3.1041.0 + resolution: "@aws-sdk/token-providers@npm:3.1041.0" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/nested-clients": "npm:^3.997.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/bf101933d1daf6056fe92b40f88a65306fc3be17df834acbec4181f5eb781b8968f2aaf3e6f9be31ebf1302da001ae2ba44954b9c8071318ef8bed5d12d36cc9 + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.1042.0": + version: 3.1042.0 + resolution: "@aws-sdk/token-providers@npm:3.1042.0" + dependencies: + "@aws-sdk/core": "npm:^3.974.8" + "@aws-sdk/nested-clients": "npm:^3.997.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/1a313a54a66ec87a759065299e8f4a3fb1fc9f059c0c999f537abc0feba5e2070025552034c9f128788ee815eaa821d092dd178d9794c88f1fc8d08131deac6c + languageName: node + linkType: hard + +"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.973.8": + version: 3.973.8 + resolution: "@aws-sdk/types@npm:3.973.8" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/76f613d0dfcb9fd3f66aa39aaba81d9d0a0e20d09ad1c8dff9c0c990c69f5d5ee758dfbbb4cf7445ed0d30f96454369282ede7a4a9e64b7e7be95c7b5e34ebc3 + languageName: node + linkType: hard + +"@aws-sdk/util-arn-parser@npm:^3.972.3": + version: 3.972.3 + resolution: "@aws-sdk/util-arn-parser@npm:3.972.3" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/140a30615c914bcb37a5bb6ff825e8b6d2bedea757c2b03a4f5abb986003683ceadc322c0ee9f9a3ba4d5925357515ed7be01ef13c56c3b0126d4e1bd7292a33 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:^3.996.8": + version: 3.996.8 + resolution: "@aws-sdk/util-endpoints@npm:3.996.8" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-endpoints": "npm:^3.4.2" + tslib: "npm:^2.6.2" + checksum: 10/9b95fbe21751616f3b1453060dbf3db6350acc9dcb8490acfa53a161d3c7a96acc0297531613b4d2067c602fbb097b8f37c72bd7aafd803ab5589ab35c61a6be + languageName: node + linkType: hard + +"@aws-sdk/util-format-url@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/util-format-url@npm:3.972.10" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/querystring-builder": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/57698e7df25da153fee7b2b98ece6009d139faa4fb81bb4fa51455a462c1bd2696ba9b451a7d7f14e9a34b28b6c01e3f191458dcd3d7ca841e5ff91e59e799b1 + languageName: node + linkType: hard + +"@aws-sdk/util-locate-window@npm:^3.0.0": + version: 3.965.5 + resolution: "@aws-sdk/util-locate-window@npm:3.965.5" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/66391a7f6d0c383d6bc3ea67e35b0b0164798d9acbe47271fbc676cf74e7a56690f48425a91cce70e764c53ca46619f4abc076b569d8f991c19dd7c1ac4b0a79 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.972.10" + dependencies: + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10/dc76c0ede53607e7d98aff0f25a766dfa8f8a73ef974cd6976bf8d07d908effb88f1a374af44ce28c4babb0564e63207f4650e2a8b755dd9188c3a485b69f1ec + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:^3.973.24": + version: 3.973.24 + resolution: "@aws-sdk/util-user-agent-node@npm:3.973.24" + dependencies: + "@aws-sdk/middleware-user-agent": "npm:^3.972.38" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-config-provider": "npm:^4.2.2" + tslib: "npm:^2.6.2" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10/e94edde07b98f4ebae95a30a5f2b17514faba5643d596344d142457d7d827c34d7e9217308a479dab89cb95eed9aaadd1fa9479bb087b343d73541eb0b8abba2 + languageName: node + linkType: hard + +"@aws-sdk/xml-builder@npm:^3.972.22": + version: 3.972.22 + resolution: "@aws-sdk/xml-builder@npm:3.972.22" + dependencies: + "@nodable/entities": "npm:2.1.0" + "@smithy/types": "npm:^4.14.1" + fast-xml-parser: "npm:5.7.2" + tslib: "npm:^2.6.2" + checksum: 10/54032fdf33434cdefcecd374747c0cb16f1e13ee0237a089fdb49c2f01758fbb55c1ba22457cae86223af9abfdf54727099cbe67416af1664bc6e290e10f0b0d + languageName: node + linkType: hard + +"@aws/lambda-invoke-store@npm:^0.2.2": + version: 0.2.4 + resolution: "@aws/lambda-invoke-store@npm:0.2.4" + checksum: 10/47e73cf73141be73854c69722502e928a435b3d908ffa693a9545c1099dd7b2dd3f67c43c523d786a75911100e77ed52dce1f88d09363a67526448c5b7c804d5 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": version: 7.29.0 resolution: "@babel/code-frame@npm:7.29.0" @@ -930,6 +1539,23 @@ __metadata: languageName: node linkType: hard +"@google/genai@npm:^1.40.0": + version: 1.52.0 + resolution: "@google/genai@npm:1.52.0" + dependencies: + google-auth-library: "npm:^10.3.0" + p-retry: "npm:^4.6.2" + protobufjs: "npm:^7.5.4" + ws: "npm:^8.18.0" + peerDependencies: + "@modelcontextprotocol/sdk": ^1.25.2 + peerDependenciesMeta: + "@modelcontextprotocol/sdk": + optional: true + checksum: 10/4815bb7198910e20bc1976cb9c649d31c0d90b9352c5a433fc60a8906c74cd4022768daf75a7bdf86daa73beb9d0a6e426efbc2e5d6ed02df5c21ea63d335eae + languageName: node + linkType: hard + "@hapi/boom@npm:^9.1.3": version: 9.1.4 resolution: "@hapi/boom@npm:9.1.4" @@ -947,11 +1573,11 @@ __metadata: linkType: hard "@hono/node-server@npm:^1.19.9": - version: 1.19.13 - resolution: "@hono/node-server@npm:1.19.13" + version: 1.19.11 + resolution: "@hono/node-server@npm:1.19.11" peerDependencies: hono: ^4 - checksum: 10/67e453b0f6c0854244f3985eecbcf657b7d0247eb9404f2f507fec9242d8f8077ab8b0a212de1ac147290e76167e3598ca0df38940f6b63bde67f58526aba253 + checksum: 10/1718910924944bfa2f2649ae102fbdaee42e388ed373f1e36bb2674447b9f1a64a9344908c046c6100e6c387fe2d1dab5c95684cb3a415ba0a0b719fbef0a6cb languageName: node linkType: hard @@ -1213,6 +1839,8 @@ __metadata: dependencies: "@anthropic-ai/sdk": "npm:^0.71.2" "@inklabs/shared": "workspace:*" + "@mariozechner/pi-agent-core": "npm:0.71.1" + "@mariozechner/pi-coding-agent": "npm:0.71.1" "@modelcontextprotocol/sdk": "npm:^1.26.0" "@slack/bolt": "npm:^4.6.0" "@supabase/supabase-js": "npm:^2.39.3" @@ -2004,125 +2632,347 @@ __metadata: languageName: node linkType: hard -"@modelcontextprotocol/sdk@npm:^1.12.1, @modelcontextprotocol/sdk@npm:^1.26.0": - version: 1.27.1 - resolution: "@modelcontextprotocol/sdk@npm:1.27.1" - dependencies: - "@hono/node-server": "npm:^1.19.9" - ajv: "npm:^8.17.1" - ajv-formats: "npm:^3.0.1" - content-type: "npm:^1.0.5" - cors: "npm:^2.8.5" - cross-spawn: "npm:^7.0.5" - eventsource: "npm:^3.0.2" - eventsource-parser: "npm:^3.0.0" - express: "npm:^5.2.1" - express-rate-limit: "npm:^8.2.1" - hono: "npm:^4.11.4" - jose: "npm:^6.1.3" - json-schema-typed: "npm:^8.0.2" - pkce-challenge: "npm:^5.0.0" - raw-body: "npm:^3.0.0" - zod: "npm:^3.25 || ^4.0" - zod-to-json-schema: "npm:^3.25.1" - peerDependencies: - "@cfworker/json-schema": ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - "@cfworker/json-schema": - optional: true - zod: - optional: false - checksum: 10/3cb0d61cfb916e555c85b4a527e772f88fcf9c6abacbe5eb5e965aac7c898190c416341ab3b3cba8c2d5f5ce4d513279fba3ad7784a0903d7ccd335decc55395 +"@mariozechner/clipboard-darwin-arm64@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-darwin-arm64@npm:0.3.2" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.1": - version: 1.1.1 - resolution: "@napi-rs/wasm-runtime@npm:1.1.1" - dependencies: - "@emnapi/core": "npm:^1.7.1" - "@emnapi/runtime": "npm:^1.7.1" - "@tybys/wasm-util": "npm:^0.10.1" - checksum: 10/080e7f2aefb84e09884d21c650a2cbafdf25bfd2634693791b27e36eec0ddaa3c1656a943f8c913ac75879a0b04e68f8a827897ee655ab54a93169accf05b194 +"@mariozechner/clipboard-darwin-universal@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-darwin-universal@npm:0.3.2" + conditions: os=darwin languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.1.4 - resolution: "@napi-rs/wasm-runtime@npm:1.1.4" - dependencies: - "@tybys/wasm-util": "npm:^0.10.1" - peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af +"@mariozechner/clipboard-darwin-x64@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-darwin-x64@npm:0.3.2" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/env@npm:16.2.3": - version: 16.2.3 - resolution: "@next/env@npm:16.2.3" - checksum: 10/30ed128d8ffae47e58732ee134b78da36e2d6942da7479ec5e640d205b7822224daf2f07d7a69352dc362908eb260fc9fa7eaba1ce5e6311abeacc6ffb0fe90a +"@mariozechner/clipboard-linux-arm64-gnu@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-linux-arm64-gnu@npm:0.3.2" + conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-darwin-arm64@npm:16.2.3" - conditions: os=darwin & cpu=arm64 +"@mariozechner/clipboard-linux-arm64-musl@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-linux-arm64-musl@npm:0.3.2" + conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-darwin-x64@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-darwin-x64@npm:16.2.3" - conditions: os=darwin & cpu=x64 +"@mariozechner/clipboard-linux-riscv64-gnu@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-linux-riscv64-gnu@npm:0.3.2" + conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-linux-arm64-gnu@npm:16.2.3" - conditions: os=linux & cpu=arm64 & libc=glibc +"@mariozechner/clipboard-linux-x64-gnu@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-linux-x64-gnu@npm:0.3.2" + conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-linux-arm64-musl@npm:16.2.3" - conditions: os=linux & cpu=arm64 & libc=musl +"@mariozechner/clipboard-linux-x64-musl@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-linux-x64-musl@npm:0.3.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@mariozechner/clipboard-win32-arm64-msvc@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-win32-arm64-msvc@npm:0.3.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@mariozechner/clipboard-win32-x64-msvc@npm:0.3.2": + version: 0.3.2 + resolution: "@mariozechner/clipboard-win32-x64-msvc@npm:0.3.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@mariozechner/clipboard@npm:^0.3.5": + version: 0.3.5 + resolution: "@mariozechner/clipboard@npm:0.3.5" + dependencies: + "@mariozechner/clipboard-darwin-arm64": "npm:0.3.2" + "@mariozechner/clipboard-darwin-universal": "npm:0.3.2" + "@mariozechner/clipboard-darwin-x64": "npm:0.3.2" + "@mariozechner/clipboard-linux-arm64-gnu": "npm:0.3.2" + "@mariozechner/clipboard-linux-arm64-musl": "npm:0.3.2" + "@mariozechner/clipboard-linux-riscv64-gnu": "npm:0.3.2" + "@mariozechner/clipboard-linux-x64-gnu": "npm:0.3.2" + "@mariozechner/clipboard-linux-x64-musl": "npm:0.3.2" + "@mariozechner/clipboard-win32-arm64-msvc": "npm:0.3.2" + "@mariozechner/clipboard-win32-x64-msvc": "npm:0.3.2" + dependenciesMeta: + "@mariozechner/clipboard-darwin-arm64": + optional: true + "@mariozechner/clipboard-darwin-universal": + optional: true + "@mariozechner/clipboard-darwin-x64": + optional: true + "@mariozechner/clipboard-linux-arm64-gnu": + optional: true + "@mariozechner/clipboard-linux-arm64-musl": + optional: true + "@mariozechner/clipboard-linux-riscv64-gnu": + optional: true + "@mariozechner/clipboard-linux-x64-gnu": + optional: true + "@mariozechner/clipboard-linux-x64-musl": + optional: true + "@mariozechner/clipboard-win32-arm64-msvc": + optional: true + "@mariozechner/clipboard-win32-x64-msvc": + optional: true + checksum: 10/fc69ef49308b476cd05e02b6a1d92d97a9a4dffba3a4976c8eea69b64234e73db09ec251083fda04c72336e0760c20bef3ae79fe07e9c13b6fe34586e80ec24a languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-linux-x64-gnu@npm:16.2.3" +"@mariozechner/jiti@npm:^2.6.2": + version: 2.6.5 + resolution: "@mariozechner/jiti@npm:2.6.5" + dependencies: + std-env: "npm:^3.10.0" + yoctocolors: "npm:^2.1.2" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10/53e94727a025d722a1d624c8f0ebde0fff4dcc768e9ea79f0a17e42a1056864da7568d997022c60832b083ebd69914af5244577bf5873329be98134d5977a489 + languageName: node + linkType: hard + +"@mariozechner/pi-agent-core@npm:0.71.1, @mariozechner/pi-agent-core@npm:^0.71.1": + version: 0.71.1 + resolution: "@mariozechner/pi-agent-core@npm:0.71.1" + dependencies: + "@mariozechner/pi-ai": "npm:^0.71.1" + typebox: "npm:^1.1.24" + checksum: 10/f253ae61a39becb5b32956263fa58a236c153a196828527ea1d157cb86c6e626be33e88596e52713963e8e5e4cf261dda1c56746991aab09135ffd60d7978c1d + languageName: node + linkType: hard + +"@mariozechner/pi-ai@npm:^0.71.1": + version: 0.71.1 + resolution: "@mariozechner/pi-ai@npm:0.71.1" + dependencies: + "@anthropic-ai/sdk": "npm:^0.91.1" + "@aws-sdk/client-bedrock-runtime": "npm:^3.1030.0" + "@google/genai": "npm:^1.40.0" + "@mistralai/mistralai": "npm:^2.2.0" + chalk: "npm:^5.6.2" + openai: "npm:6.26.0" + partial-json: "npm:^0.1.7" + proxy-agent: "npm:^6.5.0" + typebox: "npm:^1.1.24" + undici: "npm:^7.19.1" + zod-to-json-schema: "npm:^3.24.6" + bin: + pi-ai: dist/cli.js + checksum: 10/121448e263bf5f9949c2265335f823d0161ba58cd938e248ed475eae2ce2f930510b3e3edd5a903c69f93a07f353bffc343b6cd2c89fbc5da1c0b5774d16260e + languageName: node + linkType: hard + +"@mariozechner/pi-coding-agent@npm:0.71.1": + version: 0.71.1 + resolution: "@mariozechner/pi-coding-agent@npm:0.71.1" + dependencies: + "@mariozechner/clipboard": "npm:^0.3.5" + "@mariozechner/jiti": "npm:^2.6.2" + "@mariozechner/pi-agent-core": "npm:^0.71.1" + "@mariozechner/pi-ai": "npm:^0.71.1" + "@mariozechner/pi-tui": "npm:^0.71.1" + "@silvia-odwyer/photon-node": "npm:^0.3.4" + chalk: "npm:^5.5.0" + cli-highlight: "npm:^2.1.11" + diff: "npm:^8.0.2" + extract-zip: "npm:^2.0.1" + file-type: "npm:^21.1.1" + glob: "npm:^13.0.1" + hosted-git-info: "npm:^9.0.2" + ignore: "npm:^7.0.5" + marked: "npm:^15.0.12" + minimatch: "npm:^10.2.3" + proper-lockfile: "npm:^4.1.2" + strip-ansi: "npm:^7.1.0" + typebox: "npm:^1.1.24" + undici: "npm:^7.19.1" + uuid: "npm:^14.0.0" + yaml: "npm:^2.8.2" + dependenciesMeta: + "@mariozechner/clipboard": + optional: true + bin: + pi: dist/cli.js + checksum: 10/ac868029019b9b4baf7e555a8ab29e8210b9163cd8bc203cfb70cdd8dffbb003c2c81c3b8c11ea6fabd1c80a33a4691efdf4bc33814fd171854724a423fa1cdd + languageName: node + linkType: hard + +"@mariozechner/pi-tui@npm:^0.71.1": + version: 0.71.1 + resolution: "@mariozechner/pi-tui@npm:0.71.1" + dependencies: + "@types/mime-types": "npm:^2.1.4" + chalk: "npm:^5.5.0" + get-east-asian-width: "npm:^1.3.0" + koffi: "npm:^2.9.0" + marked: "npm:^15.0.12" + mime-types: "npm:^3.0.1" + dependenciesMeta: + koffi: + optional: true + checksum: 10/ab29d89f6a6b678bb46d86adf91fc9947296c4054cef1790234bcb18b4997a46e5d9d78cc0cd29f7b2d9b2bd2796ee1d5d2d9f10fcf63d7cd9dad7514335a23d + languageName: node + linkType: hard + +"@mistralai/mistralai@npm:^2.2.0": + version: 2.2.1 + resolution: "@mistralai/mistralai@npm:2.2.1" + dependencies: + ws: "npm:^8.18.0" + zod: "npm:^3.25.0 || ^4.0.0" + zod-to-json-schema: "npm:^3.25.0" + checksum: 10/0a62da698b116b4d4ddfeb9d299774a1c9f113949e95d64b11a85b9e76fafd5f4713cf4c3ed70a75f88e34de5b7aaa14988e969290e554c089c9470c05c583cb + languageName: node + linkType: hard + +"@modelcontextprotocol/sdk@npm:^1.12.1, @modelcontextprotocol/sdk@npm:^1.26.0": + version: 1.27.1 + resolution: "@modelcontextprotocol/sdk@npm:1.27.1" + dependencies: + "@hono/node-server": "npm:^1.19.9" + ajv: "npm:^8.17.1" + ajv-formats: "npm:^3.0.1" + content-type: "npm:^1.0.5" + cors: "npm:^2.8.5" + cross-spawn: "npm:^7.0.5" + eventsource: "npm:^3.0.2" + eventsource-parser: "npm:^3.0.0" + express: "npm:^5.2.1" + express-rate-limit: "npm:^8.2.1" + hono: "npm:^4.11.4" + jose: "npm:^6.1.3" + json-schema-typed: "npm:^8.0.2" + pkce-challenge: "npm:^5.0.0" + raw-body: "npm:^3.0.0" + zod: "npm:^3.25 || ^4.0" + zod-to-json-schema: "npm:^3.25.1" + peerDependencies: + "@cfworker/json-schema": ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + "@cfworker/json-schema": + optional: true + zod: + optional: false + checksum: 10/3cb0d61cfb916e555c85b4a527e772f88fcf9c6abacbe5eb5e965aac7c898190c416341ab3b3cba8c2d5f5ce4d513279fba3ad7784a0903d7ccd335decc55395 + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:^1.1.1": + version: 1.1.1 + resolution: "@napi-rs/wasm-runtime@npm:1.1.1" + dependencies: + "@emnapi/core": "npm:^1.7.1" + "@emnapi/runtime": "npm:^1.7.1" + "@tybys/wasm-util": "npm:^0.10.1" + checksum: 10/080e7f2aefb84e09884d21c650a2cbafdf25bfd2634693791b27e36eec0ddaa3c1656a943f8c913ac75879a0b04e68f8a827897ee655ab54a93169accf05b194 + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:^1.1.4": + version: 1.1.4 + resolution: "@napi-rs/wasm-runtime@npm:1.1.4" + dependencies: + "@tybys/wasm-util": "npm:^0.10.1" + peerDependencies: + "@emnapi/core": ^1.7.1 + "@emnapi/runtime": ^1.7.1 + checksum: 10/1db3dc7eeb981306b09360487bd8ce4dfa5588d273bd8ea9f07dccca1b4ade57b675414180fc9bb66966c6c50b17208b0263194993e2f7f92cc7af28bda4d1af + languageName: node + linkType: hard + +"@next/env@npm:16.2.5": + version: 16.2.5 + resolution: "@next/env@npm:16.2.5" + checksum: 10/54e3b113f9f758e58b47bc4c4d927a6dac9561696c667a2e39e2f39df567925798b88ce6f53f0fe76e2ccc41446a567307bafcf533827e4d20d1d419f86e0d77 + languageName: node + linkType: hard + +"@next/swc-darwin-arm64@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-darwin-arm64@npm:16.2.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-darwin-x64@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-darwin-x64@npm:16.2.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-linux-arm64-gnu@npm:16.2.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-arm64-musl@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-linux-arm64-musl@npm:16.2.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-linux-x64-gnu@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-linux-x64-gnu@npm:16.2.5" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-linux-x64-musl@npm:16.2.3" +"@next/swc-linux-x64-musl@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-linux-x64-musl@npm:16.2.5" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-win32-arm64-msvc@npm:16.2.3" +"@next/swc-win32-arm64-msvc@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-win32-arm64-msvc@npm:16.2.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:16.2.3": - version: 16.2.3 - resolution: "@next/swc-win32-x64-msvc@npm:16.2.3" +"@next/swc-win32-x64-msvc@npm:16.2.5": + version: 16.2.5 + resolution: "@next/swc-win32-x64-msvc@npm:16.2.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard +"@nodable/entities@npm:2.1.0, @nodable/entities@npm:^2.1.0": + version: 2.1.0 + resolution: "@nodable/entities@npm:2.1.0" + checksum: 10/355c55e82aebe45d4b962d16530951df51e19e3e63a27ea61ad3260c0807064619b270b9c83db10e8394f42760abd5b7f7c5b5117678c4246ce8364a4aafc637 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -2193,10 +3043,10 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.127.0": - version: 0.127.0 - resolution: "@oxc-project/types@npm:0.127.0" - checksum: 10/f154f4720367186aed63a16fb1395f9039d4e6872265fe9e6b5eacc02fb2b948f9ea6c5f85efd3a015ea28aa8c31232b7a8301218ae28651659e46dd0c4f2031 +"@oxc-project/types@npm:=0.128.0": + version: 0.128.0 + resolution: "@oxc-project/types@npm:0.128.0" + checksum: 10/cf1479772d22902ee5a0a6fda74f71cce2a569d084f9e26b524972f3145439b2e514ffc92de518757c0502bf5d2d26cb1de99e4c56bd2c57f54fcd4179e9c730 languageName: node linkType: hard @@ -2228,6 +3078,13 @@ __metadata: languageName: node linkType: hard +"@protobufjs/codegen@npm:^2.0.5": + version: 2.0.5 + resolution: "@protobufjs/codegen@npm:2.0.5" + checksum: 10/290335fa114f26202abc0695f279d53e2fd516b01cfd8298923591e0bda011295ff40e3582a1cda0a0f27cbc5039a0292082d5ad08872bb5d6243a614ac15c88 + languageName: node + linkType: hard + "@protobufjs/eventemitter@npm:^1.1.0": version: 1.1.0 resolution: "@protobufjs/eventemitter@npm:1.1.0" @@ -2259,6 +3116,13 @@ __metadata: languageName: node linkType: hard +"@protobufjs/inquire@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/inquire@npm:1.1.1" + checksum: 10/504740e8ac348f70b33bcf6a20c83d5b9679901654c1a96b18c0491ec2f2f7ac580e74019b6d1bce16113bfb9746bc6e7dfd4e12a717deed699675b7f230ce9e + languageName: node + linkType: hard + "@protobufjs/path@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/path@npm:1.1.2" @@ -2280,6 +3144,13 @@ __metadata: languageName: node linkType: hard +"@protobufjs/utf8@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/utf8@npm:1.1.1" + checksum: 10/ed0c3f9ff1afd602a0aed54c4c03a0b8f641686a5587d8949e088dcac653fb2019d15691ed92eef23dfdf9f4293249532d0508ecd15cef810acf026917719a19 + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.1.3": version: 1.1.3 resolution: "@radix-ui/primitive@npm:1.1.3" @@ -2673,9 +3544,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.17" +"@rolldown/binding-android-arm64@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.18" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -2687,9 +3558,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.17" +"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.18" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -2701,9 +3572,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.17" +"@rolldown/binding-darwin-x64@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.18" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -2715,9 +3586,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.17" +"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.18" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -2729,9 +3600,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.17" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.18" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -2743,9 +3614,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.18" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -2757,9 +3628,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.17" +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.18" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -2771,9 +3642,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.18" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard @@ -2785,9 +3656,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.18" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard @@ -2799,9 +3670,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.17" +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.18" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -2813,9 +3684,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.17" +"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.18" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -2827,9 +3698,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.17" +"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.18" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -2841,9 +3712,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.17" +"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.18" dependencies: "@emnapi/core": "npm:1.10.0" "@emnapi/runtime": "npm:1.10.0" @@ -2861,9 +3732,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.17" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.18" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -2875,9 +3746,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.17" +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.18" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2889,10 +3760,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.17" - checksum: 10/d659ea756ee6d360a015708d1035c07047e08db99a4160c74c7f22a7ece5611efcc18ad56db4a63b69edb506ded47596d9c0d301919242470d8c412d916b9750 +"@rolldown/pluginutils@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.18" + checksum: 10/9dc298defd9615e55c7b57caca78e5f160d6a8dffbf823c990082421a59b7e6413daf113b81d93d9ef8db3d7608816a3117748e9b2f8d16e0964fe2c520d342e languageName: node linkType: hard @@ -2941,6 +3812,13 @@ __metadata: languageName: node linkType: hard +"@silvia-odwyer/photon-node@npm:^0.3.4": + version: 0.3.4 + resolution: "@silvia-odwyer/photon-node@npm:0.3.4" + checksum: 10/9353e3b2ed8e89f3410bdfe390c12e269867440ceb9a6f5c8c765732efd86bf299fbb7fae68c8959241878705b0f8516016c98cd83e69770bfc05f531dc6edb7 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.10 resolution: "@sinclair/typebox@npm:0.27.10" @@ -3049,6 +3927,541 @@ __metadata: languageName: node linkType: hard +"@smithy/config-resolver@npm:^4.4.17": + version: 4.4.17 + resolution: "@smithy/config-resolver@npm:4.4.17" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-config-provider": "npm:^4.2.2" + "@smithy/util-endpoints": "npm:^3.4.2" + "@smithy/util-middleware": "npm:^4.2.14" + tslib: "npm:^2.6.2" + checksum: 10/23590df61ac91cee66a7540d1a988244a1b299d6035375ea8753df2018c4f0aa5fc9317f9c487bef9055c8a89e0ae25be08268b7d22cb4410189803732202de8 + languageName: node + linkType: hard + +"@smithy/core@npm:^3.23.17": + version: 3.23.17 + resolution: "@smithy/core@npm:3.23.17" + dependencies: + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-body-length-browser": "npm:^4.2.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-stream": "npm:^4.5.25" + "@smithy/util-utf8": "npm:^4.2.2" + "@smithy/uuid": "npm:^1.1.2" + tslib: "npm:^2.6.2" + checksum: 10/fe609d8b83792eb4922b884ffd53b48a8a79c1c33c8d361b2538d3904ca34d1452992999735cc6b7dcf7eb4663f6cb542dd42495d9af561349b3a36157308e8f + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/credential-provider-imds@npm:4.2.14" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + tslib: "npm:^2.6.2" + checksum: 10/40b82b3e6a5ec6f52c9344642eb10a572f1a18483edf1948d14537adeddadb575757101bea240e8b33570f8d03622ba2fd4a81fd52da1113456076a3f2fa0a08 + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-codec@npm:4.2.14" + dependencies: + "@aws-crypto/crc32": "npm:5.2.0" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-hex-encoding": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/e6a2ec5f0dccf3f22094b11f008fd4e0c389cf6ac43abcf3971eddf9fcbb1eb3e7f4ad87c867df73d2340e0c713660d0d3fcdf71222a455d6b3771d8564d2a9c + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-serde-browser@npm:4.2.14" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/c3cd659638dd3fd70743c1305d6baef9ecafd3d98d5a1d06afc174e43ced92532c606ca1a1588999f8cbdf732c728b48d14d76ccb8b4c687ab3af43339f5e41a + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^4.3.14": + version: 4.3.14 + resolution: "@smithy/eventstream-serde-config-resolver@npm:4.3.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/1fe57d331395593e088622667e61fa2f322b14d7c7e46ea32b119f1462ba73c870c59a9d4910361cee5f3b1022f01e95f325f3a418eb07416af8dd5f65b8f868 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-serde-node@npm:4.2.14" + dependencies: + "@smithy/eventstream-serde-universal": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/81eff86735ae5a0a93863c5f0d86576bc91d590dd1606774e66d508199e7441919662bcfd01ce193f4198e86643d2cf74cbaed29e578d3259c0eaf61796b448e + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-serde-universal@npm:4.2.14" + dependencies: + "@smithy/eventstream-codec": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/47be7249a64ec44a7275a26e311b66769b055b082885504a2d50613a0b6c93b213db714f2668dd58ed852703b41665bc9b67c914dc229be4615244b3702cb006 + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^5.3.17": + version: 5.3.17 + resolution: "@smithy/fetch-http-handler@npm:5.3.17" + dependencies: + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/querystring-builder": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-base64": "npm:^4.3.2" + tslib: "npm:^2.6.2" + checksum: 10/1a5e737fbea32fa58b83e120d7d48e19ebfe0c64f03d7a1b09d97f3bd46bdf95beb54047a08d7e8712441ace12137e14f3a63a2ef6d699f4b5621ab200033d84 + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/hash-node@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + "@smithy/util-buffer-from": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/d5ef7312b9d1b3f85b3dfdabc90355fcf1c490b7c0c66031ba64f48fd2406cbae6408afd614dcb782b06a270da938339e7c3f9d641cc9e563c6ee5337b822431 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/invalid-dependency@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/3c2a86a3392822015a47e86afeaca11efa0413e57eec5400d8eb6d8f93d86b4bae2365e7d020aec340282e2378d03744aecb57f1218a5df125da9f97142631cd + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/is-array-buffer@npm:2.2.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/d366743ecc7a9fc3bad21dbb3950d213c12bdd4aeb62b1265bf6cbe38309df547664ef3e51ab732e704485194f15e89d361943b0bfbe3fe1a4b3178b942913cc + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/is-array-buffer@npm:4.2.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/ebf9bac3daad0e1c3b201d41c4d2ab4be0c08c4c34604f87965b73cb052b1fd99133088f3b9837527f8fd6ed071b8684bb554ff381e5fdeacfc5907a66e4688b + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/middleware-content-length@npm:4.2.14" + dependencies: + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/8da4f2b00323fcd3133b0d5d2b705cbbcc33c0f91aa1d4c43dfde8e0414b6756d98ee99764da5a0ee86e9c44555d646429c3e9e74e0360918efc6faf65d60c59 + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^4.4.32": + version: 4.4.32 + resolution: "@smithy/middleware-endpoint@npm:4.4.32" + dependencies: + "@smithy/core": "npm:^3.23.17" + "@smithy/middleware-serde": "npm:^4.2.20" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-middleware": "npm:^4.2.14" + tslib: "npm:^2.6.2" + checksum: 10/84b58d1856c245df8a3d0d8cb19f9577e580e0788bc7f0c2ae5b6d0d02e27a9c4fec89c640cb971ddec9635c9c62fd6482db1a82851e3c7d47e5d1fc08008324 + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^4.5.7": + version: 4.5.7 + resolution: "@smithy/middleware-retry@npm:4.5.7" + dependencies: + "@smithy/core": "npm:^3.23.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/service-error-classification": "npm:^4.3.1" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.6" + "@smithy/uuid": "npm:^1.1.2" + tslib: "npm:^2.6.2" + checksum: 10/accdd12c43fbc30703656a1849c80f3ef0e28835664078700240d83ca2f8636ca708000b08f5e7828f88fcc79ed9f06cb4f6c6c3dfc38f1cb03d339fdea8b21c + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^4.2.20": + version: 4.2.20 + resolution: "@smithy/middleware-serde@npm:4.2.20" + dependencies: + "@smithy/core": "npm:^3.23.17" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/84c6072347a66ec13fed32e53cc8d2027ddbb0e2d75cff1a66d1676ec2cfc8a4ee5e082a5f61d1d0904a0fbafe247e4dd9b8d78935837d34fb0de67a3e8379a2 + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/middleware-stack@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/353fdcbcd92233b93d303ecb9e827caccdd10136c6f0fec4a21bfb418483bb34dfb6fccfe3d25bfa9a78e0bb1a67a04ecc46ba7f206352a8c7e1f06b1afe6d00 + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^4.3.14": + version: 4.3.14 + resolution: "@smithy/node-config-provider@npm:4.3.14" + dependencies: + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/f184bf150f36c4ae6ba32a694c3e47bea6b406d0859b07d14d59864e7bb8868aa98f565ba3ea5a7cc1c107bf20b29d55318fe82ef95a1e156b2b5511af58c817 + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^4.6.1": + version: 4.6.1 + resolution: "@smithy/node-http-handler@npm:4.6.1" + dependencies: + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/querystring-builder": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/0dd0766f85a963a5a9b0ba0d6ead445819526b7abefadf84631812f2db114e7ad7115b188af603b2283568cbf0c0d47f064e1b6c04094042d8a06887e6c3cef7 + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/property-provider@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/63b496cb880bab841c12c53a64532fad4729c3713194e02027d721f8ed5e06b93084245abd7927a43f95626c71a404b11ce030f6e50b309786e5d3cf90181707 + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^5.3.14": + version: 5.3.14 + resolution: "@smithy/protocol-http@npm:5.3.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/4fdf35ce266dd86328be3b46e821ba30fdb7116e0af05956a9e7ad16ed03700a901859f6c34c46c40118b751625ae35c56d20e0476806ffa8cdfea466b604df7 + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/querystring-builder@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + "@smithy/util-uri-escape": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/cb9357512429e084c3acddd95bf888ccdc5ca981f62f885d1d91f989d6096d57fe2116ac58810f48483fbc7a58f6c4515848a8979ff16ea19ceba4224f710ff0 + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/querystring-parser@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/efbebff52a92859fa378ac16935c4fc2b70b738032a23daab9ff6bd6c0cc0fcd98838e4f51c3be98f3c20d9c4f9258b9a26dba56ecf51a226bfaaa066fa95bbf + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^4.3.1": + version: 4.3.1 + resolution: "@smithy/service-error-classification@npm:4.3.1" + dependencies: + "@smithy/types": "npm:^4.14.1" + checksum: 10/611fa6a143e48430d1cb9b3cefa9c5683a9bbb2e6a0215e36605bd35a679ab6347c6070bfee3c2376c25df24e497b87407d284d0b212b758017a4b179deaf424 + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^4.4.9": + version: 4.4.9 + resolution: "@smithy/shared-ini-file-loader@npm:4.4.9" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/8b094b76ce1af93e3acce92d67f3518a40e8527c160928531a793a976443b7f71b98a91b45f5b1e39ef716785cc27371a25d8c6b4796ba481ea077ace752991f + languageName: node + linkType: hard + +"@smithy/signature-v4@npm:^5.3.14": + version: 5.3.14 + resolution: "@smithy/signature-v4@npm:5.3.14" + dependencies: + "@smithy/is-array-buffer": "npm:^4.2.2" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-hex-encoding": "npm:^4.2.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-uri-escape": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/4f8110d30207eb1235e96795b284841c068c71d734170d54ada7aac87ef85be4fadf45488db115fb80dc111a12763bbeacf8f68d94302c03bfd521754dbd790b + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^4.12.13": + version: 4.12.13 + resolution: "@smithy/smithy-client@npm:4.12.13" + dependencies: + "@smithy/core": "npm:^3.23.17" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/middleware-stack": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-stream": "npm:^4.5.25" + tslib: "npm:^2.6.2" + checksum: 10/078cb152d478559cc77aca0b9e0d1307220e6a5dcb989667e44d9ae691eef8b3827acfdbadebc14a0cc5dee95f6d8acf58e3e238e31c32986756f6c8d85ce7dc + languageName: node + linkType: hard + +"@smithy/types@npm:^4.14.1": + version: 4.14.1 + resolution: "@smithy/types@npm:4.14.1" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/45ee555075cb41dc50ce983fd58c504b4c64ef5fa50e73fa2ff14c5fa014be4af112823b07975cb1aa683d77a8c8c520c95224227c9108a904561a8d175984d4 + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/url-parser@npm:4.2.14" + dependencies: + "@smithy/querystring-parser": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/cd7556388084b5d347192632c7c1b9560f4fc7eb85c7ead0b3520a64948434fd622609172a1da730a83bfa145e52ec63f80984906732bf910359d37c3d7102b1 + languageName: node + linkType: hard + +"@smithy/util-base64@npm:^4.3.2": + version: 4.3.2 + resolution: "@smithy/util-base64@npm:4.3.2" + dependencies: + "@smithy/util-buffer-from": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/9246e1874c2e96c2059e97c88e039bf40abb8b0d6fb229e1aca9df896371c44a9dc951c99d2febbd8267033920cef4180852e6655f3de8c4743d9a9bbf6d96b4 + languageName: node + linkType: hard + +"@smithy/util-body-length-browser@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-body-length-browser@npm:4.2.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/c3058710fddecee22d4bc03839310c13783a7593a0a1808998998c465f6408012b5ce34bbec379fff84fb56f4017747d7817c4a7f050b45834b2aada27a2e7a7 + languageName: node + linkType: hard + +"@smithy/util-body-length-node@npm:^4.2.3": + version: 4.2.3 + resolution: "@smithy/util-body-length-node@npm:4.2.3" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/ad271366f72505f66ac82b87f0964211c79f40cb5bebeceeb23d74613ce33f1e77a9e81b03b48aced35c76ddd7184103886ce13dde96c8ee6e05dec59239798b + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/util-buffer-from@npm:2.2.0" + dependencies: + "@smithy/is-array-buffer": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 10/53253e4e351df3c4b7907dca48a0a6ceae783e98a8e73526820b122b3047a53fd127c19f4d8301f68d852011d821da519da783de57e0b22eed57c4df5b90d089 + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-buffer-from@npm:4.2.2" + dependencies: + "@smithy/is-array-buffer": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/111148eb7fb8c2913c0f09ca9991a409c69b2df643aa73378e64e14404ce040f67c716f7b4f55b76c0640f4357b649b9eb6a7f1539d7b37a2f0a7e0c3ba7062d + languageName: node + linkType: hard + +"@smithy/util-config-provider@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-config-provider@npm:4.2.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/ff3989fc07b5162674ccce6aacf7c74dbaea9e7a731c05399a80ed758a67adf83e87d47431a69aa2b1c325497670ff7d1390d9441e3c0c2cea66e830f61f965a + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^4.3.49": + version: 4.3.49 + resolution: "@smithy/util-defaults-mode-browser@npm:4.3.49" + dependencies: + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/e51b381e4c62131e594e78bae53aa7ca5aea7e063f85d70e17d82c0034a14e7e77491ab4d1e5c781066c56706c225faf2116a9cda250e7c05a8b938d7d1f5365 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^4.2.54": + version: 4.2.54 + resolution: "@smithy/util-defaults-mode-node@npm:4.2.54" + dependencies: + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/credential-provider-imds": "npm:^4.2.14" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/e2ce34e7297275f3b71ede565c5bce7c605ff31824d775d98625b101851700d51b242cd270fc8d7cffce132eb3d908d99b9f333b4a90c8fc8e3984d0c7352a86 + languageName: node + linkType: hard + +"@smithy/util-endpoints@npm:^3.4.2": + version: 3.4.2 + resolution: "@smithy/util-endpoints@npm:3.4.2" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/b292b8e1c5eaef76989e205eba9eb4c39f3cc7e685a7258dd2a3b6cefd459b6208143724041241470eca52c4612b79a9a809a5a70cd281d86f2acaed7a232c0d + languageName: node + linkType: hard + +"@smithy/util-hex-encoding@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-hex-encoding@npm:4.2.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/2b1dfed0fcbe13c9a449b06adf805814fb3ec5d0d614704bfb250875cec7cf19f5a77a81013c91b81f45b7193038268f92d59de339192d578c9ef77a1b51c4d9 + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/util-middleware@npm:4.2.14" + dependencies: + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/b468d35214cfe1809db56a35d6cffb3dbc6d84ca911a88890d43040e726de68c98b2157d467630df2ad0625ee3e4bd208ae0b14c9f3140b2a56318b2c78700f2 + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^4.3.6": + version: 4.3.8 + resolution: "@smithy/util-retry@npm:4.3.8" + dependencies: + "@smithy/service-error-classification": "npm:^4.3.1" + "@smithy/types": "npm:^4.14.1" + tslib: "npm:^2.6.2" + checksum: 10/1e093b8d1e8b00ba5a381cfc65cbeff80d5930823d73c253ab73d06449d397a8c2e045e4a201753557551f6518a215432562e326289a8209ac8ad57bdc11a0c9 + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^4.5.25": + version: 4.5.25 + resolution: "@smithy/util-stream@npm:4.5.25" + dependencies: + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-base64": "npm:^4.3.2" + "@smithy/util-buffer-from": "npm:^4.2.2" + "@smithy/util-hex-encoding": "npm:^4.2.2" + "@smithy/util-utf8": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/0767af65bab121afe8bc44be9baed7e647b0e55e773ab61cebc812df24b3699f0daa4c09206b21cfb5993c9970a59c09359e3a7a1abb8b9ff2556cfb5b9016a0 + languageName: node + linkType: hard + +"@smithy/util-uri-escape@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-uri-escape@npm:4.2.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/2b649e431a92c89e4fb7cc817e7bfcbe547d07f725c401445ea81aaea37e4ede383e1e2b9c3f0dfb228dee597166e95805a2f3e57fa6ae1b5341abc48a397935 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.0": + version: 2.3.0 + resolution: "@smithy/util-utf8@npm:2.3.0" + dependencies: + "@smithy/util-buffer-from": "npm:^2.2.0" + tslib: "npm:^2.6.2" + checksum: 10/c766ead8dac6bc6169f4cac1cc47ef7bd86928d06255148f9528228002f669c8cc49f78dc2b9ba5d7e214d40315024a9e32c5c9130b33e20f0fe4532acd0dff5 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^4.2.2": + version: 4.2.2 + resolution: "@smithy/util-utf8@npm:4.2.2" + dependencies: + "@smithy/util-buffer-from": "npm:^4.2.2" + tslib: "npm:^2.6.2" + checksum: 10/4dd23ac07cab78279a9f4f250b43df69d3303458b741e38c447ba7e92f7c6b1651076479023687b9eb3996aa3269ee1a0d363a1c320d15e78247ca9ea74aca58 + languageName: node + linkType: hard + +"@smithy/uuid@npm:^1.1.2": + version: 1.1.2 + resolution: "@smithy/uuid@npm:1.1.2" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10/35b77a2483a37755c2be1faf66036f5e0b7939a7c608b93982fce9d4f137f1778784f101a2874a6756d9fd25092c6a95dd07314df12dcb9a0a03244b4cc4d8c4 + languageName: node + linkType: hard + "@so-ric/colorspace@npm:^1.1.6": version: 1.1.6 resolution: "@so-ric/colorspace@npm:1.1.6" @@ -3540,6 +4953,13 @@ __metadata: languageName: node linkType: hard +"@tootallnate/quickjs-emscripten@npm:^0.23.0": + version: 0.23.0 + resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" + checksum: 10/95cbad451d195b9d8f312103abafcc010741eb9256e98d7953e7c026d4c1ed4abb2248a14018bf49e3201c350104fc643137b23aa0bbed2744c795c39dc48a28 + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.1": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -3899,6 +5319,13 @@ __metadata: languageName: node linkType: hard +"@types/mime-types@npm:^2.1.4": + version: 2.1.4 + resolution: "@types/mime-types@npm:2.1.4" + checksum: 10/f8c521c54ee0c0b9f90a65356a80b1413ed27ccdc94f5c7ebb3de5d63cedb559cd2610ea55b4100805c7349606a920d96e54f2d16b2f0afa6b7cd5253967ccc9 + languageName: node + linkType: hard + "@types/mime@npm:^1": version: 1.3.5 resolution: "@types/mime@npm:1.3.5" @@ -4107,6 +5534,15 @@ __metadata: languageName: node linkType: hard +"@types/yauzl@npm:^2.9.1": + version: 2.10.3 + resolution: "@types/yauzl@npm:2.10.3" + dependencies: + "@types/node": "npm:*" + checksum: 10/5ee966ea7bd6b2802f31ad4281c92c4c0b6dfa593c378a2582c58541fa113bec3d70eb0696b34ad95e8e6861a884cba6c3e351285816693ed176222f840a8c08 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^6.17.0": version: 6.21.0 resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0" @@ -4620,6 +6056,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:^0.13.4": + version: 0.13.4 + resolution: "ast-types@npm:0.13.4" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10/c55b375b9aaf44713d8c0f77a08215ab6d44f368b13e44f2141c421022af3c62b615a30c8ea629457f0cbaec409c713401c0188a124552c8fe4a5ad6b17ff3c3 + languageName: node + linkType: hard + "ast-v8-to-istanbul@npm:^1.0.0": version: 1.0.0 resolution: "ast-v8-to-istanbul@npm:1.0.0" @@ -4821,6 +6266,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + "base64-js@npm:^1.3.0": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -4837,6 +6289,13 @@ __metadata: languageName: node linkType: hard +"basic-ftp@npm:^5.0.2": + version: 5.3.1 + resolution: "basic-ftp@npm:5.3.1" + checksum: 10/9232ee155114efafadf5adee86a6750208653a9071e53e9803dceac61a66b3ba3974771ff2490ff28f1a118fdfb806ffcc488f64609e61a9cedb52480b312843 + languageName: node + linkType: hard + "bcrypt@npm:^6.0.0": version: 6.0.0 resolution: "bcrypt@npm:6.0.0" @@ -4906,13 +6365,29 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:1.1.13": - version: 1.1.13 - resolution: "brace-expansion@npm:1.1.13" +"bowser@npm:^2.11.0": + version: 2.14.1 + resolution: "bowser@npm:2.14.1" + checksum: 10/a002f0795ef360314c75552b94daa42f74473f38b34255cfa959779e875806ef8e41b24ec63a533717798c8ef70bb991aef3037a2bb5dd32e8f507b39a509163 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.14 + resolution: "brace-expansion@npm:1.1.14" dependencies: balanced-match: "npm:^1.0.0" concat-map: "npm:0.0.1" - checksum: 10/b5f4329fdbe9d2e25fa250c8f866ebd054ba946179426e99b86dcccddabdb1d481f0e40ee5430032e62a7d0a6c2837605ace6783d015aa1d65d85ca72154d936 + checksum: 10/2de747a5891ea0d3a1946ea1ae26e056a47f7ea8d42a3009e1736ec3a31a5aa69a3c5da59d998426773553afe4c258e5b12d7953b534fa7f2cf12ce92eed4931 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.2, brace-expansion@npm:^5.0.5": + version: 5.0.5 + resolution: "brace-expansion@npm:5.0.5" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/f259b2ddf04489da9512ad637ba6b4ef2d77abd4445d20f7f1714585f153435200a53fa6a2e4a5ee974df14ddad4cd16421f6f803e96e8b452bd48598878d0ee languageName: node linkType: hard @@ -4975,6 +6450,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 10/06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + "buffer-equal-constant-time@npm:^1.0.1": version: 1.0.1 resolution: "buffer-equal-constant-time@npm:1.0.1" @@ -5120,7 +6602,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.3.0, chalk@npm:^5.6.0": +"chalk@npm:^5.3.0, chalk@npm:^5.5.0, chalk@npm:^5.6.0, chalk@npm:^5.6.2": version: 5.6.2 resolution: "chalk@npm:5.6.2" checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 @@ -5294,6 +6776,22 @@ __metadata: languageName: node linkType: hard +"cli-highlight@npm:^2.1.11": + version: 2.1.11 + resolution: "cli-highlight@npm:2.1.11" + dependencies: + chalk: "npm:^4.0.0" + highlight.js: "npm:^10.7.1" + mz: "npm:^2.4.0" + parse5: "npm:^5.1.1" + parse5-htmlparser2-tree-adapter: "npm:^6.0.0" + yargs: "npm:^16.0.0" + bin: + highlight: bin/highlight + checksum: 10/05d2b5beb8a4d3259f693517d013bf53d04ad20f470b77c3d02e051963092fae388388e3127f67d3679884a0c32cb855bf590292017c5e68c0f8d86f4b8e146e + languageName: node + linkType: hard + "cli-spinners@npm:^2.9.2": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" @@ -5346,6 +6844,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^7.0.0" + checksum: 10/db858c49af9d59a32d603987e6fddaca2ce716cd4602ba5a2bb3a5af1351eebe82aba8dff3ef3e1b331f7fa9d40ca66e67bdf8e7c327ce0ea959747ead65c0ef + languageName: node + linkType: hard + "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -5702,6 +7211,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^6.0.2": + version: 6.0.2 + resolution: "data-uri-to-buffer@npm:6.0.2" + checksum: 10/8b6927c33f9b54037f442856be0aa20e5fd49fa6c9c8ceece408dc306445d593ad72d207d57037c529ce65f413b421da800c6827b1dbefb607b8056f17123a61 + languageName: node + linkType: hard + "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -5765,6 +7281,17 @@ __metadata: languageName: node linkType: hard +"degenerator@npm:^5.0.0": + version: 5.0.1 + resolution: "degenerator@npm:5.0.1" + dependencies: + ast-types: "npm:^0.13.4" + escodegen: "npm:^2.1.0" + esprima: "npm:^4.0.1" + checksum: 10/a64fa39cdf6c2edd75188157d32338ee9de7193d7dbb2aeb4acb1eb30fa4a15ed80ba8dae9bd4d7b085472cf174a5baf81adb761aaa8e326771392c922084152 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -5844,6 +7371,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^8.0.2": + version: 8.0.4 + resolution: "diff@npm:8.0.4" + checksum: 10/b4036ceda0d1e10683a2313079ed52c5e6b09553ae29da87bce81d98714d9725dbf3c0f6f7c3b1f16eec049fe17087e38ee329e732580fa62f6ec1c2487b2435 + languageName: node + linkType: hard + "dijkstrajs@npm:^1.0.1": version: 1.0.3 resolution: "dijkstrajs@npm:1.0.3" @@ -6025,6 +7559,15 @@ __metadata: languageName: node linkType: hard +"end-of-stream@npm:^1.1.0": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: "npm:^1.4.0" + checksum: 10/1e0cfa6e7f49887544e03314f9dfc56a8cb6dde910cbb445983ecc2ff426fc05946df9d75d8a21a3a64f2cecfe1bf88f773952029f46756b2ed64a24e95b1fb8 + languageName: node + linkType: hard + "entities@npm:^4.2.0, entities@npm:^4.4.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -6240,6 +7783,24 @@ __metadata: languageName: node linkType: hard +"escodegen@npm:^2.1.0": + version: 2.1.0 + resolution: "escodegen@npm:2.1.0" + dependencies: + esprima: "npm:^4.0.1" + estraverse: "npm:^5.2.0" + esutils: "npm:^2.0.2" + source-map: "npm:~0.6.1" + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 10/47719a65b2888b4586e3fa93769068b275961c13089e90d5d01a96a6e8e95871b1c3893576814c8fbf08a4a31a496f37e7b2c937cf231270f4d81de012832c7c + languageName: node + linkType: hard + "eslint-scope@npm:^7.2.2": version: 7.2.2 resolution: "eslint-scope@npm:7.2.2" @@ -6316,7 +7877,7 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0": +"esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" bin: @@ -6571,6 +8132,23 @@ __metadata: languageName: node linkType: hard +"extract-zip@npm:^2.0.1": + version: 2.0.1 + resolution: "extract-zip@npm:2.0.1" + dependencies: + "@types/yauzl": "npm:^2.9.1" + debug: "npm:^4.1.1" + get-stream: "npm:^5.1.0" + yauzl: "npm:^2.10.0" + dependenciesMeta: + "@types/yauzl": + optional: true + bin: + extract-zip: cli.js + checksum: 10/8cbda9debdd6d6980819cc69734d874ddd71051c9fe5bde1ef307ebcedfe949ba57b004894b585f758b7c9eeeea0e3d87f2dda89b7d25320459c2c9643ebb635 + languageName: node + linkType: hard + "fast-deep-equal@npm:3.1.3, fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -6644,6 +8222,29 @@ __metadata: languageName: node linkType: hard +"fast-xml-builder@npm:^1.1.5": + version: 1.1.8 + resolution: "fast-xml-builder@npm:1.1.8" + dependencies: + path-expression-matcher: "npm:^1.1.3" + checksum: 10/9ed7f690d0718392aececad0344cb43f59ca99b25fbeee25ea365a3a564629e444bb685677c6b5222bfdb61f514d0ee4d6fd03d6872b79000f4b7354f9c35084 + languageName: node + linkType: hard + +"fast-xml-parser@npm:5.7.2": + version: 5.7.2 + resolution: "fast-xml-parser@npm:5.7.2" + dependencies: + "@nodable/entities": "npm:^2.1.0" + fast-xml-builder: "npm:^1.1.5" + path-expression-matcher: "npm:^1.5.0" + strnum: "npm:^2.2.3" + bin: + fxparser: src/cli/cli.js + checksum: 10/7f32d77127dbd5eb1b4c9f7f6ad81972527049905cebe8926c88d102b84c2a56468180dd7541384c93bfc8dad2cef0b1b82b097a931893d3cab3989bd1cf83e1 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.20.1 resolution: "fastq@npm:1.20.1" @@ -6662,6 +8263,15 @@ __metadata: languageName: node linkType: hard +"fd-slicer@npm:~1.1.0": + version: 1.1.0 + resolution: "fd-slicer@npm:1.1.0" + dependencies: + pend: "npm:~1.2.0" + checksum: 10/db3e34fa483b5873b73f248e818f8a8b59a6427fd8b1436cd439c195fdf11e8659419404826059a642b57d18075c856d06d6a50a1413b714f12f833a9341ead3 + languageName: node + linkType: hard + "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -6700,6 +8310,18 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^21.1.1": + version: 21.3.4 + resolution: "file-type@npm:21.3.4" + dependencies: + "@tokenizer/inflate": "npm:^0.4.1" + strtok3: "npm:^10.3.4" + token-types: "npm:^6.1.1" + uint8array-extras: "npm:^1.4.0" + checksum: 10/42d5cf6aafb998fb2f0357e96ea7c48bcce5249f899523a3a5a4c297f4fe2346cc0aff9cf04243ada5c54b6457183550b2a04902b6533cd5e64aaa344d78e0eb + languageName: node + linkType: hard + "file-type@npm:^21.3.1": version: 21.3.3 resolution: "file-type@npm:21.3.3" @@ -6795,7 +8417,17 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.11, follow-redirects@npm:^1.16.0": +"follow-redirects@npm:^1.15.11": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba + languageName: node + linkType: hard + +"follow-redirects@npm:^1.16.0": version: 1.16.0 resolution: "follow-redirects@npm:1.16.0" peerDependenciesMeta: @@ -6940,7 +8572,7 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": version: 1.5.0 resolution: "get-east-asian-width@npm:1.5.0" checksum: 10/60bc34cd1e975055ab99f0f177e31bed3e516ff7cee9c536474383954a976abaa6b94a51d99ad158ef1e372790fa096cab7d07f166bb0778f6587954c0fbe946 @@ -6992,6 +8624,15 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^5.1.0": + version: 5.2.0 + resolution: "get-stream@npm:5.2.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 10/13a73148dca795e41421013da6e3ebff8ccb7fba4d2f023fd0c6da2c166ec4e789bec9774a73a7b49c08daf2cae552f8a3e914042ac23b5f59dd278cc8f9cbfb + languageName: node + linkType: hard + "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -7008,6 +8649,17 @@ __metadata: languageName: node linkType: hard +"get-uri@npm:^6.0.1": + version: 6.0.5 + resolution: "get-uri@npm:6.0.5" + dependencies: + basic-ftp: "npm:^5.0.2" + data-uri-to-buffer: "npm:^6.0.2" + debug: "npm:^4.3.4" + checksum: 10/6daa56eb367dc030ae7bf6db4b5d36f200c9bb47ab00593c142176e4f33f22e129a294ac94329c6bcaebda19b7506080267a336742d20a915fb2bef9c400347f + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -7026,7 +8678,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^13.0.0": +"glob@npm:^13.0.0, glob@npm:^13.0.1": version: 13.0.6 resolution: "glob@npm:13.0.6" dependencies: @@ -7074,7 +8726,7 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^10.1.0, google-auth-library@npm:^10.2.0": +"google-auth-library@npm:^10.1.0, google-auth-library@npm:^10.2.0, google-auth-library@npm:^10.3.0": version: 10.6.2 resolution: "google-auth-library@npm:10.6.2" dependencies: @@ -7125,7 +8777,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -7237,10 +8889,17 @@ __metadata: languageName: node linkType: hard +"highlight.js@npm:^10.7.1": + version: 10.7.3 + resolution: "highlight.js@npm:10.7.3" + checksum: 10/db8d10a541936b058e221dbde77869664b2b45bca75d660aa98065be2cd29f3924755fbc7348213f17fd931aefb6e6597448ba6fe82afba6d8313747a91983ee + languageName: node + linkType: hard + "hono@npm:^4.11.4": - version: 4.12.18 - resolution: "hono@npm:4.12.18" - checksum: 10/0adda91eeb68c921e073bcbedd8758f026cc47dbbe7824147cf0ee8ef03004b1891f4d4edc80f6ab4c6ab33ec1f2fac52c4f7098752f8acf6833bcb76efb39e4 + version: 4.12.8 + resolution: "hono@npm:4.12.8" + checksum: 10/f6cbb5cd6f24c1a8eac3bcda82481f2ef77d6f56b5754c9004a1be8f2672433a401cd670d62e7852f3065b6daf49e3f16ad9525c09f5cf40858a903b95404d74 languageName: node linkType: hard @@ -7258,6 +8917,15 @@ __metadata: languageName: node linkType: hard +"hosted-git-info@npm:^9.0.2": + version: 9.0.3 + resolution: "hosted-git-info@npm:9.0.3" + dependencies: + lru-cache: "npm:^11.1.0" + checksum: 10/c5683d03a57a691c60973eb7f6e6b83e0d70903df7e8e032c9a4673fb9c70c6df70cdcd0f3bf796426c75973a3251c39b0755510ff1ec2a704239668ef1881cc + languageName: node + linkType: hard + "html-comment-regex@npm:^1.1.2": version: 1.1.2 resolution: "html-comment-regex@npm:1.1.2" @@ -7311,7 +8979,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -7321,7 +8989,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -7386,6 +9054,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1": version: 3.3.1 resolution: "import-fresh@npm:3.3.1" @@ -8433,6 +10108,13 @@ __metadata: languageName: node linkType: hard +"koffi@npm:^2.9.0": + version: 2.16.1 + resolution: "koffi@npm:2.16.1" + checksum: 10/6675c723e0c120246a469c4e93895b7bf0837b0a862ef8a160b846f1684d23096da9a65f3b77cd8a85b775dcba668d478634e0b712c76d85e8a9a1eca1faadf6 + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -8831,6 +10513,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^7.14.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: 10/6029ca5aba3aacb554e919d7ef804fffd4adfc4c83db00fac8248c7c78811fb6d4b6f70f7fd9d55032b3823446546a007edaa66ad1f2377ae833bd983fac5d98 + languageName: node + linkType: hard + "lucide-react@npm:^0.469.0": version: 0.469.0 resolution: "lucide-react@npm:0.469.0" @@ -8958,6 +10647,15 @@ __metadata: languageName: node linkType: hard +"marked@npm:^15.0.12": + version: 15.0.12 + resolution: "marked@npm:15.0.12" + bin: + marked: bin/marked.js + checksum: 10/deeb619405c0c46af00c99b18b3365450abeb309104b24e3658f46142344f6b7c4117608c3b5834084d8738e92f81240c19f596e6ee369260f96e52b3457eaee + languageName: node + linkType: hard + "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -9757,7 +11455,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^3.0.0, mime-types@npm:^3.0.2": +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1, mime-types@npm:^3.0.2": version: 3.0.2 resolution: "mime-types@npm:3.0.2" dependencies: @@ -9807,6 +11505,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.2.3": + version: 10.2.5 + resolution: "minimatch@npm:10.2.5" + dependencies: + brace-expansion: "npm:^5.0.5" + checksum: 10/19e87a931aff60ee7b9d80f39f817b8bfc54f61f8356ee3549fbf636dbccacacfec8d803eac73293955c4527cd085247dfc064bce4a5e349f8f3b85e2bf5da0f + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.5 resolution: "minimatch@npm:3.1.5" @@ -9954,7 +11661,7 @@ __metadata: languageName: node linkType: hard -"mz@npm:^2.7.0": +"mz@npm:^2.4.0, mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" dependencies: @@ -10009,19 +11716,26 @@ __metadata: languageName: node linkType: hard +"netmask@npm:^2.0.2": + version: 2.1.1 + resolution: "netmask@npm:2.1.1" + checksum: 10/473430b03b09787ad80041d5df4f43d93e7d9484c2646156eee0b31b95d627983aa5ccaf2bb9fc3fb3926bcf2dab82aa41005ad90d0095eb802f9a2354b2de27 + languageName: node + linkType: hard + "next@npm:^16.2.3": - version: 16.2.3 - resolution: "next@npm:16.2.3" - dependencies: - "@next/env": "npm:16.2.3" - "@next/swc-darwin-arm64": "npm:16.2.3" - "@next/swc-darwin-x64": "npm:16.2.3" - "@next/swc-linux-arm64-gnu": "npm:16.2.3" - "@next/swc-linux-arm64-musl": "npm:16.2.3" - "@next/swc-linux-x64-gnu": "npm:16.2.3" - "@next/swc-linux-x64-musl": "npm:16.2.3" - "@next/swc-win32-arm64-msvc": "npm:16.2.3" - "@next/swc-win32-x64-msvc": "npm:16.2.3" + version: 16.2.5 + resolution: "next@npm:16.2.5" + dependencies: + "@next/env": "npm:16.2.5" + "@next/swc-darwin-arm64": "npm:16.2.5" + "@next/swc-darwin-x64": "npm:16.2.5" + "@next/swc-linux-arm64-gnu": "npm:16.2.5" + "@next/swc-linux-arm64-musl": "npm:16.2.5" + "@next/swc-linux-x64-gnu": "npm:16.2.5" + "@next/swc-linux-x64-musl": "npm:16.2.5" + "@next/swc-win32-arm64-msvc": "npm:16.2.5" + "@next/swc-win32-x64-msvc": "npm:16.2.5" "@swc/helpers": "npm:0.5.15" baseline-browser-mapping: "npm:^2.9.19" caniuse-lite: "npm:^1.0.30001579" @@ -10065,7 +11779,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/5164885daacbb36a771380e1b5efba524863e1bdf2b5a6c80413cbf1e3ab4e8ddab5716cd91ff94ca5c5c5deb2a12d1312d6d6ae994e16ebfa985fdda6134bc6 + checksum: 10/65a095a5c02d6c80cc5520f8816e9f1c18ee146ca57efc2da9fb2e2b48508e3655b13c8af12d8d02eaa9b40d4e4b946f2a87e82addc0d3564c7f984158880957 languageName: node linkType: hard @@ -10263,7 +11977,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.4.0": +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -10299,6 +12013,23 @@ __metadata: languageName: node linkType: hard +"openai@npm:6.26.0": + version: 6.26.0 + resolution: "openai@npm:6.26.0" + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + bin: + openai: bin/cli + checksum: 10/33dc7beb65916331985ba811b7a30fab974f47ceed6b4ef1bf537d35a977c9cc1d8471154456fd0ab5efd1656e8881f447756cebf88def568973fbf5d08ca392 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -10407,7 +12138,7 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:^4": +"p-retry@npm:^4, p-retry@npm:^4.6.2": version: 4.6.2 resolution: "p-retry@npm:4.6.2" dependencies: @@ -10447,6 +12178,32 @@ __metadata: languageName: node linkType: hard +"pac-proxy-agent@npm:^7.1.0": + version: 7.2.0 + resolution: "pac-proxy-agent@npm:7.2.0" + dependencies: + "@tootallnate/quickjs-emscripten": "npm:^0.23.0" + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + get-uri: "npm:^6.0.1" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.6" + pac-resolver: "npm:^7.0.1" + socks-proxy-agent: "npm:^8.0.5" + checksum: 10/187656be62d5a6b983d90a86d64106a38b1a9ee78f591fabb27b3cf0d51e5d528456a9faaaf981c93dd54dc9c9ee8d33e35a51072b73a19ec1a8e0d0c36a2b99 + languageName: node + linkType: hard + +"pac-resolver@npm:^7.0.1": + version: 7.0.1 + resolution: "pac-resolver@npm:7.0.1" + dependencies: + degenerator: "npm:^5.0.0" + netmask: "npm:^2.0.2" + checksum: 10/839134328781b80d49f9684eae1f5c74f50a1d4482076d44c84fc2f3ca93da66fa11245a4725a057231e06b311c20c989fd0681e662a0792d17f644d8fe62a5e + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1" @@ -10497,6 +12254,15 @@ __metadata: languageName: node linkType: hard +"parse5-htmlparser2-tree-adapter@npm:^6.0.0": + version: 6.0.1 + resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1" + dependencies: + parse5: "npm:^6.0.1" + checksum: 10/3400a2cd1ad450b2fe148544154f86ea53d3ed6b6eab56c78bb43b9629d3dfe9f580dffd75bbf32be134ffef645b68081fc764bf75c210f236ab9c5c8c38c252 + languageName: node + linkType: hard + "parse5-htmlparser2-tree-adapter@npm:^7.0.0": version: 7.1.0 resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0" @@ -10507,6 +12273,20 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^5.1.1": + version: 5.1.1 + resolution: "parse5@npm:5.1.1" + checksum: 10/5b509744cfe81488a33be05578df490c460690e64519fa67f0a0acb9c1bca05914e8acad17a977e2cf5964a000e43959b40024f0c243dd6595dd0cca8a32f71b + languageName: node + linkType: hard + +"parse5@npm:^6.0.1": + version: 6.0.1 + resolution: "parse5@npm:6.0.1" + checksum: 10/dfb110581f62bd1425725a7c784ae022a24669bd0efc24b58c71fc731c4d868193e2ebd85b74cde2dbb965e4dcf07059b1e651adbec1b3b5267531bd132fdb75 + languageName: node + linkType: hard + "parse5@npm:^7.0.0": version: 7.3.0 resolution: "parse5@npm:7.3.0" @@ -10523,6 +12303,13 @@ __metadata: languageName: node linkType: hard +"partial-json@npm:^0.1.7": + version: 0.1.7 + resolution: "partial-json@npm:0.1.7" + checksum: 10/6fb8d305638e4747fccfc0649b8a8de99314bfafcc4e3136a3a27c0632857c40b62e6e6bb5f17af00472387393f1d8a58a7865242ca20382af31b593a7bb1f89 + languageName: node + linkType: hard + "patch-console@npm:^2.0.0": version: 2.0.0 resolution: "patch-console@npm:2.0.0" @@ -10537,6 +12324,13 @@ __metadata: languageName: node linkType: hard +"path-expression-matcher@npm:^1.1.3, path-expression-matcher@npm:^1.5.0": + version: 1.5.0 + resolution: "path-expression-matcher@npm:1.5.0" + checksum: 10/28303bb9ee6831e6df14c10cd3f3f7b2d7c8d7f788d8bdb7440136fd696064c82a3e264999a0764d28e39f698275fc03a5493bec93c57ef4a22566280367dd64 + languageName: node + linkType: hard + "path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -10603,6 +12397,13 @@ __metadata: languageName: node linkType: hard +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 10/6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -10812,7 +12613,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.5.10": +"postcss@npm:^8.5.14": version: 8.5.14 resolution: "postcss@npm:8.5.14" dependencies: @@ -10874,6 +12675,17 @@ __metadata: languageName: node linkType: hard +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: "npm:^4.2.4" + retry: "npm:^0.12.0" + signal-exit: "npm:^3.0.2" + checksum: 10/000a4875f543f591872b36ca94531af8a6463ddb0174f41c0b004d19e231d7445268b422ff1ea595e43d238655c702250cd3d27f408e7b9d97b56f1533ba26bf + languageName: node + linkType: hard + "property-information@npm:^7.0.0": version: 7.1.0 resolution: "property-information@npm:7.1.0" @@ -11119,6 +12931,26 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^7.5.4": + version: 7.5.6 + resolution: "protobufjs@npm:7.5.6" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.5" + "@protobufjs/eventemitter": "npm:^1.1.0" + "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/inquire": "npm:^1.1.1" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.1" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.0.0" + checksum: 10/13d7ab5b5d588b704476a600506cda9f647e2f657faf5a18e8866ce5b04800026a508a4958673e4d5674090bbc528717b01128f8e226055d56e90745e3ee6ada + languageName: node + linkType: hard + "proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -11129,6 +12961,22 @@ __metadata: languageName: node linkType: hard +"proxy-agent@npm:^6.5.0": + version: 6.5.0 + resolution: "proxy-agent@npm:6.5.0" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + http-proxy-agent: "npm:^7.0.1" + https-proxy-agent: "npm:^7.0.6" + lru-cache: "npm:^7.14.1" + pac-proxy-agent: "npm:^7.1.0" + proxy-from-env: "npm:^1.1.0" + socks-proxy-agent: "npm:^8.0.5" + checksum: 10/56d5a494d96dafad94868870af776939e7b9aaca172465a5c251d2523496a8353b029c32d2a72a012bd62622cdc9a43ba3df59b5738ab7b740bc6b362e9f9477 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -11143,6 +12991,16 @@ __metadata: languageName: node linkType: hard +"pump@npm:^3.0.0": + version: 3.0.4 + resolution: "pump@npm:3.0.4" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10/d043c3e710c56ffd280711e98a94e863ab334f79ea43cee0fb70e1349b2355ffd2ff287c7522e4c960a247699d5b7825f00fa090b85d6179c973be13f78a6c49 + languageName: node + linkType: hard + "punycode.js@npm:^2.3.1": version: 2.3.1 resolution: "punycode.js@npm:2.3.1" @@ -11632,6 +13490,13 @@ __metadata: languageName: node linkType: hard +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10/1f914879f97e7ee931ad05fe3afa629bd55270fc6cf1c1e589b6a99fab96d15daad0fa1a52a00c729ec0078045fe3e399bd4fd0c93bcc906957bdc17f89cb8e6 + languageName: node + linkType: hard + "retry@npm:^0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" @@ -11664,27 +13529,27 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.0-rc.17": - version: 1.0.0-rc.17 - resolution: "rolldown@npm:1.0.0-rc.17" - dependencies: - "@oxc-project/types": "npm:=0.127.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.17" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.17" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.17" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.17" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.17" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.17" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.17" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.17" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.17" - "@rolldown/pluginutils": "npm:1.0.0-rc.17" +"rolldown@npm:1.0.0-rc.18": + version: 1.0.0-rc.18 + resolution: "rolldown@npm:1.0.0-rc.18" + dependencies: + "@oxc-project/types": "npm:=0.128.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-rc.18" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.18" + "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.18" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.18" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.18" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.18" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.18" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.18" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.18" + "@rolldown/pluginutils": "npm:1.0.0-rc.18" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -11718,7 +13583,7 @@ __metadata: optional: true bin: rolldown: bin/cli.mjs - checksum: 10/5e7415a7cb732c4f7168ab6dcc841ed9ec4ad614058294a53d94821a762c274a69b009e41e9c8e4983a059907f02d462030a36b42543c0f41ce702fcd68d10d5 + checksum: 10/43a3cea928039291de3b2ba8c64424a22c17bd403b8f260cf1a4b3f3e6bb2ccdcf895f133e768479753ab37544a28a7f062a93f2eebb90217f0cdf0054a5bdc3 languageName: node linkType: hard @@ -12192,7 +14057,7 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.3": +"socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" dependencies: @@ -12239,7 +14104,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff @@ -12306,6 +14171,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.10.0": + version: 3.10.0 + resolution: "std-env@npm:3.10.0" + checksum: 10/19c9cda4f370b1ffae2b8b08c72167d8c3e5cfa972aaf5c6873f85d0ed2faa729407f5abb194dc33380708c00315002febb6f1e1b484736bfcf9361ad366013a + languageName: node + linkType: hard + "std-env@npm:^4.0.0-rc.1": version: 4.0.0 resolution: "std-env@npm:4.0.0" @@ -12427,6 +14299,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.2.3": + version: 2.2.3 + resolution: "strnum@npm:2.2.3" + checksum: 10/fb70206301858c319f59ed34fecedf90ac3b821692c2accd403d9d4a3384223a09df8fd92b130bbd4e885b67b7790715c003405ce5f959d9cabbf07d41d62aa8 + languageName: node + linkType: hard + "strtok3@npm:^10.3.4": version: 10.3.4 resolution: "strtok3@npm:10.3.4" @@ -12863,7 +14742,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -12951,6 +14830,13 @@ __metadata: languageName: node linkType: hard +"typebox@npm:^1.1.24": + version: 1.1.37 + resolution: "typebox@npm:1.1.37" + checksum: 10/022f7e4dac815ebd612ec72e962964c46985effc09bc9cef0d7893020b0c4c98d85aaf5e908231245c677b8df6fb7443d2b413bf24dd306b150ab2d47e1d50bb + languageName: node + linkType: hard + "typescript@npm:^5.3.0, typescript@npm:^5.3.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" @@ -13267,6 +15153,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^14.0.0": + version: 14.0.0 + resolution: "uuid@npm:14.0.0" + bin: + uuid: dist-node/bin/uuid + checksum: 10/8ee9b98f9650e25555515f7a28d3c3ae9364e72f7bb19b9e08b681bc135338beba5509b2830f6ae1cfaba4d45401da0d16d4d109b977097bc3d6ba0c5583341b + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.3.0 resolution: "v8-to-istanbul@npm:9.3.0" @@ -13386,18 +15281,18 @@ __metadata: linkType: hard "vite@npm:^8.0.9": - version: 8.0.10 - resolution: "vite@npm:8.0.10" + version: 8.0.11 + resolution: "vite@npm:8.0.11" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.10" - rolldown: "npm:1.0.0-rc.17" + postcss: "npm:^8.5.14" + rolldown: "npm:1.0.0-rc.18" tinyglobby: "npm:^0.2.16" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.0 + "@vitejs/devtools": ^0.1.18 esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 @@ -13438,7 +15333,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/64c6fa4efa1a9ca3e1cacbcca16487b75ea25d62efbfb99c4e571b5f716296dc4f8af825eb624e273b11c3bee4e87daec35815fb6a56e01c843659c003ed2bcd + checksum: 10/355a0ec8dd206330f5f8b79623dc06815ded830ce0e0e7594d8de8bce1a33c6a18950c8d69e819731163244ce0a1cf06aee949ac11c814ff3080370ebb2ca146 languageName: node linkType: hard @@ -13755,6 +15650,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^20.2.2": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 10/0188f430a0f496551d09df6719a9132a3469e47fe2747208b1dd0ab2bb0c512a95d0b081628bbca5400fb20dbf2fabe63d22badb346cecadffdd948b049f3fcc + languageName: node + linkType: hard + "yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -13796,6 +15698,31 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^16.0.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: "npm:^7.0.2" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^20.2.2" + checksum: 10/807fa21211d2117135d557f95fcd3c3d390530cda2eca0c840f1d95f0f40209dcfeb5ec18c785a1f3425896e623e3b2681e8bb7b6600060eda1c3f4804e7957e + languageName: node + linkType: hard + +"yauzl@npm:^2.10.0": + version: 2.10.0 + resolution: "yauzl@npm:2.10.0" + dependencies: + buffer-crc32: "npm:~0.2.3" + fd-slicer: "npm:~1.1.0" + checksum: 10/1e4c311050dc0cf2ee3dbe8854fe0a6cde50e420b3e561a8d97042526b4cf7a0718d6c8d89e9e526a152f4a9cec55bcea9c3617264115f48bd6704cf12a04445 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" @@ -13803,6 +15730,13 @@ __metadata: languageName: node linkType: hard +"yoctocolors@npm:^2.1.2": + version: 2.1.2 + resolution: "yoctocolors@npm:2.1.2" + checksum: 10/6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 + languageName: node + linkType: hard + "yoga-layout@npm:~3.2.1": version: 3.2.1 resolution: "yoga-layout@npm:3.2.1" @@ -13810,6 +15744,15 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.24.6, zod-to-json-schema@npm:^3.25.0": + version: 3.25.2 + resolution: "zod-to-json-schema@npm:3.25.2" + peerDependencies: + zod: ^3.25.28 || ^4 + checksum: 10/7035328654113f1a0b8e4c2d34a06f918c93650ef8a50d4fb30ad8f22e47d5762c163af9c82494756b34776bae3c41c26cfc6945105b0eee7dceb528cc07e665 + languageName: node + linkType: hard + "zod-to-json-schema@npm:^3.25.1": version: 3.25.1 resolution: "zod-to-json-schema@npm:3.25.1" @@ -13833,6 +15776,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.25.0 || ^4.0.0": + version: 4.4.3 + resolution: "zod@npm:4.4.3" + checksum: 10/804b9a42aa8f35f2b3c5a8dff906291cb749115f83ee2afe3576d70b5b5c53c965365c7f4967690647a9c54af9838ff232a85ff9577a0a36c44b68bc6cdefe36 + languageName: node + linkType: hard + "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5"