diff --git a/.ai/zod-first-migration.md b/.ai/zod-first-migration.md deleted file mode 100644 index 74628d0b..00000000 --- a/.ai/zod-first-migration.md +++ /dev/null @@ -1,192 +0,0 @@ -# Zod-First Typing Migration Plan (Full Cutover) - -## Intent -Move to a single Zod-first typing system that keeps all current behavior, removes drift, and eliminates every legacy path (no PortDataType, no port.* helpers, no contract registry). Tools do not exist yet, so the core typing refactor must land before any tool work. Workflow JSON stays exactly the same. No backwards-compatibility shims are allowed. - -## Context From Our Conversation -- The current system is not "stronger" overall; it is more explicit but drifts and is less expressive than Zod. -- We must preserve all functionality and pass all tests. -- We want typed `meta` without global Zod module augmentation (use a typed helper). -- Avoid auto-registering vague or complex types as contracts; contracts must be explicit. -- Ports should remain shallow; nested fields stay inside a single port unless explicitly opted in. -- Vague types (`any`/`unknown`) must be explicitly acknowledged. -- This refactor must remove all legacy code paths; no zombies. - -## Non-Negotiable Invariants -- Workflow definition JSON remains unchanged (nodes/edges/params/mappings stay identical). -- Every test passes at the end (typecheck, lint, unit, integration, e2e as applicable). -- No legacy typing system code remains in the codebase. -- Shared port/connection logic is centralized in component-sdk and imported by backend/frontend. - -## Ralph Wiggum Loop (Process Rule) -- After each phase, update this file by checking off completed todo items and adding any new discovered tasks. -- After each phase, create a git commit (or multiple commits if appropriate for the size of the work). -- Before starting the next phase, validate that all remaining todos in the current phase are either completed or explicitly deferred with a reason. -- Add a short "Phase Notes" subsection when a phase completes, summarizing decisions and any follow-ups. - -## Definitions (New System) -- Component definition uses `inputs` and `outputs` Zod schemas only. -- Port metadata is attached via a typed helper, not global Zod augmentation. -- Ports, connection types, and tool schemas are derived from Zod. -- Contracts are explicit named Zod exports with `meta.schemaName`. - -### Example API Shape (Target) -```ts -import { z } from 'zod'; -import { defineComponent, withPortMeta } from '@shipsec/component-sdk'; - -const inputs = z.object({ - apiKey: withPortMeta(z.string(), { label: 'API Key', bindingType: 'credential', icon: 'Key' }), - target: withPortMeta(z.string(), { label: 'Target' }), -}); - -const outputs = z.object({ - result: withPortMeta(z.string(), { label: 'Result' }), -}); - -export default defineComponent({ - id: 'security.example', - label: 'Example', - category: 'security', - runner: { kind: 'inline' }, - inputs, - outputs, - ui: { icon: 'Shield', description: 'Example component', examples: [] }, - async execute(params, context) { /* ... */ }, -}); -``` - -## Phase 0: Baseline + Guardrails -Intent: Freeze expectations and set guardrails before changing behavior. -Todos: -- [x] Snapshot current tests and key workflows that must continue to pass (list critical suites and known workflows). -- [x] Add a temporary migration branch README note explaining the goal and process rules. -- [x] Define success criteria: "all tests pass" + "no PortDataType, no contract registry, no port.* usage remains". -- [x] Add a single-source "migration checklist" pointer to this file for agents. - -Must-pass test suites and workflows: -- `bun run test` -- `bun run typecheck` -- `bun run lint` -- `bun run test:e2e` -- Core workflow compile + execute (backend DSL compiler + worker runtime) -- Frontend workflow builder connection validation and editor inference - -Exit criteria: -- [x] The team agrees on invariants and the no-backcompat stance. -- [x] A list of must-pass tests and workflows is recorded here. - -### Phase Notes -- Added a repo-level README notice pointing to this migration checklist. -- Recorded baseline test suite commands and critical workflow behavior to preserve. - -## Phase 1: New Core Typing API in component-sdk -Intent: Build the Zod-first core API and shared helpers. -Todos: -- [ ] Introduce `PortMeta` and `withPortMeta(schema, meta)` helper (typed, no Zod module augmentation). -- [ ] Add `mergePortMeta` behavior so repeated `withPortMeta` merges instead of overwriting. -- [ ] Implement `extractPorts(zodSchema)` that derives `ComponentPortMetadata[]` from Zod. -- [ ] Implement `zodToConnectionType(schema)` and `canConnect()` in component-sdk. -- [ ] Define explicit handling for Zod types: optional, default, nullable, effects, union, enum, record, array, object, void. -- [ ] Implement JSON schema generation directly from Zod (`getToolSchema`) even if tools are not shipped yet. -- [ ] Add a schema validation pipeline that enforces: - - [ ] Required `meta.label` where needed (default label = field name if absent). - - [ ] `z.any` / `z.unknown` only with explicit `meta.allowAny` and optional `meta.reason`. - - [ ] Max depth for port-visible fields (default 1 level); require explicit opt-in for deeper. - - [ ] `meta.schemaName` required for named contracts; no implicit contracts. - - [ ] Union or complex types must have explicit `meta.connectionType` or `meta.editor` override. -- [ ] Update component registry to compute/cache derived ports and connection types. -- [ ] Add unit tests for extraction/connection/validation rules in component-sdk. - -Exit criteria: -- [ ] SDK exposes a stable API for ports/connection types derived from Zod. -- [ ] Validation pipeline blocks ambiguous schemas without explicit meta. - -## Phase 2: Backend Migration (DSL + Runtime) -Intent: Use Zod-derived ports for validation and runtime input handling. -Todos: -- [ ] Replace backend `backend/src/dsl/port-utils.ts` with component-sdk `zodToConnectionType` and `canConnect`. -- [ ] Update DSL validation to use derived ports from Zod (no `metadata.inputs`). -- [ ] Remove `coerceValueForPort` usage in runtime input resolution; use Zod coercion in schemas instead. -- [ ] Update placeholder generation logic to use Zod-derived types (for validation placeholders). -- [ ] Ensure workflow JSON format remains unchanged (validate compile/execute flow). -- [ ] Add or update tests in `backend/src/dsl/__tests__` to reflect Zod-derived port behavior. - -Exit criteria: -- [ ] DSL validation and runtime no longer depend on PortDataType or port.* helpers. - -## Phase 3: Frontend Migration (Ports + UI) -Intent: Align frontend connection validation and editor inference to Zod-derived metadata. -Todos: -- [ ] Remove frontend port utils (`frontend/src/utils/portUtils.ts` and related compatibility logic). -- [ ] Import shared helpers from component-sdk for compatibility and type checks. -- [ ] Update `frontend/src/utils/connectionValidation.ts` to use `zodToConnectionType` and derived ports. -- [ ] Update editor inference to rely on Zod + `meta.editor` overrides, including secrets and enums. -- [ ] Verify dynamic ports derived from `resolvePorts` schemas render correctly. -- [ ] Add frontend tests or manual verification notes to confirm connection validation behavior. - -Exit criteria: -- [ ] UI shows the same ports as before, but derived from Zod. -- [ ] Connection validation is consistent with backend logic. - -## Phase 4: Contracts Migration -Intent: Replace contract registry with explicit schema exports. -Todos: -- [ ] Create `packages/contracts` with explicit Zod schema exports. -- [ ] Each contract must include `meta.schemaName` and optional `meta.isCredential`. -- [ ] Replace `registerContract/getContract` usage with direct schema imports. -- [ ] Remove contract registry implementation (`packages/component-sdk/src/contracts.ts`). -- [ ] Update any tests that expected registry behavior. - -Exit criteria: -- [ ] No contract registry exists; all contracts are explicit Zod exports. - -## Phase 5: Component Cutover (All Components) -Intent: Migrate every component to the new Zod-first definition. -Todos: -- [ ] Replace `inputSchema/outputSchema + metadata.inputs/outputs` with `inputs/outputs` Zod-only on every component. -- [ ] Replace all `port.*` usage with Zod types + `withPortMeta`. -- [ ] Update all `resolvePorts` to return Zod schemas (SDK merges with base schema). -- [ ] Ensure `outputs` is safe when not an object (void/record/any) and does not crash extraction. -- [ ] Tighten `any/unknown` usages or add explicit `meta.allowAny` with justification. -- [ ] Migrate components in waves to reduce risk: - - [ ] Core components (entry-point, workflow-call, console-log, file-writer, http-request). - - [ ] Manual actions (approval, selection, form). - - [ ] Security components (subfinder, dnsx, nuclei, trufflehog, etc). - - [ ] AI components (agent, providers). - - [ ] Notification, IT automation, GitHub components. -- [ ] Update component tests to the new API shape without changing assertions. - -Exit criteria: -- [ ] No component uses `metadata.inputs/outputs` or `port.*`. - -## Phase 6: Remove Legacy Code -Intent: Delete every leftover legacy type system artifact. -Todos: -- [ ] Delete `PortDataType` and `port.*` helpers. -- [ ] Delete old JSON schema mapping and old tool helpers. -- [ ] Remove any compatibility shims or dual-path logic. -- [ ] Delete redundant frontend/backend port logic. -- [ ] Remove unused tests that targeted the legacy API. -- [ ] Audit the repo for legacy symbols with `rg` and confirm zero matches. - -Exit criteria: -- [ ] `rg -n \"PortDataType|port\\.\"` returns no matches. -- [ ] `rg -n \"registerContract|getContract\"` returns no matches. - -## Phase 7: Validation + Final Pass -Intent: Prove the migration is complete and stable. -Todos: -- [ ] Run full test suite and typecheck. -- [ ] Fix regressions until all tests pass. -- [ ] Validate that workflow JSON inputs and outputs are identical to pre-migration behavior. -- [ ] Confirm no legacy symbols remain. - -Exit criteria: -- [ ] All tests pass, and all completion checks are done. - -## Completion Checklist -- [ ] All phases completed with updated todo checks. -- [ ] Tests pass. -- [ ] No legacy typing system code remains. -- [ ] Workflow JSON remains unchanged. diff --git a/backend/package.json b/backend/package.json index e93ade6b..38e9fb2f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,62 +17,63 @@ "delete:runs": "bun scripts/delete-all-workflow-runs.ts" }, "dependencies": { - "@clerk/backend": "^2.9.4", - "@clerk/types": "^4.81.0", - "@grpc/grpc-js": "^1.14.0", - "@nestjs/common": "^10.4.0", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.0", - "@nestjs/platform-express": "^10.4.0", - "@nestjs/swagger": "^11.2.0", + "@clerk/backend": "^2.29.5", + "@clerk/types": "^4.101.13", + "@grpc/grpc-js": "^1.14.3", + "@nestjs/common": "^10.4.22", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.22", + "@nestjs/platform-express": "^10.4.22", + "@nestjs/swagger": "^11.2.5", "@shipsec/component-sdk": "workspace:*", "@shipsec/shared": "workspace:*", "@shipsec/studio-worker": "workspace:*", - "@temporalio/client": "^1.11.3", - "@temporalio/worker": "^1.11.3", - "@temporalio/workflow": "^1.11.3", - "@types/express": "^5.0.3", + "@temporalio/client": "^1.14.1", + "@temporalio/worker": "^1.14.1", + "@temporalio/workflow": "^1.14.1", + "@types/express": "^5.0.6", "@types/minio": "^7.1.1", - "ai": "^6.0.0-beta.68", + "ai": "^6.0.49", "bcryptjs": "^3.0.3", "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", + "class-validator": "^0.14.3", "date-fns": "^4.1.0", "dotenv": "^17.2.3", - "drizzle-orm": "^0.44.6", - "ioredis": "^5.4.1", + "drizzle-orm": "^0.44.7", + "express": "^5.2.1", + "ioredis": "^5.9.2", "kafkajs": "^2.2.4", - "long": "^5.2.4", + "long": "^5.3.2", "minio": "^8.0.6", "multer": "^2.0.2", - "nestjs-zod": "^5.0.1", - "pg": "^8.16.3", - "posthog-node": "^5.17.2", + "nestjs-zod": "^5.1.1", + "pg": "^8.17.2", + "posthog-node": "^5.24.2", "reflect-metadata": "^0.2.2", "swagger-ui-express": "^5.0.1", - "zod": "^4.1.12" + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.39.2", - "@nestjs/testing": "^10.4.0", + "@nestjs/testing": "^10.4.22", "@types/bcryptjs": "^3.0.0", - "@types/express-serve-static-core": "^4.19.6", + "@types/express-serve-static-core": "^4.19.8", "@types/har-format": "^1.2.16", "@types/multer": "^2.0.0", - "@types/node": "^20.16.11", - "@types/pg": "^8.15.5", + "@types/node": "^20.19.30", + "@types/pg": "^8.16.0", "@types/supertest": "^2.0.16", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", - "bun-types": "^1.2.23", - "drizzle-kit": "^0.31.5", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "bun-types": "^1.3.6", + "drizzle-kit": "^0.31.8", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.0.0", - "prettier": "^3.8.0", - "supertest": "^7.1.4", - "typescript": "^5.6.3", - "typescript-eslint": "^8.53.0" + "globals": "^17.1.0", + "prettier": "^3.8.1", + "supertest": "^7.2.2", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1" } } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index e08627cc..b893b6d0 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -18,6 +18,7 @@ import { TestingSupportModule } from './testing/testing.module'; import { IntegrationsModule } from './integrations/integrations.module'; import { SchedulesModule } from './schedules/schedules.module'; import { AnalyticsModule } from './analytics/analytics.module'; +import { McpModule } from './mcp/mcp.module'; import { ApiKeysModule } from './api-keys/api-keys.module'; import { WebhooksModule } from './webhooks/webhooks.module'; @@ -37,6 +38,7 @@ const coreModules = [ ApiKeysModule, WebhooksModule, HumanInputsModule, + McpModule, ]; const testingModules = process.env.NODE_ENV === 'production' ? [] : [TestingSupportModule]; diff --git a/backend/src/components/components.controller.ts b/backend/src/components/components.controller.ts index aedc7412..1122b4c0 100644 --- a/backend/src/components/components.controller.ts +++ b/backend/src/components/components.controller.ts @@ -6,6 +6,7 @@ import '@shipsec/studio-worker/components'; import { componentRegistry, extractPorts, + getToolSchema, type CachedComponentMetadata, } from '@shipsec/component-sdk'; import { categorizeComponent, getCategoryConfig } from './utils/categorization'; @@ -45,6 +46,8 @@ function serializeComponent(entry: CachedComponentMetadata) { outputs: entry.outputs ?? [], parameters: entry.parameters ?? [], examples: metadata.examples ?? [], + agentTool: metadata.agentTool ?? null, + toolSchema: metadata.agentTool?.enabled ? getToolSchema(component) : null, }; } @@ -221,6 +224,15 @@ export class ComponentsController { type: 'array', items: { type: 'string' }, }, + agentTool: { + type: 'object', + nullable: true, + properties: { + enabled: { type: 'boolean' }, + toolName: { type: 'string', nullable: true }, + toolDescription: { type: 'string', nullable: true }, + }, + }, }, }, }, @@ -334,9 +346,17 @@ export class ComponentsController { }, parameters: { type: 'array' }, examples: { type: 'array' }, - isLatest: { type: 'boolean', nullable: true }, deprecated: { type: 'boolean', nullable: true }, example: { type: 'string', nullable: true }, + agentTool: { + type: 'object', + nullable: true, + properties: { + enabled: { type: 'boolean' }, + toolName: { type: 'string', nullable: true }, + toolDescription: { type: 'string', nullable: true }, + }, + }, }, }, }) diff --git a/backend/src/components/utils/categorization.ts b/backend/src/components/utils/categorization.ts index a2f8d11b..f3081054 100644 --- a/backend/src/components/utils/categorization.ts +++ b/backend/src/components/utils/categorization.ts @@ -12,6 +12,7 @@ const SUPPORTED_CATEGORIES: readonly ComponentCategory[] = [ 'input', 'transform', 'ai', + 'mcp', 'security', 'it_ops', 'notification', @@ -41,6 +42,13 @@ const COMPONENT_CATEGORY_CONFIG: Record { kind: 'success', }); }); + + it('populates connectedToolNodeIds for nodes with incoming edges to the tools port', () => { + const graph: WorkflowGraphDto = { + name: 'Agent Tool workflow', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { + label: 'Start', + config: { params: { runtimeInputs: [] }, inputOverrides: {} }, + }, + }, + { + id: 'tool1', + type: 'core.http.request', + position: { x: 0, y: 100 }, + data: { + label: 'Tool 1', + config: { + mode: 'tool', + params: {}, + inputOverrides: { url: 'https://tool1.com' }, + }, + }, + }, + { + id: 'tool2', + type: 'core.http.request', + position: { x: 100, y: 100 }, + data: { + label: 'Tool 2', + config: { + mode: 'tool', + params: {}, + inputOverrides: { url: 'https://tool2.com' }, + }, + }, + }, + { + id: 'agent', + type: 'core.ai.agent', + position: { x: 50, y: 200 }, + data: { + label: 'Agent', + config: { + params: {}, + inputOverrides: { userInput: 'Hello' }, + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'tool1' }, + { id: 'e2', source: 'start', target: 'tool2' }, + { + id: 't1', + source: 'tool1', + target: 'agent', + sourceHandle: 'tools', + targetHandle: 'tools', + }, + { + id: 't2', + source: 'tool2', + target: 'agent', + sourceHandle: 'tools', + targetHandle: 'tools', + }, + ], + viewport: { x: 0, y: 0, zoom: 1 }, + }; + + const definition = compileWorkflowGraph(graph); + + expect(definition.nodes.agent.connectedToolNodeIds).toHaveLength(2); + expect(definition.nodes.agent.connectedToolNodeIds).toContain('tool1'); + expect(definition.nodes.agent.connectedToolNodeIds).toContain('tool2'); + expect(definition.nodes.tool1.mode).toBe('tool'); + expect(definition.nodes.tool2.mode).toBe('tool'); + }); }); diff --git a/backend/src/dsl/compiler.ts b/backend/src/dsl/compiler.ts index 18369925..3251f3ab 100644 --- a/backend/src/dsl/compiler.ts +++ b/backend/src/dsl/compiler.ts @@ -1,7 +1,7 @@ import { WorkflowGraphDto, WorkflowNodeDto } from '../workflows/dto/workflow-graph.dto'; // Ensure all worker components are registered before accessing the registry import '../../../worker/src/components'; -import { componentRegistry } from '@shipsec/component-sdk'; +import { componentRegistry, type ComponentPortMetadata } from '@shipsec/component-sdk'; import { extractPorts } from '@shipsec/component-sdk/zod-ports'; import { WorkflowAction, @@ -101,8 +101,17 @@ export function compileWorkflowGraph(graph: WorkflowGraphDto): WorkflowDefinitio const groupIdValue = config.groupId; const maxConcurrencyValue = config.maxConcurrency; + const mode = (config.mode as WorkflowNodeMetadata['mode']) ?? 'normal'; + const toolConfig = config.toolConfig as WorkflowNodeMetadata['toolConfig']; + + const connectedToolNodeIds = edgesByTarget + .get(node.id) + ?.filter((edge) => edge.targetHandle === 'tools') + .map((edge) => edge.source); + nodesMetadata[node.id] = { ref: node.id, + mode, label: node.data?.label, joinStrategy, streamId: @@ -113,6 +122,9 @@ export function compileWorkflowGraph(graph: WorkflowGraphDto): WorkflowDefinitio typeof maxConcurrencyValue === 'number' && Number.isFinite(maxConcurrencyValue) ? maxConcurrencyValue : undefined, + toolConfig, + connectedToolNodeIds: + connectedToolNodeIds && connectedToolNodeIds.length > 0 ? connectedToolNodeIds : undefined, }; } @@ -148,7 +160,8 @@ export function compileWorkflowGraph(graph: WorkflowGraphDto): WorkflowDefinitio const params: Record = { ...rawParams }; const inputOverrides: Record = { ...rawInputOverrides }; - let inputs = componentRegistry.getMetadata(node.type)?.inputs ?? []; + let inputs: ComponentPortMetadata[] = + (componentRegistry.getMetadata(node.type)?.inputs as ComponentPortMetadata[]) ?? []; if (component?.resolvePorts) { try { const resolved = component.resolvePorts(params); diff --git a/backend/src/dsl/types.ts b/backend/src/dsl/types.ts index 6fbf235a..9592a04b 100644 --- a/backend/src/dsl/types.ts +++ b/backend/src/dsl/types.ts @@ -55,6 +55,14 @@ export const WorkflowNodeMetadataSchema = z.object({ maxConcurrency: z.number().int().positive().optional(), groupId: z.string().optional(), streamId: z.string().optional(), + mode: z.enum(['normal', 'tool']).default('normal'), + toolConfig: z + .object({ + boundInputIds: z.array(z.string()).default([]), + exposedInputIds: z.array(z.string()).default([]), + }) + .optional(), + connectedToolNodeIds: z.array(z.string()).optional(), }); export type WorkflowNodeMetadata = z.infer; diff --git a/backend/src/dsl/validator.ts b/backend/src/dsl/validator.ts index 3f888ead..6ec112a9 100644 --- a/backend/src/dsl/validator.ts +++ b/backend/src/dsl/validator.ts @@ -303,7 +303,7 @@ function validateInputMappings( } for (const [portId, count] of portsSeen.entries()) { - if (count > 1) { + if (count > 1 && portId !== 'tools') { const inputMetadata = actionPorts.get(action.ref)?.inputs.find((i) => i.id === portId); const portLabel = inputMetadata?.label || portId; @@ -395,14 +395,19 @@ function validateEdgeCompatibility( const targetPort = targetPorts.inputs.find((port) => port.id === targetHandle); if (!sourcePort) { - errors.push({ - node: targetAction.ref, - field: 'inputMappings', - message: `Source port '${sourceHandle}' not found on ${edge.sourceRef}`, - severity: 'error', - suggestion: 'Confirm the source component exposes this output port', - }); - continue; + const sourceNodeMetadata = compiledDefinition.nodes[sourceAction.ref]; + if (sourceHandle === 'tools' && sourceNodeMetadata?.mode === 'tool') { + // Allow explicit tool connection bypass + } else { + errors.push({ + node: targetAction.ref, + field: 'inputMappings', + message: `Source port '${sourceHandle}' not found on ${edge.sourceRef}`, + severity: 'error', + suggestion: 'Confirm the source component exposes this output port', + }); + continue; + } } if (!targetPort) { @@ -416,7 +421,7 @@ function validateEdgeCompatibility( continue; } - const sourceType = getPortConnectionType(sourcePort); + const sourceType = sourcePort ? getPortConnectionType(sourcePort) : { kind: 'any' as const }; const targetType = getPortConnectionType(targetPort); if (!sourceType || !targetType) { diff --git a/backend/src/mcp/__tests__/mcp-internal.integration.spec.ts b/backend/src/mcp/__tests__/mcp-internal.integration.spec.ts new file mode 100644 index 00000000..cc3ca04d --- /dev/null +++ b/backend/src/mcp/__tests__/mcp-internal.integration.spec.ts @@ -0,0 +1,174 @@ +import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ForbiddenException, INestApplication } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ConfigModule } from '@nestjs/config'; +import request from 'supertest'; +import { AuthService } from '../../auth/auth.service'; +import { AuthGuard } from '../../auth/auth.guard'; +import { ApiKeysService } from '../../api-keys/api-keys.service'; +import { AnalyticsService } from '../../analytics/analytics.service'; +import { AgentTraceIngestService } from '../../agent-trace/agent-trace-ingest.service'; +import { EventIngestService } from '../../events/event-ingest.service'; +import { LogIngestService } from '../../logging/log-ingest.service'; +import { NodeIOIngestService } from '../../node-io/node-io-ingest.service'; +import { SecretsEncryptionService } from '../../secrets/secrets.encryption'; +import { InternalMcpController } from '../internal-mcp.controller'; +import { ToolRegistryService, TOOL_REGISTRY_REDIS } from '../tool-registry.service'; +import { Pool } from 'pg'; + +// Simple Mock Redis +class MockRedis { + data = new Map>(); + kv = new Map(); + async hset(key: string, field: string, value: string) { + if (!this.data.has(key)) this.data.set(key, new Map()); + this.data.get(key)!.set(field, value); + return 1; + } + async hget(key: string, field: string) { + return this.data.get(key)?.get(field) || null; + } + async expire() { + return 1; + } + async get(key: string) { + return this.kv.get(key) ?? null; + } + async set(key: string, value: string) { + this.kv.set(key, value); + return 'OK'; + } + async del(key: string) { + return this.kv.delete(key) ? 1 : 0; + } + async quit() {} +} + +describe('MCP Internal API (Integration)', () => { + let app: INestApplication; + let redis: MockRedis; + const INTERNAL_TOKEN = 'test-internal-token'; + + beforeAll(async () => { + process.env.INTERNAL_SERVICE_TOKEN = INTERNAL_TOKEN; + process.env.NODE_ENV = 'test'; + process.env.SKIP_INGEST_SERVICES = 'true'; + process.env.SHIPSEC_SKIP_MIGRATION_CHECK = 'true'; + + const { McpModule } = await import('../mcp.module'); + const mockRedis = new MockRedis(); + const encryption = new SecretsEncryptionService(); + const toolRegistryService = new ToolRegistryService(mockRedis as unknown as any, encryption); + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot({ isGlobal: true, ignoreEnvFile: true }), McpModule], + }) + .overrideProvider(NodeIOIngestService) + .useValue({ + onModuleInit: async () => {}, + onModuleDestroy: async () => {}, + }) + .overrideProvider(LogIngestService) + .useValue({ + onModuleInit: async () => {}, + onModuleDestroy: async () => {}, + }) + .overrideProvider(EventIngestService) + .useValue({ + onModuleInit: async () => {}, + onModuleDestroy: async () => {}, + }) + .overrideProvider(AgentTraceIngestService) + .useValue({ + onModuleInit: async () => {}, + onModuleDestroy: async () => {}, + }) + .overrideProvider(ToolRegistryService) + .useValue(toolRegistryService) + .overrideProvider(ApiKeysService) + .useValue({ + validateKey: async () => null, + }) + .overrideProvider(AnalyticsService) + .useValue({ + isEnabled: () => false, + track: () => {}, + trackWorkflowStarted: () => {}, + trackWorkflowCompleted: () => {}, + trackApiCall: () => {}, + trackComponentExecuted: () => {}, + }) + .overrideProvider(AuthService) + .useValue({ + authenticate: async () => { + throw new ForbiddenException('Unauthorized'); + }, + providerName: 'local', + }) + .overrideProvider(Pool) + .useValue({ + connect: async () => ({ + query: async () => ({ rows: [] }), + release: () => {}, + }), + on: () => {}, + }) + .overrideProvider(TOOL_REGISTRY_REDIS) + .useValue(mockRedis) + .compile(); + + app = moduleFixture.createNestApplication(); + const authService = moduleFixture.get(AuthService); + const apiKeysService = moduleFixture.get(ApiKeysService); + const reflector = moduleFixture.get(Reflector); + app.useGlobalGuards(new AuthGuard(authService, apiKeysService, reflector)); + await app.init(); + + redis = moduleFixture.get(TOOL_REGISTRY_REDIS); + const controller = moduleFixture.get(InternalMcpController); + (controller as unknown as { toolRegistry: ToolRegistryService }).toolRegistry = + toolRegistryService; + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + it('registers a component tool via internal API', async () => { + const payload = { + runId: 'run-test-1', + nodeId: 'node-test-1', + toolName: 'test_tool', + componentId: 'core.test', + description: 'Test Tool', + inputSchema: { type: 'object', properties: {} }, + credentials: { apiKey: 'secret' }, + }; + + const response = await request(app.getHttpServer()) + .post('/internal/mcp/register-component') + .set('x-internal-token', INTERNAL_TOKEN) + .send(payload); + + expect(response.status).toBe(201); + expect(response.body).toEqual({ success: true }); + + // Verify it's in Redis + const toolJson = await redis.hget('mcp:run:run-test-1:tools', 'node-test-1'); + expect(toolJson).not.toBeNull(); + const tool = JSON.parse(toolJson!); + expect(tool.toolName).toBe('test_tool'); + expect(tool.status).toBe('ready'); + }); + + it('rejects identity-less internal requests', async () => { + const response = await request(app.getHttpServer()) + .post('/internal/mcp/register-component') + .send({}); + + // Should be caught by global AuthGuard + expect(response.status).toBe(403); + }); +}); diff --git a/backend/src/mcp/__tests__/tool-registry.service.spec.ts b/backend/src/mcp/__tests__/tool-registry.service.spec.ts new file mode 100644 index 00000000..5dd541dd --- /dev/null +++ b/backend/src/mcp/__tests__/tool-registry.service.spec.ts @@ -0,0 +1,243 @@ +import { describe, it, expect, beforeEach } from 'bun:test'; +import { ToolRegistryService } from '../tool-registry.service'; +import type { SecretsEncryptionService } from '../../secrets/secrets.encryption'; + +// Mock Redis +class MockRedis { + private data = new Map>(); + + async hset(key: string, field: string, value: string): Promise { + if (!this.data.has(key)) { + this.data.set(key, new Map()); + } + this.data.get(key)!.set(field, value); + return 1; + } + + async hget(key: string, field: string): Promise { + return this.data.get(key)?.get(field) ?? null; + } + + async hgetall(key: string): Promise> { + const hash = this.data.get(key); + if (!hash) return {}; + return Object.fromEntries(hash.entries()); + } + + async del(key: string): Promise { + this.data.delete(key); + return 1; + } + + async expire(_key: string, _seconds: number): Promise { + return 1; + } + + async quit(): Promise {} +} + +// Mock encryption service +class MockEncryptionService { + async encrypt(value: string): Promise<{ ciphertext: string; keyId: string }> { + return { + ciphertext: Buffer.from(value).toString('base64'), + keyId: 'test-key', + }; + } + + async decrypt(material: { ciphertext: string }): Promise { + return Buffer.from(material.ciphertext, 'base64').toString('utf-8'); + } +} + +describe('ToolRegistryService', () => { + let service: ToolRegistryService; + let redis: MockRedis; + let encryption: MockEncryptionService; + + beforeEach(() => { + redis = new MockRedis(); + encryption = new MockEncryptionService(); + service = new ToolRegistryService(redis as any, encryption as any as SecretsEncryptionService); + }); + + describe('registerComponentTool', () => { + it('registers a component tool with encrypted credentials', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'check_ip_reputation', + componentId: 'security.abuseipdb', + description: 'Check IP reputation', + inputSchema: { + type: 'object', + properties: { ipAddress: { type: 'string' } }, + required: ['ipAddress'], + }, + credentials: { apiKey: 'secret-123' }, + }); + + const tool = await service.getTool('run-1', 'node-a'); + expect(tool).not.toBeNull(); + expect(tool?.toolName).toBe('check_ip_reputation'); + expect(tool?.status).toBe('ready'); + expect(tool?.type).toBe('component'); + expect(tool?.encryptedCredentials).toBeDefined(); + }); + }); + + describe('getToolsForRun', () => { + it('returns all tools for a run', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'tool_a', + componentId: 'comp.a', + description: 'Tool A', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-b', + toolName: 'tool_b', + componentId: 'comp.b', + description: 'Tool B', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + const tools = await service.getToolsForRun('run-1'); + expect(tools.length).toBe(2); + expect(tools.map((t) => t.toolName).sort()).toEqual(['tool_a', 'tool_b']); + }); + }); + + describe('getToolByName', () => { + it('finds a tool by name', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'my_tool', + componentId: 'comp.a', + description: 'My Tool', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + const tool = await service.getToolByName('run-1', 'my_tool'); + expect(tool).not.toBeNull(); + expect(tool?.nodeId).toBe('node-a'); + }); + + it('returns null for unknown tool name', async () => { + const tool = await service.getToolByName('run-1', 'unknown'); + expect(tool).toBeNull(); + }); + }); + + describe('getToolCredentials', () => { + it('decrypts and returns credentials', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'tool', + componentId: 'comp', + description: 'Tool', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: { apiKey: 'secret-value', token: 'another-secret' }, + }); + + const creds = await service.getToolCredentials('run-1', 'node-a'); + expect(creds).toEqual({ apiKey: 'secret-value', token: 'another-secret' }); + }); + + it('decrypts and returns remote MCP auth token as credentials object', async () => { + await service.registerRemoteMcp({ + runId: 'run-1', + nodeId: 'node-remote', + toolName: 'remote_tool', + description: 'Remote Tool', + inputSchema: { type: 'object', properties: {}, required: [] }, + endpoint: 'http://example.com', + authToken: 'my-plain-token', + }); + + const creds = await service.getToolCredentials('run-1', 'node-remote'); + expect(creds).toEqual({ authToken: 'my-plain-token' }); + }); + }); + + describe('areAllToolsReady', () => { + it('returns true when all required tools are ready', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'tool_a', + componentId: 'comp.a', + description: 'Tool A', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-b', + toolName: 'tool_b', + componentId: 'comp.b', + description: 'Tool B', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + const ready = await service.areAllToolsReady('run-1', ['node-a', 'node-b']); + expect(ready).toBe(true); + }); + + it('returns false when a required tool is missing', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'tool_a', + componentId: 'comp.a', + description: 'Tool A', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + const ready = await service.areAllToolsReady('run-1', ['node-a', 'node-b']); + expect(ready).toBe(false); + }); + }); + + describe('cleanupRun', () => { + it('removes all tools and returns container IDs', async () => { + await service.registerComponentTool({ + runId: 'run-1', + nodeId: 'node-a', + toolName: 'tool_a', + componentId: 'comp.a', + description: 'Tool A', + inputSchema: { type: 'object', properties: {}, required: [] }, + credentials: {}, + }); + + await service.registerLocalMcp({ + runId: 'run-1', + nodeId: 'node-mcp', + toolName: 'steampipe', + description: 'Steampipe MCP', + inputSchema: { type: 'object', properties: {}, required: [] }, + endpoint: 'http://localhost:8080', + containerId: 'container-123', + }); + + const containerIds = await service.cleanupRun('run-1'); + expect(containerIds).toEqual(['container-123']); + + const tools = await service.getToolsForRun('run-1'); + expect(tools.length).toBe(0); + }); + }); +}); diff --git a/backend/src/mcp/dto/mcp-gateway.dto.ts b/backend/src/mcp/dto/mcp-gateway.dto.ts new file mode 100644 index 00000000..6ac81a11 --- /dev/null +++ b/backend/src/mcp/dto/mcp-gateway.dto.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { createZodDto } from 'nestjs-zod'; + +export const ListToolsQuerySchema = z.object({ + runId: z.string().describe('The workflow run ID to list tools for'), +}); + +export class ListToolsQueryDto extends createZodDto(ListToolsQuerySchema) {} + +export const CallToolRequestSchema = z.object({ + runId: z.string().describe('The workflow run ID'), + name: z.string().describe('The tool name to call'), + arguments: z.record(z.string(), z.unknown()).describe('The arguments for the tool call'), +}); + +export class CallToolRequestDto extends createZodDto(CallToolRequestSchema) {} + +export interface McpToolResponse { + tools: { + name: string; + description: string; + inputSchema: Record; + }[]; +} + +export interface McpCallToolResponse { + content: { + type: 'text' | 'image' | 'resource'; + text?: string; + [key: string]: unknown; + }[]; + isError?: boolean; +} diff --git a/backend/src/mcp/dto/mcp.dto.ts b/backend/src/mcp/dto/mcp.dto.ts new file mode 100644 index 00000000..e2bc7093 --- /dev/null +++ b/backend/src/mcp/dto/mcp.dto.ts @@ -0,0 +1,41 @@ +import { ToolInputSchema } from '@shipsec/component-sdk'; + +/** + * Input for registering a component tool + */ +export class RegisterComponentToolInput { + runId!: string; + nodeId!: string; + toolName!: string; + componentId!: string; + description!: string; + inputSchema!: ToolInputSchema; + credentials!: Record; + parameters?: Record; +} + +/** + * Input for registering a remote MCP + */ +export class RegisterRemoteMcpInput { + runId!: string; + nodeId!: string; + toolName!: string; + description!: string; + inputSchema!: ToolInputSchema; + endpoint!: string; + authToken?: string; +} + +/** + * Input for registering a local MCP (stdio container) + */ +export class RegisterLocalMcpInput { + runId!: string; + nodeId!: string; + toolName!: string; + description!: string; + inputSchema!: ToolInputSchema; + endpoint!: string; + containerId!: string; +} diff --git a/backend/src/mcp/index.ts b/backend/src/mcp/index.ts new file mode 100644 index 00000000..69079647 --- /dev/null +++ b/backend/src/mcp/index.ts @@ -0,0 +1,5 @@ +export * from './mcp.module'; +export * from './tool-registry.service'; +export * from './mcp-gateway.service'; +export * from './mcp-gateway.controller'; +export * from './dto/mcp-gateway.dto'; diff --git a/backend/src/mcp/internal-mcp.controller.ts b/backend/src/mcp/internal-mcp.controller.ts new file mode 100644 index 00000000..14953827 --- /dev/null +++ b/backend/src/mcp/internal-mcp.controller.ts @@ -0,0 +1,64 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { ToolRegistryService } from './tool-registry.service'; +import { McpGatewayService } from './mcp-gateway.service'; +import { McpAuthService } from './mcp-auth.service'; +import { + RegisterComponentToolInput, + RegisterLocalMcpInput, + RegisterRemoteMcpInput, +} from './dto/mcp.dto'; + +@Controller('internal/mcp') +export class InternalMcpController { + constructor( + private readonly toolRegistry: ToolRegistryService, + private readonly mcpAuthService: McpAuthService, + private readonly mcpGatewayService: McpGatewayService, + ) {} + + @Post('generate-token') + async generateToken( + @Body() + body: { + runId: string; + organizationId?: string | null; + agentId?: string; + allowedNodeIds?: string[]; + }, + ) { + const token = await this.mcpAuthService.generateSessionToken( + body.runId, + body.organizationId ?? null, + body.agentId, + body.allowedNodeIds, + ); + return { token }; + } + + @Post('register-component') + async registerComponent(@Body() body: RegisterComponentToolInput) { + await this.toolRegistry.registerComponentTool(body); + await this.mcpGatewayService.refreshServersForRun(body.runId); + return { success: true }; + } + + @Post('register-remote') + async registerRemote(@Body() body: RegisterRemoteMcpInput) { + await this.toolRegistry.registerRemoteMcp(body); + await this.mcpGatewayService.refreshServersForRun(body.runId); + return { success: true }; + } + + @Post('register-local') + async registerLocal(@Body() body: RegisterLocalMcpInput) { + await this.toolRegistry.registerLocalMcp(body); + await this.mcpGatewayService.refreshServersForRun(body.runId); + return { success: true }; + } + + @Post('cleanup') + async cleanupRun(@Body() body: { runId: string }) { + const containerIds = await this.toolRegistry.cleanupRun(body.runId); + return { containerIds }; + } +} diff --git a/backend/src/mcp/mcp-auth.guard.ts b/backend/src/mcp/mcp-auth.guard.ts new file mode 100644 index 00000000..01bf90c1 --- /dev/null +++ b/backend/src/mcp/mcp-auth.guard.ts @@ -0,0 +1,58 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Logger, +} from '@nestjs/common'; +import type { Request } from 'express'; +import { McpAuthService } from './mcp-auth.service'; +import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; + +/** + * Request interface for MCP Gateway which uses spec-compliant AuthInfo + */ +export interface McpGatewayRequest extends Request { + auth?: AuthInfo; +} + +@Injectable() +export class McpAuthGuard implements CanActivate { + private readonly logger = new Logger(McpAuthGuard.name); + + constructor(private readonly mcpAuthService: McpAuthService) {} + + async canActivate(context: ExecutionContext): Promise { + const http = context.switchToHttp(); + const request = http.getRequest(); + + let token: string | undefined; + + // Try Authorization header (Standard Bearer token) + const authHeader = request.headers['authorization']; + if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + this.logger.log(`Found Bearer token in Authorization header for ${request.url}`); + } + + if (!token) { + this.logger.warn(`Missing or invalid Authorization header for MCP request to ${request.url}`); + throw new UnauthorizedException('Bearer token required in Authorization header'); + } + + const authInfo = await this.mcpAuthService.validateToken(token); + if (authInfo) { + this.logger.log(`Successfully validated MCP session token for run: ${authInfo.extra?.runId}`); + } + + if (!authInfo) { + this.logger.warn('Invalid or expired MCP session token'); + throw new UnauthorizedException('Invalid or expired session token'); + } + + // Attach spec-compliant auth info to request + request.auth = authInfo; + + return true; + } +} diff --git a/backend/src/mcp/mcp-auth.service.ts b/backend/src/mcp/mcp-auth.service.ts new file mode 100644 index 00000000..5c5ba2d1 --- /dev/null +++ b/backend/src/mcp/mcp-auth.service.ts @@ -0,0 +1,89 @@ +import { Injectable, Inject, Logger } from '@nestjs/common'; +import Redis from 'ioredis'; +import { uuid4 } from '@temporalio/workflow'; +import { TOOL_REGISTRY_REDIS } from './tool-registry.service'; +import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; + +export interface McpSessionMetadata { + runId: string; + organizationId: string | null; + agentId?: string; + allowedNodeIds?: string[]; + expiresAt: number; +} + +@Injectable() +export class McpAuthService { + private readonly logger = new Logger(McpAuthService.name); + private readonly TOKEN_PREFIX = 'mcp:session:'; + + constructor(@Inject(TOOL_REGISTRY_REDIS) private readonly redis: Redis) {} + + /** + * Generate a secure, short-lived session token for an MCP agent + */ + async generateSessionToken( + runId: string, + organizationId: string | null, + agentId = 'agent', + allowedNodeIds?: string[], + ttlSeconds = 3600, // 1 hour default + ): Promise { + const token = `mcp_sk_${uuid4().replace(/-/g, '')}`; + const expiresAt = Math.floor(Date.now() / 1000) + ttlSeconds; + + const metadata: McpSessionMetadata = { + runId, + organizationId, + agentId, + allowedNodeIds, + expiresAt, + }; + + await this.redis.set( + `${this.TOKEN_PREFIX}${token}`, + JSON.stringify(metadata), + 'EX', + ttlSeconds, + ); + + return token; + } + + /** + * Validate a session token and return the corresponding AuthInfo + */ + async validateToken(token: string): Promise { + const data = await this.redis.get(`${this.TOKEN_PREFIX}${token}`); + if (!data) { + return null; + } + + try { + const metadata: McpSessionMetadata = JSON.parse(data); + + // Map to MCP Spec AuthInfo + return { + token, + clientId: metadata.agentId || 'unknown-agent', + scopes: ['tools:list', 'tools:call', 'resources:list'], + expiresAt: metadata.expiresAt, + extra: { + runId: metadata.runId, + organizationId: metadata.organizationId, + allowedNodeIds: metadata.allowedNodeIds, + }, + }; + } catch (err) { + this.logger.error(`Failed to parse MCP session metadata: ${err}`); + return null; + } + } + + /** + * Revoke a specific session token + */ + async revokeToken(token: string): Promise { + await this.redis.del(`${this.TOKEN_PREFIX}${token}`); + } +} diff --git a/backend/src/mcp/mcp-gateway.controller.ts b/backend/src/mcp/mcp-gateway.controller.ts new file mode 100644 index 00000000..fbd4ddeb --- /dev/null +++ b/backend/src/mcp/mcp-gateway.controller.ts @@ -0,0 +1,118 @@ +import { Controller, All, UseGuards, Req, Res, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import type { Response } from 'express'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; + +import { Public } from '../auth/public.decorator'; +import { McpAuthGuard, type McpGatewayRequest } from './mcp-auth.guard'; +import { McpGatewayService } from './mcp-gateway.service'; + +@ApiTags('mcp') +@Controller('mcp') +@Public() +@UseGuards(McpAuthGuard) +export class McpGatewayController { + private readonly logger = new Logger(McpGatewayController.name); + + // Mapping of runId to its current Streamable HTTP transport + private readonly transports = new Map(); + + constructor(private readonly mcpGateway: McpGatewayService) {} + + @All('gateway') + @ApiOperation({ summary: 'Unified MCP Gateway endpoint (Streamable HTTP)' }) + async handleGateway(@Req() req: McpGatewayRequest, @Res() res: Response) { + const auth = req.auth; + if (!auth || !auth.extra) { + return res.status(401).send('Authentication missing'); + } + + const runId = auth.extra.runId as string; + const organizationId = auth.extra.organizationId as string | null; + const allowedNodeIds = auth.extra.allowedNodeIds as string[] | undefined; + + if (!runId) { + return res.status(400).send('runId missing in session token'); + } + + // Cache key includes allowedNodeIds to support multiple agents with different tool scopes + const cacheKey = + allowedNodeIds && allowedNodeIds.length > 0 + ? `${runId}:${allowedNodeIds.sort().join(',')}` + : runId; + + let transport = this.transports.get(cacheKey); + const body = req.body as unknown; + const isPost = req.method === 'POST'; + const isGet = req.method === 'GET'; + const isDelete = req.method === 'DELETE'; + const isInitRequest = + isPost && + (isInitializeRequest(body) || + (Array.isArray(body) && body.some((item) => isInitializeRequest(item)))); + + // Initialization if transport doesn't exist + if (!transport) { + if (!isInitRequest) { + return res.status(400).send('Bad Request: No valid session ID provided'); + } + + this.logger.log( + `Initializing new MCP transport for run: ${runId} with allowedNodeIds: ${allowedNodeIds?.join(',') ?? 'none'}`, + ); + + const allowedToolsHeader = req.headers['x-allowed-tools']; + const allowedTools = + typeof allowedToolsHeader === 'string' + ? allowedToolsHeader.split(',').map((t) => t.trim()) + : undefined; + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => cacheKey, + enableJsonResponse: true, + }); + this.transports.set(cacheKey, transport); + + try { + const server = await this.mcpGateway.getServerForRun( + runId, + organizationId, + allowedTools, + allowedNodeIds, + ); + await server.connect(transport); + } catch (error) { + this.logger.error(`Failed to initialize MCP server for run ${runId}: ${error}`); + this.transports.delete(cacheKey); + return res + .status(error instanceof Error && error.name === 'NotFoundException' ? 404 : 403) + .send(error instanceof Error ? error.message : 'Access denied'); + } + } + + if ((isGet || isDelete) && !transport.sessionId) { + return res.status(400).send('Bad Request: Server not initialized'); + } + + if (isGet) { + // Cleanup on client disconnect (specifically for the SSE stream) + res.on('close', async () => { + this.logger.log( + `MCP SSE connection closed for run: ${runId} with allowedNodeIds: ${allowedNodeIds?.join(',') ?? 'none'}`, + ); + // We don't necessarily want to delete the transport here if POSTs are still allowed, + // but for ShipSec run-bounded sessions, closing SSE usually means the agent is done. + this.transports.delete(cacheKey); + await this.mcpGateway.cleanupRun(runId); + }); + + // Handle the initial GET request to start the SSE stream + // We don't await this because for SSE, it blocks until the connection is closed. + void transport.handleRequest(req, res); + } else { + // Handle POST (Messages) or DELETE (Session termination) + await transport.handleRequest(req, res, req.body); + } + } +} diff --git a/backend/src/mcp/mcp-gateway.service.ts b/backend/src/mcp/mcp-gateway.service.ts new file mode 100644 index 00000000..c841217a --- /dev/null +++ b/backend/src/mcp/mcp-gateway.service.ts @@ -0,0 +1,565 @@ +import { + Injectable, + Logger, + BadRequestException, + ForbiddenException, + NotFoundException, +} from '@nestjs/common'; +import { + componentRegistry, + getActionInputIds, + getExposedParameterIds, + getToolInputShape, +} from '@shipsec/component-sdk'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; + +import { ToolRegistryService, RegisteredTool } from './tool-registry.service'; +import { TemporalService } from '../temporal/temporal.service'; +import { WorkflowRunRepository } from '../workflows/repository/workflow-run.repository'; +import { TraceRepository } from '../trace/trace.repository'; +import type { TraceEventType } from '../trace/types'; + +@Injectable() +export class McpGatewayService { + private readonly logger = new Logger(McpGatewayService.name); + + // Cache of servers per runId + private readonly servers = new Map(); + private readonly registeredToolNames = new Map>(); + + constructor( + private readonly toolRegistry: ToolRegistryService, + private readonly temporalService: TemporalService, + private readonly workflowRunRepository: WorkflowRunRepository, + private readonly traceRepository: TraceRepository, + ) {} + + /** + * Get or create an MCP Server instance for a specific workflow run + * Key includes both runId and allowedNodeIds to support multiple agents with different tool scopes + */ + async getServerForRun( + runId: string, + organizationId?: string | null, + allowedTools?: string[], + allowedNodeIds?: string[], + ): Promise { + // 1. Validate Access + await this.validateRunAccess(runId, organizationId); + + // Cache key includes allowedNodeIds so different agents with different tool scopes get different servers + const cacheKey = + allowedNodeIds && allowedNodeIds.length > 0 + ? `${runId}:${allowedNodeIds.sort().join(',')}` + : runId; + + const existing = this.servers.get(cacheKey); + if (existing) { + return existing; + } + + const server = new McpServer({ + name: 'shipsec-studio-gateway', + version: '1.0.0', + }); + + const toolSet = new Set(); + this.registeredToolNames.set(cacheKey, toolSet); + await this.registerTools(server, runId, allowedTools, allowedNodeIds, toolSet); + this.servers.set(cacheKey, server); + + return server; + } + + /** + * Refresh tool registrations for any cached servers for a run. + * This is used when tools register after an MCP session has already initialized. + */ + async refreshServersForRun(runId: string): Promise { + const matchingEntries = Array.from(this.servers.entries()).filter( + ([key]) => key === runId || key.startsWith(`${runId}:`), + ); + + if (matchingEntries.length === 0) { + return; + } + + this.logger.log( + `Refreshing MCP servers for run ${runId} (${matchingEntries.length} instance(s))`, + ); + + await Promise.all( + matchingEntries.map(async ([cacheKey, server]) => { + const allowedNodeIds = + cacheKey === runId ? undefined : cacheKey.split(':').slice(1).join(':').split(','); + const toolSet = this.registeredToolNames.get(cacheKey) ?? new Set(); + this.registeredToolNames.set(cacheKey, toolSet); + await this.registerTools(server, runId, undefined, allowedNodeIds, toolSet); + }), + ); + } + + private async validateRunAccess(runId: string, organizationId?: string | null) { + const run = await this.workflowRunRepository.findByRunId(runId); + if (!run) { + throw new NotFoundException(`Workflow run ${runId} not found`); + } + + if (organizationId && run.organizationId !== organizationId) { + throw new ForbiddenException(`You do not have access to workflow run ${runId}`); + } + } + + private async logToolCall( + runId: string, + toolName: string, + status: 'STARTED' | 'COMPLETED' | 'FAILED', + nodeRef: string, + details: { duration?: number; error?: any; output?: any } = {}, + ) { + try { + const lastSeq = await this.traceRepository.getLastSequence(runId); + const sequence = lastSeq + 1; + + const type: TraceEventType = 'NODE_PROGRESS'; + // Map status to approximate node events for visualization, + // though 'NODE_PROGRESS' is safer if we don't want to mess up graph state. + // But ticket asks for logging. + // 'NODE_PROGRESS' with message is good. + + await this.traceRepository.append({ + runId, + type, + nodeRef, + timestamp: new Date().toISOString(), + sequence, + level: status === 'FAILED' ? 'error' : 'info', + message: `Tool ${status}: ${toolName}`, + error: details.error, + outputSummary: details.output, + data: details.duration ? { duration: details.duration, toolName } : { toolName }, + }); + } catch (err) { + this.logger.error(`Failed to log tool call: ${err}`); + } + } + + /** + * Register all available tools (internal and external) for this run + */ + private async registerTools( + server: McpServer, + runId: string, + allowedTools?: string[], + allowedNodeIds?: string[], + registeredToolNames?: Set, + ) { + this.logger.log( + `Registering tools for run ${runId} (allowedNodeIds=${allowedNodeIds?.join(',') ?? 'none'}, allowedTools=${allowedTools?.join(',') ?? 'none'})`, + ); + const allRegistered = await this.toolRegistry.getToolsForRun(runId, allowedNodeIds); + this.logger.log( + `Tool registry returned ${allRegistered.length} tool(s) for run ${runId}: ${allRegistered.map((t) => `${t.toolName}:${t.type}`).join(', ') || 'none'}`, + ); + + // Filter by allowed tools if specified + if (allowedTools && allowedTools.length > 0) { + // Note: For external tools, we need to check the proxied name, so we can't filter sources yet. + // We filter individual tools below. + // For component tools, we can filter here. + // But let's simplify and just filter inside the loops. + } + + // 1. Register Internal Tools + const internalTools = allRegistered.filter((t) => t.type === 'component'); + this.logger.log(`Registering ${internalTools.length} internal tool(s) for run ${runId}`); + for (const tool of internalTools) { + if (allowedTools && allowedTools.length > 0 && !allowedTools.includes(tool.toolName)) { + this.logger.log(`Skipping internal tool ${tool.toolName} (not in allowedTools)`); + continue; + } + + if (registeredToolNames?.has(tool.toolName)) { + this.logger.log(`Skipping internal tool ${tool.toolName} (already registered)`); + continue; + } + + this.logger.log(`Registering internal tool ${tool.toolName} (node=${tool.nodeId})`); + const component = tool.componentId ? componentRegistry.get(tool.componentId) : null; + const inputShape = component ? getToolInputShape(component) : undefined; + + server.registerTool( + tool.toolName, + { + description: tool.description, + inputSchema: inputShape, + _meta: { inputSchema: tool.inputSchema }, + }, + async (args: any) => { + const startTime = Date.now(); + await this.logToolCall(runId, tool.toolName, 'STARTED', tool.nodeId); + + try { + const result = await this.callComponentTool(tool, runId, args ?? {}); + + await this.logToolCall(runId, tool.toolName, 'COMPLETED', tool.nodeId, { + duration: Date.now() - startTime, + output: result, + }); + + // Signal Temporal that the tool call is completed + await this.temporalService.signalWorkflow({ + workflowId: runId, + signalName: 'toolCallCompleted', + args: { + nodeRef: tool.nodeId, + toolName: tool.toolName, + output: result, + status: 'completed', + }, + }); + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + await this.logToolCall(runId, tool.toolName, 'FAILED', tool.nodeId, { + duration: Date.now() - startTime, + error: errorMessage, + }); + + // Signal Temporal that the tool call failed + await this.temporalService.signalWorkflow({ + workflowId: runId, + signalName: 'toolCallCompleted', + args: { + nodeRef: tool.nodeId, + toolName: tool.toolName, + output: null, + status: 'failed', + errorMessage, + }, + }); + + return { + content: [ + { + type: 'text' as const, + text: `Error: ${errorMessage}`, + }, + ], + isError: true, + }; + } + }, + ); + registeredToolNames?.add(tool.toolName); + } + + // 2. Register External Tools (Proxied) + const externalSources = allRegistered.filter((t) => t.type !== 'component'); + this.logger.log( + `Registering ${externalSources.length} external MCP source(s) for run ${runId}`, + ); + for (const source of externalSources) { + try { + this.logger.log( + `Fetching tools from external source ${source.toolName} (type=${source.type}, endpoint=${source.endpoint ?? 'missing'})`, + ); + const tools = await this.fetchExternalTools(source); + const prefix = source.toolName; + + this.logger.log(`External source ${source.toolName} returned ${tools.length} tool(s)`); + for (const t of tools) { + const proxiedName = `${prefix}__${t.name}`; + + if (allowedTools && allowedTools.length > 0 && !allowedTools.includes(proxiedName)) { + this.logger.log(`Skipping proxied tool ${proxiedName} (not in allowedTools)`); + continue; + } + + if (registeredToolNames?.has(proxiedName)) { + this.logger.log(`Skipping proxied tool ${proxiedName} (already registered)`); + continue; + } + + this.logger.log(`Registering proxied tool ${proxiedName} (source=${source.toolName})`); + server.registerTool( + proxiedName, + { + description: t.description, + _meta: { inputSchema: t.inputSchema }, + }, + async (args: any) => { + const startTime = Date.now(); + const nodeRef = `mcp:${proxiedName}`; + await this.logToolCall(runId, proxiedName, 'STARTED', nodeRef); + + try { + const result = await this.proxyCallToExternal(source, t.name, args); + + await this.logToolCall(runId, proxiedName, 'COMPLETED', nodeRef, { + duration: Date.now() - startTime, + output: result, + }); + return result; + } catch (err) { + await this.logToolCall(runId, proxiedName, 'FAILED', nodeRef, { + duration: Date.now() - startTime, + error: err, + }); + throw err; + } + }, + ); + registeredToolNames?.add(proxiedName); + } + } catch (error) { + this.logger.error(`Failed to fetch tools from external source ${source.toolName}:`, error); + } + } + } + + /** + * Fetches tools from an external MCP source + */ + private async fetchExternalTools(source: RegisteredTool): Promise { + if (!source.endpoint) { + this.logger.warn(`Missing endpoint for external source ${source.toolName}`); + return []; + } + + const MAX_RETRIES = 5; + const RETRY_DELAY_MS = 1000; + let lastError: unknown; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) { + const sessionId = `stdio-proxy-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const transport = new StreamableHTTPClientTransport(new URL(source.endpoint), { + requestInit: { + headers: { + 'Mcp-Session-Id': sessionId, + }, + }, + }); + const client = new Client( + { name: 'shipsec-gateway-client', version: '1.0.0' }, + { capabilities: {} }, + ); + + this.logger.log( + `Connecting to external MCP source ${source.toolName} at ${source.endpoint} (attempt ${attempt}/${MAX_RETRIES})`, + ); + + try { + await client.connect(transport); + const response = await client.listTools(); + this.logger.log( + `listTools from ${source.toolName} returned ${response.tools?.length ?? 0} tool(s)`, + ); + return response.tools; + } catch (error) { + lastError = error; + this.logger.warn( + `listTools failed for ${source.toolName} (attempt ${attempt}/${MAX_RETRIES}): ${error instanceof Error ? error.message : String(error)}`, + ); + if (attempt < MAX_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); + } + } finally { + this.logger.log(`Closing external MCP client for ${source.toolName}`); + await client.close(); + } + } + + if (lastError) { + throw lastError; + } + return []; + } + + /** + * Proxies a tool call to an external MCP source + */ + private async proxyCallToExternal( + source: RegisteredTool, + toolName: string, + args: any, + ): Promise { + if (!source.endpoint) { + throw new McpError( + ErrorCode.InternalError, + `Missing endpoint for external source ${source.toolName}`, + ); + } + + const MAX_RETRIES = 3; + const TIMEOUT_MS = 30000; + + let lastError: unknown; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + const sessionId = `stdio-proxy-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const transport = new StreamableHTTPClientTransport(new URL(source.endpoint), { + requestInit: { + headers: { + 'Mcp-Session-Id': sessionId, + }, + }, + }); + const client = new Client( + { name: 'shipsec-gateway-client', version: '1.0.0' }, + { capabilities: {} }, + ); + + try { + await client.connect(transport); + + const result = await Promise.race([ + client.callTool({ + name: toolName, + arguments: args, + }), + new Promise((_, reject) => + setTimeout( + () => reject(new Error(`Tool call timed out after ${TIMEOUT_MS}ms`)), + TIMEOUT_MS, + ), + ), + ]); + + return result; + } catch (error) { + lastError = error; + this.logger.warn(`External tool call attempt ${attempt} failed: ${error}`); + if (attempt < MAX_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); + } + } finally { + await client.close().catch(() => {}); + } + } + + throw lastError; + } + + /** + * Internal handler for executing component-based tools via Temporal workflow + */ + private async callComponentTool( + tool: RegisteredTool, + runId: string, + args: Record, + ): Promise { + if (!tool.componentId) { + throw new BadRequestException(`Component ID missing for tool '${tool.toolName}'`); + } + + const component = componentRegistry.get(tool.componentId); + const actionInputIds = component ? new Set(getActionInputIds(component)) : new Set(); + const exposedParamIds = component ? getExposedParameterIds(component) : []; + const exposedParamSet = new Set(exposedParamIds); + + const inputArgs: Record = {}; + const paramOverrides: Record = {}; + for (const [key, value] of Object.entries(args ?? {})) { + if (exposedParamSet.has(key) && !actionInputIds.has(key)) { + paramOverrides[key] = value; + } else { + inputArgs[key] = value; + } + } + + // Resolve credentials from registry + const credentials = await this.toolRegistry.getToolCredentials(runId, tool.nodeId); + + const mergedParams = { ...(tool.parameters ?? {}), ...paramOverrides }; + + // Generate a unique call ID for this tool invocation + const callId = `${runId}:${tool.nodeId}:${Date.now()}`; + + this.logger.log( + `Signaling tool execution: callId=${callId}, tool='${tool.toolName}' (${tool.componentId})`, + ); + + // Signal the workflow to execute the tool + await this.temporalService.signalWorkflow({ + workflowId: runId, + signalName: 'executeToolCall', + args: { + callId, + nodeId: tool.nodeId, + componentId: tool.componentId, + arguments: inputArgs, + parameters: mergedParams, + credentials: credentials ?? undefined, + requestedAt: new Date().toISOString(), + }, + }); + + // Poll for the result via workflow query + // The workflow will execute the component and store the result + const result = await this.pollForToolCallResult(runId, callId); + + if (!result.success) { + throw new Error(result.error ?? 'Tool execution failed'); + } + + return result.output; + } + + /** + * Poll the workflow for a tool call result + */ + private async pollForToolCallResult( + runId: string, + callId: string, + timeoutMs = 60000, + pollIntervalMs = 500, + ): Promise<{ success: boolean; output?: unknown; error?: string }> { + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + try { + // Query the workflow for tool call results + const result = await this.temporalService.queryWorkflow({ + workflowId: runId, + queryType: 'getToolCallResult', + args: [callId], + }); + + if (result) { + return result as { success: boolean; output?: unknown; error?: string }; + } + } catch (error) { + // Query might fail if workflow is busy, continue polling + this.logger.debug(`Polling for tool result: ${error}`); + } + + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + + return { success: false, error: `Tool call timed out after ${timeoutMs}ms` }; + } + + /** + * Cleanup server instance for a run + */ + async cleanupRun(runId: string) { + const server = this.servers.get(runId); + if (server) { + await server.close(); + this.servers.delete(runId); + } + } +} diff --git a/backend/src/mcp/mcp.module.ts b/backend/src/mcp/mcp.module.ts new file mode 100644 index 00000000..d92053f7 --- /dev/null +++ b/backend/src/mcp/mcp.module.ts @@ -0,0 +1,39 @@ +import { Global, Module } from '@nestjs/common'; +import Redis from 'ioredis'; +import { ToolRegistryService, TOOL_REGISTRY_REDIS } from './tool-registry.service'; +import { McpGatewayService } from './mcp-gateway.service'; +import { McpAuthService } from './mcp-auth.service'; +import { McpGatewayController } from './mcp-gateway.controller'; +import { SecretsModule } from '../secrets/secrets.module'; +import { InternalMcpController } from './internal-mcp.controller'; +import { WorkflowsModule } from '../workflows/workflows.module'; +import { ApiKeysModule } from '../api-keys/api-keys.module'; + +@Global() +@Module({ + imports: [SecretsModule, WorkflowsModule, ApiKeysModule], + controllers: [McpGatewayController, InternalMcpController], + providers: [ + { + provide: TOOL_REGISTRY_REDIS, + useFactory: () => { + // Use the same Redis URL as terminal or a dedicated one + const url = process.env.TOOL_REGISTRY_REDIS_URL ?? process.env.TERMINAL_REDIS_URL; + if (!url) { + console.warn('[MCP] Redis URL not set; tool registry disabled'); + } else { + console.info(`[MCP] Tool registry Redis URL: ${url}`); + } + if (!url) { + return null; + } + return new Redis(url); + }, + }, + ToolRegistryService, + McpAuthService, + McpGatewayService, + ], + exports: [ToolRegistryService, McpGatewayService, McpAuthService], +}) +export class McpModule {} diff --git a/backend/src/mcp/tool-registry.service.ts b/backend/src/mcp/tool-registry.service.ts new file mode 100644 index 00000000..e1805354 --- /dev/null +++ b/backend/src/mcp/tool-registry.service.ts @@ -0,0 +1,362 @@ +/** + * Tool Registry Service + * + * Redis-backed registry for storing tool metadata and credentials during workflow runs. + * This bridges the gap between Temporal workflows (where credentials are resolved) + * and the MCP gateway (where agents call tools). + * + * Redis key pattern: mcp:run:{runId}:tools (Hash) + * TTL: 1 hour (configurable) + */ + +import { Injectable, Logger, Inject, OnModuleDestroy } from '@nestjs/common'; +import type Redis from 'ioredis'; +import { type ToolInputSchema } from '@shipsec/component-sdk'; +import { SecretsEncryptionService } from '../secrets/secrets.encryption'; +import { + RegisterComponentToolInput, + RegisterLocalMcpInput, + RegisterRemoteMcpInput, +} from './dto/mcp.dto'; + +export const TOOL_REGISTRY_REDIS = Symbol('TOOL_REGISTRY_REDIS'); + +/** + * Types of tools that can be registered + */ +export type RegisteredToolType = 'component' | 'remote-mcp' | 'local-mcp'; + +/** + * Status of a registered tool + */ +export type ToolStatus = 'pending' | 'ready' | 'error'; + +/** + * A tool registered in the registry + */ +export interface RegisteredTool { + /** Unique ID of the workflow node */ + nodeId: string; + + /** Tool name exposed to the agent */ + toolName: string; + + /** Type of tool */ + type: RegisteredToolType; + + /** Current status */ + status: ToolStatus; + + /** Component ID (for component tools) */ + componentId?: string; + + /** Additional parameters for the component */ + parameters?: Record; + + /** JSON Schema for action inputs */ + inputSchema: ToolInputSchema; + + /** Tool description for the agent */ + description: string; + + /** Encrypted credentials (for component tools) */ + encryptedCredentials?: string; + + /** MCP endpoint URL (for remote/local MCPs) */ + endpoint?: string; + + /** Docker container ID (for local MCPs) */ + containerId?: string; + + /** Error message if status is 'error' */ + errorMessage?: string; + + /** Timestamp when tool was registered */ + registeredAt: string; +} + +const REGISTRY_TTL_SECONDS = 60 * 60; // 1 hour + +@Injectable() +export class ToolRegistryService implements OnModuleDestroy { + private readonly logger = new Logger(ToolRegistryService.name); + + constructor( + @Inject(TOOL_REGISTRY_REDIS) private readonly redis: Redis | null, + private readonly encryption: SecretsEncryptionService, + ) {} + + async onModuleDestroy() { + await this.redis?.quit(); + } + + private getRegistryKey(runId: string): string { + return `mcp:run:${runId}:tools`; + } + + /** + * Register a ShipSec component as an agent-callable tool + */ + async registerComponentTool(input: RegisterComponentToolInput): Promise { + if (!this.redis) { + this.logger.warn('Redis not configured, tool registry disabled'); + return; + } + + const { + runId, + nodeId, + toolName, + componentId, + description, + inputSchema, + credentials, + parameters, + } = input; + + // Encrypt credentials + const credentialsJson = JSON.stringify(credentials); + const encryptionMaterial = await this.encryption.encrypt(credentialsJson); + const encryptedCredentials = JSON.stringify(encryptionMaterial); + + const tool: RegisteredTool = { + nodeId, + toolName, + type: 'component', + status: 'ready', + componentId, + parameters, + description, + inputSchema, + encryptedCredentials, + registeredAt: new Date().toISOString(), + }; + + const key = this.getRegistryKey(runId); + await this.redis.hset(key, nodeId, JSON.stringify(tool)); + await this.redis.expire(key, REGISTRY_TTL_SECONDS); + + this.logger.log(`Registered component tool: ${toolName} (node: ${nodeId}, run: ${runId})`); + } + + /** + * Register a remote HTTP MCP server + */ + async registerRemoteMcp(input: RegisterRemoteMcpInput): Promise { + if (!this.redis) { + this.logger.warn('Redis not configured, tool registry disabled'); + return; + } + + const { runId, nodeId, toolName, description, inputSchema, endpoint, authToken } = input; + + // Encrypt auth token if provided - store as JSON object for consistency + let encryptedCredentials: string | undefined; + if (authToken) { + const credentials = { authToken }; + const encryptionMaterial = await this.encryption.encrypt(JSON.stringify(credentials)); + encryptedCredentials = JSON.stringify(encryptionMaterial); + } + + const tool: RegisteredTool = { + nodeId, + toolName, + type: 'remote-mcp', + status: 'ready', + description, + inputSchema, + endpoint, + encryptedCredentials, + registeredAt: new Date().toISOString(), + }; + + const key = this.getRegistryKey(runId); + await this.redis.hset(key, nodeId, JSON.stringify(tool)); + await this.redis.expire(key, REGISTRY_TTL_SECONDS); + + this.logger.log(`Registered remote MCP: ${toolName} (node: ${nodeId}, run: ${runId})`); + } + + /** + * Register a local stdio MCP running in Docker + */ + async registerLocalMcp(input: RegisterLocalMcpInput): Promise { + if (!this.redis) { + this.logger.warn('Redis not configured, tool registry disabled'); + return; + } + + const { runId, nodeId, toolName, description, inputSchema, endpoint, containerId } = input; + + const tool: RegisteredTool = { + nodeId, + toolName, + type: 'local-mcp', + status: 'ready', + description, + inputSchema, + endpoint, + containerId, + registeredAt: new Date().toISOString(), + }; + + const key = this.getRegistryKey(runId); + await this.redis.hset(key, nodeId, JSON.stringify(tool)); + await this.redis.expire(key, REGISTRY_TTL_SECONDS); + + this.logger.log( + `Registered local MCP: ${toolName} (node: ${nodeId}, container: ${containerId}, run: ${runId})`, + ); + } + + async getToolsForRun(runId: string, nodeIds?: string[]): Promise { + if (!this.redis) { + this.logger.warn('Redis not configured, tool registry disabled'); + return []; + } + + const key = this.getRegistryKey(runId); + const toolsHash = await this.redis.hgetall(key); + + let tools = Object.values(toolsHash).map((json) => JSON.parse(json) as RegisteredTool); + + this.logger.debug(`Found ${tools.length} tool(s) for run ${runId}`); + + if (nodeIds && nodeIds.length > 0) { + this.logger.debug(`Filtering tools by nodeIds: ${nodeIds.join(', ')}`); + tools = tools.filter((t) => nodeIds.includes(t.nodeId)); + this.logger.debug(`Filtered down to ${tools.length} tool(s)`); + } + + return tools; + } + + /** + * Get a specific tool by node ID + */ + async getTool(runId: string, nodeId: string): Promise { + if (!this.redis) { + return null; + } + + const key = this.getRegistryKey(runId); + const toolJson = await this.redis.hget(key, nodeId); + + if (!toolJson) { + return null; + } + + return JSON.parse(toolJson) as RegisteredTool; + } + + /** + * Get a tool by its tool name + */ + async getToolByName(runId: string, toolName: string): Promise { + const tools = await this.getToolsForRun(runId); + return tools.find((t) => t.toolName === toolName) ?? null; + } + + /** + * Decrypt and return credentials for a tool + */ + async getToolCredentials(runId: string, nodeId: string): Promise | null> { + const tool = await this.getTool(runId, nodeId); + if (!tool?.encryptedCredentials) { + return null; + } + + try { + const encryptionMaterial = JSON.parse(tool.encryptedCredentials); + const decrypted = await this.encryption.decrypt(encryptionMaterial); + try { + return JSON.parse(decrypted); + } catch (e) { + // Fallback for tools that might have stored raw strings (e.g. older remote-mcp implementations) + if (tool.type === 'remote-mcp') { + return { authToken: decrypted }; + } + throw e; + } + } catch (error) { + this.logger.error(`Failed to decrypt credentials for tool ${nodeId}:`, error); + return null; + } + } + + /** + * Check if all required tools are ready + */ + async areAllToolsReady(runId: string, requiredNodeIds: string[]): Promise { + if (!this.redis) { + return true; // If Redis is disabled, assume ready + } + + const key = this.getRegistryKey(runId); + + for (const nodeId of requiredNodeIds) { + const toolJson = await this.redis.hget(key, nodeId); + if (!toolJson) { + return false; + } + + const tool = JSON.parse(toolJson) as RegisteredTool; + if (tool.status !== 'ready') { + return false; + } + } + + return true; + } + + /** + * Update tool status (e.g., to 'error') + */ + async updateToolStatus( + runId: string, + nodeId: string, + status: ToolStatus, + errorMessage?: string, + ): Promise { + if (!this.redis) { + return; + } + + const tool = await this.getTool(runId, nodeId); + if (!tool) { + return; + } + + tool.status = status; + if (errorMessage) { + tool.errorMessage = errorMessage; + } + + const key = this.getRegistryKey(runId); + await this.redis.hset(key, nodeId, JSON.stringify(tool)); + } + + /** + * Clean up all tools for a run (called when workflow completes) + * Returns container IDs that need to be stopped + */ + async cleanupRun(runId: string): Promise { + if (!this.redis) { + return []; + } + + const tools = await this.getToolsForRun(runId); + const containerIds = tools + .filter((t) => t.type === 'local-mcp' && t.containerId) + .map((t) => t.containerId!); + + const key = this.getRegistryKey(runId); + await this.redis.del(key); + + this.logger.log( + `Cleaned up tool registry for run ${runId} (${tools.length} tools, ${containerIds.length} containers)`, + ); + + return containerIds; + } +} diff --git a/backend/src/secrets/secrets.module.ts b/backend/src/secrets/secrets.module.ts index 677ece28..f6bac70a 100644 --- a/backend/src/secrets/secrets.module.ts +++ b/backend/src/secrets/secrets.module.ts @@ -10,6 +10,6 @@ import { SecretsService } from './secrets.service'; imports: [DatabaseModule], controllers: [SecretsController], providers: [SecretsService, SecretsRepository, SecretsEncryptionService], - exports: [SecretsService], + exports: [SecretsService, SecretsEncryptionService], }) export class SecretsModule {} diff --git a/backend/src/temporal/temporal.service.ts b/backend/src/temporal/temporal.service.ts index b2a9909a..0f822a80 100644 --- a/backend/src/temporal/temporal.service.ts +++ b/backend/src/temporal/temporal.service.ts @@ -201,6 +201,19 @@ export class TemporalService implements OnModuleDestroy { await handle.signal(input.signalName, input.args); } + /** + * Query a running workflow for state + */ + async queryWorkflow(input: { + workflowId: string; + queryType: string; + args?: unknown[]; + }): Promise { + const handle = await this.getWorkflowHandle({ workflowId: input.workflowId }); + this.logger.debug(`Querying workflow ${input.workflowId} with query '${input.queryType}'`); + return handle.query(input.queryType, ...(input.args ?? [])); + } + private async getWorkflowHandle(ref: WorkflowRunReference): Promise> { const client = await this.getClient(); return client.getHandle(ref.workflowId, ref.runId); diff --git a/backend/src/trace/trace.repository.ts b/backend/src/trace/trace.repository.ts index 37fbbabb..c188b72d 100644 --- a/backend/src/trace/trace.repository.ts +++ b/backend/src/trace/trace.repository.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common'; -import { eq, and, gt } from 'drizzle-orm'; +import { eq, and, gt, desc } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { workflowTracesTable, type WorkflowTraceRecord } from '../database/schema'; @@ -167,6 +167,18 @@ export class TraceRepository implements OnModuleDestroy { }; } + async getLastSequence(runId: string, organizationId?: string | null): Promise { + const runFilter = this.buildRunFilter(runId, organizationId); + const [result] = await this.db + .select({ sequence: workflowTracesTable.sequence }) + .from(workflowTracesTable) + .where(runFilter) + .orderBy(desc(workflowTracesTable.sequence)) + .limit(1); + + return result?.sequence ?? 0; + } + private mapToInsert(event: PersistedTraceEvent) { return { runId: event.runId, diff --git a/backend/src/workflows/dto/workflow-graph.dto.ts b/backend/src/workflows/dto/workflow-graph.dto.ts index bc94f294..30120dda 100644 --- a/backend/src/workflows/dto/workflow-graph.dto.ts +++ b/backend/src/workflows/dto/workflow-graph.dto.ts @@ -18,6 +18,14 @@ export const WorkflowNodeDataSchema = z.object({ streamId: z.string().optional(), groupId: z.string().optional(), maxConcurrency: z.number().int().positive().optional(), + mode: z.enum(['normal', 'tool']).optional(), + toolConfig: z + .object({ + boundInputIds: z.array(z.string()).default([]), + exposedInputIds: z.array(z.string()).default([]), + }) + .optional(), + connectedToolNodeIds: z.array(z.string()).optional(), }) .default({ params: {}, inputOverrides: {} }), // Dynamic ports resolved from component's resolvePorts function diff --git a/backend/src/workflows/workflows.module.ts b/backend/src/workflows/workflows.module.ts index 5db7d342..01332d0e 100644 --- a/backend/src/workflows/workflows.module.ts +++ b/backend/src/workflows/workflows.module.ts @@ -38,6 +38,6 @@ import { WorkflowRoleGuard } from './workflow-role.guard'; TerminalArchiveService, WorkflowRoleGuard, ], - exports: [WorkflowsService], + exports: [WorkflowsService, WorkflowRepository, WorkflowRunRepository, WorkflowVersionRepository], }) export class WorkflowsModule {} diff --git a/bun.lock b/bun.lock index dc70ede1..e34fe06c 100644 --- a/bun.lock +++ b/bun.lock @@ -12,77 +12,80 @@ "long": "^5.3.2", }, "devDependencies": { - "@types/bun": "^1.3.5", - "@types/node": "^24.10.4", - "bun-types": "^1.3.5", + "@ai-sdk/mcp": "^1.0.13", + "@modelcontextprotocol/sdk": "^1.25.3", + "@types/bun": "^1.3.6", + "@types/node": "^24.10.9", + "bun-types": "^1.3.6", "openapi-typescript": "^7.10.1", "pm2": "^6.0.14", "tsx": "^4.21.0", "typescript": "^5.9.3", - "undici": "^7.18.2", + "undici": "^7.19.0", }, }, "backend": { "name": "@shipsec/studio-backend", "version": "0.1.0", "dependencies": { - "@clerk/backend": "^2.9.4", - "@clerk/types": "^4.81.0", - "@grpc/grpc-js": "^1.14.0", - "@nestjs/common": "^10.4.0", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.0", - "@nestjs/platform-express": "^10.4.0", - "@nestjs/swagger": "^11.2.0", + "@clerk/backend": "^2.29.5", + "@clerk/types": "^4.101.13", + "@grpc/grpc-js": "^1.14.3", + "@nestjs/common": "^10.4.22", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.22", + "@nestjs/platform-express": "^10.4.22", + "@nestjs/swagger": "^11.2.5", "@shipsec/component-sdk": "workspace:*", "@shipsec/shared": "workspace:*", "@shipsec/studio-worker": "workspace:*", - "@temporalio/client": "^1.11.3", - "@temporalio/worker": "^1.11.3", - "@temporalio/workflow": "^1.11.3", - "@types/express": "^5.0.3", + "@temporalio/client": "^1.14.1", + "@temporalio/worker": "^1.14.1", + "@temporalio/workflow": "^1.14.1", + "@types/express": "^5.0.6", "@types/minio": "^7.1.1", - "ai": "^6.0.0-beta.68", + "ai": "^6.0.49", "bcryptjs": "^3.0.3", "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", + "class-validator": "^0.14.3", "date-fns": "^4.1.0", "dotenv": "^17.2.3", - "drizzle-orm": "^0.44.6", - "ioredis": "^5.4.1", + "drizzle-orm": "^0.44.7", + "express": "^5.2.1", + "ioredis": "^5.9.2", "kafkajs": "^2.2.4", - "long": "^5.2.4", + "long": "^5.3.2", "minio": "^8.0.6", "multer": "^2.0.2", - "nestjs-zod": "^5.0.1", - "pg": "^8.16.3", - "posthog-node": "^5.17.2", + "nestjs-zod": "^5.1.1", + "pg": "^8.17.2", + "posthog-node": "^5.24.2", "reflect-metadata": "^0.2.2", "swagger-ui-express": "^5.0.1", - "zod": "^4.1.12", + "zod": "^4.3.6", }, "devDependencies": { "@eslint/js": "^9.39.2", - "@nestjs/testing": "^10.4.0", + "@nestjs/testing": "^10.4.22", "@types/bcryptjs": "^3.0.0", - "@types/express-serve-static-core": "^4.19.6", + "@types/express-serve-static-core": "^4.19.8", "@types/har-format": "^1.2.16", "@types/multer": "^2.0.0", - "@types/node": "^20.16.11", - "@types/pg": "^8.15.5", + "@types/node": "^20.19.30", + "@types/pg": "^8.16.0", "@types/supertest": "^2.0.16", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", - "bun-types": "^1.2.23", - "drizzle-kit": "^0.31.5", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "bun-types": "^1.3.6", + "drizzle-kit": "^0.31.8", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.0.0", - "prettier": "^3.8.0", - "supertest": "^7.1.4", - "typescript": "^5.6.3", - "typescript-eslint": "^8.53.0", + "globals": "^17.1.0", + "prettier": "^3.8.1", + "supertest": "^7.2.2", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1", }, }, "frontend": { @@ -186,6 +189,7 @@ "zod": "^4.1.12", }, "devDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2", "@types/har-format": "^1.2.16", "@types/node": "^20.16.11", "@typescript-eslint/eslint-plugin": "^8.53.0", @@ -233,57 +237,58 @@ "name": "@shipsec/studio-worker", "version": "0.1.0", "dependencies": { - "@ai-sdk/google": "^3.0.0-beta.27", - "@ai-sdk/openai": "^3.0.0-beta.34", - "@aws-sdk/client-s3": "^3.642.0", + "@ai-sdk/google": "^3.0.13", + "@ai-sdk/mcp": "^1.0.13", + "@ai-sdk/openai": "^3.0.18", + "@aws-sdk/client-s3": "^3.975.0", "@googleapis/admin": "^29.0.0", - "@grpc/grpc-js": "^1.14.0", + "@grpc/grpc-js": "^1.14.3", "@okta/okta-sdk-nodejs": "^7.3.0", "@shipsec/component-sdk": "*", "@shipsec/contracts": "*", "@shipsec/shared": "*", - "@temporalio/activity": "^1.13.1", - "@temporalio/client": "^1.11.3", - "@temporalio/common": "^1.13.1", - "@temporalio/worker": "^1.11.3", - "@temporalio/workflow": "^1.11.3", + "@temporalio/activity": "^1.14.1", + "@temporalio/client": "^1.14.1", + "@temporalio/common": "^1.14.1", + "@temporalio/worker": "^1.14.1", + "@temporalio/workflow": "^1.14.1", "adm-zip": "^0.5.16", - "ai": "^6.0.0-beta.68", + "ai": "^6.0.49", "dotenv": "^17.2.3", - "drizzle-orm": "^0.44.6", - "google-auth-library": "^10.4.2", - "ioredis": "^5.4.1", + "drizzle-orm": "^0.44.7", + "google-auth-library": "^10.5.0", + "ioredis": "^5.9.2", "js-yaml": "^4.1.1", "kafkajs": "^2.2.4", "long": "^5.3.2", "minio": "^8.0.6", "node-pty": "^1.1.0", - "pg": "^8.16.3", - "zod": "^4.1.12", + "pg": "^8.17.2", + "zod": "^4.3.6", }, "devDependencies": { "@eslint/js": "^9.39.2", "@types/adm-zip": "^0.5.7", "@types/js-yaml": "^4.0.9", "@types/minio": "^7.1.1", - "@types/node": "^20.16.11", - "@types/pg": "^8.15.5", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", - "bun-types": "^1.2.23", + "@types/node": "^25.1.0", + "@types/pg": "^8.16.0", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "bun-types": "^1.3.6", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.0.0", - "prettier": "^3.8.0", - "ts-loader": "^9.5.1", - "tsx": "^4.20.6", - "typescript": "^5.6.3", - "typescript-eslint": "^8.53.0", + "globals": "^17.1.0", + "prettier": "^3.8.1", + "ts-loader": "^9.5.4", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1", }, "optionalDependencies": { - "@swc/core": "^1.13.21", - "@swc/core-darwin-arm64": "^1.13.21", + "@swc/core": "^1.15.10", + "@swc/core-darwin-arm64": "^1.15.10", "@swc/core-darwin-universal": "^1.13.21", "@swc/core-wasm32-wasi": "^1.13.21", }, @@ -294,15 +299,17 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sRlPMKd38+fdp2y11USW44c0o8tsIsT6T/pgyY04VXC3URjIRnkxugxd9AkU2ogfpPDMz50cBAGPnMxj+6663Q=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "3.0.5", "@ai-sdk/provider-utils": "4.0.9", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA=="], + + "@ai-sdk/google": ["@ai-sdk/google@3.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.5", "@ai-sdk/provider-utils": "4.0.9" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HYCh8miS4FLxOIpjo/BmoFVMO5BuxNpHVVDQkoJotoH8ZSFftkJJGGayIxQT/Lwx9GGvVVCOQ+lCdBBAnkl1sA=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.6", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Nr7E+ouWd/bKO9SFlgLnJJ1+fiGHC07KAeFr08faT+lvkECWlxVox3aL0dec8uCgBDUghYbq7f4S5teUrCc+QQ=="], + "@ai-sdk/mcp": ["@ai-sdk/mcp@1.0.13", "", { "dependencies": { "@ai-sdk/provider": "3.0.5", "@ai-sdk/provider-utils": "4.0.9", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yQEa+X5/QNmWlNwURAMlobmipvg4i/3L0iTz7pJQ/Z2Imjgp/y8gRAxkIzXL1HzlOxF4Dm/4PHpHrXaSV+EAUQ=="], - "@ai-sdk/openai": ["@ai-sdk/openai@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CBoYn1U59Lop8yBL9KuVjHCKc/B06q9Qo0SasRwHoyMEq+X4I8LQZu3a8Ck1jwwcZTTxfyiExB70LtIRSynBDA=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "3.0.5", "@ai-sdk/provider-utils": "4.0.9" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uYscTyoaWij9FoPpKRNK8YgtDEuPpQlqREYylJCA8o5YQVQXghV0Dwgk1ehPVpg6USIO4L0C8GqQJ4AMm/Xb1g=="], - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-HrEmNt/BH/hkQ7zpi2o6N3k1ZR1QTb7z85WYhYygiTxOQuaml4CMtHCWRbric5WPU+RNsYI7r1EpyVQMKO1pYw=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.5", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.2", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VxhX0B/dWGbpNHxrKCWUAJKXIXV015J4e7qYjdIU9lLWeptk0KMLGcqkB4wFxff5Njqur8dt8wRi1MN9lZtDqg=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.9", "", { "dependencies": { "@ai-sdk/provider": "3.0.5", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow=="], "@ai-sdk/react": ["@ai-sdk/react@2.0.120", "", { "dependencies": { "@ai-sdk/provider-utils": "3.0.20", "ai": "5.0.118", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1", "zod": "^3.25.76 || ^4.1.8" }, "optionalPeers": ["zod"] }, "sha512-x7Oa2LDRURc8uRnAdcEfydbHLSXGYjNaFlQrGuxZAMfqhLJQ+7x4K8Z6O5vnLt414mrPaVvgirfRqsP/nsxtnw=="], @@ -328,71 +335,71 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.965.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/credential-provider-node": "3.965.0", "@aws-sdk/middleware-bucket-endpoint": "3.965.0", "@aws-sdk/middleware-expect-continue": "3.965.0", "@aws-sdk/middleware-flexible-checksums": "3.965.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-location-constraint": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-sdk-s3": "3.965.0", "@aws-sdk/middleware-ssec": "3.965.0", "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/signature-v4-multi-region": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/eventstream-serde-browser": "^4.2.7", "@smithy/eventstream-serde-config-resolver": "^4.3.7", "@smithy/eventstream-serde-node": "^4.2.7", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-blob-browser": "^4.2.8", "@smithy/hash-node": "^4.2.7", "@smithy/hash-stream-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/md5-js": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-BTeaaU1iK0BfatTCrtYjNkIHCoZH256qOI18l9bK4z6mVOgpHkYN4RvOu+NnKgyX58n+HWfOuhtKUD4OE33Vdw=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.975.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.1", "@aws-sdk/credential-provider-node": "^3.972.1", "@aws-sdk/middleware-bucket-endpoint": "^3.972.1", "@aws-sdk/middleware-expect-continue": "^3.972.1", "@aws-sdk/middleware-flexible-checksums": "^3.972.1", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-location-constraint": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-sdk-s3": "^3.972.2", "@aws-sdk/middleware-ssec": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.2", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/signature-v4-multi-region": "3.972.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.1", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.11", "@smithy/middleware-retry": "^4.4.27", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.26", "@smithy/util-defaults-mode-node": "^4.2.29", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-aF1M/iMD29BPcpxjqoym0YFa4WR9Xie1/IhVumwOGH6TB45DaqYO7vLwantDBcYNRn/cZH6DFHksO7RmwTFBhw=="], - "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.965.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iv2tr+n4aZ+nPUFFvG00hISPuEd4DU+1/Q8rPAYKXsM+vEPJ2nAnP5duUOa2fbOLIUCRxX3dcQaQaghVHDHzQw=="], + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ci+GiM0c4ULo4D79UMcY06LcOLcfvUfiyt8PzNY0vbt5O8BfCPYf4QomwVgkNcLLCYmroO4ge2Yy1EsLUlcD6g=="], - "@aws-sdk/core": ["@aws-sdk/core@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws-sdk/xml-builder": "3.965.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-aq9BhQxdHit8UUJ9C0im9TtuKeK0pT6NXmNJxMTCFeStI7GG7ImIsSislg3BZTIifVg1P6VLdzMyz9de85iutQ=="], + "@aws-sdk/core": ["@aws-sdk/core@3.973.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ocubx42QsMyVs9ANSmFpRm0S+hubWljpPLjOi9UFrtcnVJjrVJTzQ51sN0e5g4e8i8QZ7uY73zosLmgYL7kZTQ=="], - "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-9FbIyJ/Zz1AdEIrb0+Pn7wRi+F/0Y566ooepg0hDyHUzRV3ZXKjOlu3wJH3YwTz2UkdwQmldfUos2yDJps7RyA=="], + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-mdGnaIjMxTIjsb70dEj3VsWPWpoq1V5MWzBSfJq2H8zgMBXjn6d5/qHP8HMf53l9PrsgqzMpXGv3Av549A2x1g=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/etNHqnx96phy/SjI0HRC588o4vKH5F0xfkZ13yAATV7aNrb+5gYGNE6ePWafP+FuZ3HkULSSlJFj0AxgrAqYw=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-YuGQel9EgA/z25oeLM+GYYQS750+8AESvr7ZEmVnRPL0sg+K3DmGqdv+9gFjFd0UkLjTlC/jtbP2cuY6UcPiHQ=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.2", "", { "dependencies": { "@aws-sdk/core": "^3.973.1", "@aws-sdk/types": "^3.973.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-mXgdaUfe5oM+tWKyeZ7Vh/iQ94FrkMky1uuzwTOmFADiRcSk5uHy/e3boEFedXiT/PRGzgBmqvJVK4F6lUISCg=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/credential-provider-env": "3.965.0", "@aws-sdk/credential-provider-http": "3.965.0", "@aws-sdk/credential-provider-login": "3.965.0", "@aws-sdk/credential-provider-process": "3.965.0", "@aws-sdk/credential-provider-sso": "3.965.0", "@aws-sdk/credential-provider-web-identity": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-xRo72Prer5s0xYVSCxCymVIRSqrVlevK5cmU0GWq9yJtaBNpnx02jwdJg80t/Ni7pgbkQyFWRMcq38c1tc6M/w=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/credential-provider-env": "^3.972.1", "@aws-sdk/credential-provider-http": "^3.972.1", "@aws-sdk/credential-provider-login": "^3.972.1", "@aws-sdk/credential-provider-process": "^3.972.1", "@aws-sdk/credential-provider-sso": "^3.972.1", "@aws-sdk/credential-provider-web-identity": "^3.972.1", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-OdbJA3v+XlNDsrYzNPRUwr8l7gw1r/nR8l4r96MDzSBDU8WEo8T6C06SvwaXR8SpzsjO3sq5KMP86wXWg7Rj4g=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-43/H8Qku8LHyugbhLo8kjD+eauhybCeVkmrnvWl8bXNHJP7xi1jCdtBQJKKJqiIHZws4MOEwkji8kFdAVRCe6g=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CccqDGL6ZrF3/EFWZefvKW7QwwRdxlHUO8NVBKNVcNq6womrPDvqB6xc9icACtE0XB0a7PLoSTkAg8bQVkTO2w=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.965.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.965.0", "@aws-sdk/credential-provider-http": "3.965.0", "@aws-sdk/credential-provider-ini": "3.965.0", "@aws-sdk/credential-provider-process": "3.965.0", "@aws-sdk/credential-provider-sso": "3.965.0", "@aws-sdk/credential-provider-web-identity": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-cRxmMHF+Zh2lkkkEVduKl+8OQdtg/DhYA69+/7SPSQURlgyjFQGlRQ58B7q8abuNlrGT3sV+UzeOylZpJbV61Q=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.1", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.1", "@aws-sdk/credential-provider-http": "^3.972.1", "@aws-sdk/credential-provider-ini": "^3.972.1", "@aws-sdk/credential-provider-process": "^3.972.1", "@aws-sdk/credential-provider-sso": "^3.972.1", "@aws-sdk/credential-provider-web-identity": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwXPk9GfuU/xG9tmCyXFVkCr6X3W8ZCoL5Ptb0pbltEx1/LCcg7T+PBqDlPiiinNCD6ilIoMJDWsnJ8ikzZA7Q=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-gmkPmdiR0yxnTzLPDb7rwrDhGuCUjtgnj8qWP+m0gSz/W43rR4jRPVEf6DUX2iC+ImQhxo3NFhuB3V42Kzo3TQ=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-bi47Zigu3692SJwdBvo8y1dEwE6B61stCwCFnuRWJVTfiM84B+VTSCV661CSWJmIZzmcy7J5J3kWyxL02iHj0w=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.965.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.965.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/token-providers": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-N01AYvtCqG3Wo/s/LvYt19ity18/FqggiXT+elAs3X9Om/Wfx+hw9G+i7jaDmy+/xewmv8AdQ2SK5Q30dXw/Fw=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.1", "", { "dependencies": { "@aws-sdk/client-sso": "3.974.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/token-providers": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dLZVNhM7wSgVUFsgVYgI5hb5Z/9PUkT46pk/SHrSmUqfx6YDvoV4YcPtaiRqviPpEGGiRtdQMEadyOKIRqulUQ=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-T4gMZ2JzXnfxe1oTD+EDGLSxFfk1+WkLZdiHXEMZp8bFI1swP/3YyDFXI+Ib9Uq1JhnAmrCXtOnkicKEhDkdhQ=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws-sdk/util-arn-parser": "3.965.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-gbdv3Dl8l8xmg4oH60fXvfDyTxfx28w5/Hxdymx3vurM07tAyd4qld8zEXejnSpraTo45QcHRtk5auELIMfeag=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-arn-parser": "^3.972.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-YVvoitBdE8WOpHqIXvv49efT73F4bJ99XH2bi3Dn3mx7WngI4RwHwn/zF5i0q1Wdi5frGSCNF3vuh+pY817//w=="], - "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-UBxVytsmhEmFwkBnt+aV0eAJ7uc+ouNokCqMBrQ7Oc5A77qhlcHfOgXIKz2SxqsiYTsDq+a0lWFM/XpyRWraqA=="], + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6lfl2/J/kutzw/RLu1kjbahsz4vrGPysrdxWaw8fkjLYG+6M6AswocIAZFS/LgAVi/IWRwPTx9YC0/NH2wDrSw=="], - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.965.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/crc64-nvme": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-5rzEW08trcpHMe6jkQyYc4PL1KG/H7BbnySFSzhih+r/gktQEiE36sb1BNf7av9I0Vk2Ccmt7wocB5PIT7GDkQ=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.972.1", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/crc64-nvme": "3.972.0", "@aws-sdk/types": "^3.973.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kjVVREpqeUkYQsXr78AcsJbEUlxGH7+H6yS7zkjrnu6HyEVxbdSndkKX6VpKneFOihjCAhIXlk4wf3butDHkNQ=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg=="], - "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-07T1rwAarQs33mVg5U28AsSdLB5JUXu9yBTBmspFGajKVsEahIyntf53j9mAXF1N2KR0bNdP0J4A0kst4t43UQ=="], + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-YisPaCbvBk9gY5aUI8jDMDKXsLZ9Fet0WYj1MviK8tZYMgxBIYHM6l3O/OHaAIujojZvamd9F3haYYYWp5/V3w=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-arn-parser": "3.965.0", "@smithy/core": "^3.20.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-dXEgnojaaVRl+OlOx35mg3rYEbfffIN4X6tLmIfDnaKz0hMaDMvsE9jJXb/vBvokbdO1sVB27/2FEM4ttLSLnw=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.2", "", { "dependencies": { "@aws-sdk/core": "^3.973.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-arn-parser": "^3.972.1", "@smithy/core": "^3.21.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-5f9x9/G+StE8+7wd9EVDF3d+J74xK+WBA3FhZwLSkf3pHFGLKzlmUfxJJE1kkXkbj/j/H+Dh3zL/hrtQE9hNsg=="], - "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dke++CTw26y+a2D1DdVuZ4+2TkgItdx6TeuE0zOl4lsqXGvTBUG4eaIZalt7ZOAW5ys2pbDOk1bPuh4opoD3pQ=="], + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-fLtRTPd/MxJT2drJKft2GVGKm35PiNEeQ1Dvz1vc/WhhgAteYrp4f1SfSgjgLaYWGMExESJL4bt8Dxqp6tVsog=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@smithy/core": "^3.20.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-RBEYVGgu/WeAt+H/qLrGc+t8LqAUkbyvh3wBfTiuAD+uBcWsKnvnB1iSBX75FearC0fmoxzXRUc0PMxMdqpjJQ=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.2", "", { "dependencies": { "@aws-sdk/core": "^3.973.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-d+Exq074wy0X6wvShg/kmZVtkah+28vMuqCtuY3cydg8LUZOJBtbAolCpEJizSyb8mJJZF9BjWaTANXL4OYnkg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.965.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.965.0", "@aws-sdk/middleware-host-header": "3.965.0", "@aws-sdk/middleware-logger": "3.965.0", "@aws-sdk/middleware-recursion-detection": "3.965.0", "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/region-config-resolver": "3.965.0", "@aws-sdk/types": "3.965.0", "@aws-sdk/util-endpoints": "3.965.0", "@aws-sdk/util-user-agent-browser": "3.965.0", "@aws-sdk/util-user-agent-node": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/core": "^3.20.0", "@smithy/fetch-http-handler": "^5.3.8", "@smithy/hash-node": "^4.2.7", "@smithy/invalid-dependency": "^4.2.7", "@smithy/middleware-content-length": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-retry": "^4.4.17", "@smithy/middleware-serde": "^4.2.8", "@smithy/middleware-stack": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/node-http-handler": "^4.4.7", "@smithy/protocol-http": "^5.3.7", "@smithy/smithy-client": "^4.10.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.16", "@smithy/util-defaults-mode-node": "^4.2.19", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-muNVUjUEU+/KLFrLzQ8PMXyw4+a/MP6t4GIvwLtyx/kH0rpSy5s0YmqacMXheuIe6F/5QT8uksXGNAQenitkGQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/config-resolver": "^4.4.5", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.965.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/protocol-http": "^5.3.7", "@smithy/signature-v4": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-hgbAThbsUrWtNpFBQxzXevIfd5Qgr4TLbXY1AIbmpSX9fPVC114pdieRMpopJ0fYaJ7v5/blTiS6wzVdXleZ/w=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.972.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.972.0", "@aws-sdk/types": "3.972.0", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.965.0", "", { "dependencies": { "@aws-sdk/core": "3.965.0", "@aws-sdk/nested-clients": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-aR0qxg0b8flkXJVE+CM1gzo7uJ57md50z2eyCwofC0QIz5Y0P7/7vvb9/dmUQt6eT9XRN5iRcUqq2IVxVDvJOw=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.974.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ=="], - "@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ=="], - "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.965.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNGKr5Tct28jGLkL8xIkGu7swpDgBpkTVbGaofhzr/X80iclbOv656RGxhMpDvmc4S9UuQnqLRXyceNFNF2V7Q=="], + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-XnNit6H9PPHhqUXW/usjX6JeJ6Pm8ZNqivTjmNjgWHeOfVpblUc/MTic02UmCNR0jJLPjQ3mBKiMen0tnkNQjQ=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-endpoints": "^3.2.7", "tslib": "^2.6.2" } }, "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.972.0", "", { "dependencies": { "@aws-sdk/types": "3.972.0", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg=="], "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9LJFand4bIoOjOF4x3wx0UZYiFZRo4oUauxQSiEX2dVg+5qeBOJSjp2SeWykIE6+6frCZ5wvWm2fGLK8D32aJw=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.965.0", "", { "dependencies": { "@aws-sdk/types": "3.965.0", "@smithy/types": "^4.11.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.965.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.965.0", "@aws-sdk/types": "3.965.0", "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-kokIHUfNT3/P55E4fUJJrFHuuA9BbjFKUIxoLrd3UaRfdafT0ScRfg2eaZie6arf60EuhlUIZH0yALxttMEjxQ=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.1", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.2", "", {}, "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg=="], @@ -438,13 +445,13 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], - "@clerk/backend": ["@clerk/backend@2.29.2", "", { "dependencies": { "@clerk/shared": "^3.42.0", "@clerk/types": "^4.101.10", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-HflWfWG0jfnOB++3bu1N0lzAgOuapJRmkfX1jdtF3M+Wn4QVHczLMt2To9VIGiTt62+kRUWLV4SONzVvCoOcsA=="], + "@clerk/backend": ["@clerk/backend@2.29.5", "", { "dependencies": { "@clerk/shared": "^3.43.2", "@clerk/types": "^4.101.13", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-CdimMOCtADH0vmcPy0jrxIJ+DyZeIeVrdSXGygVQzRUwSI1cLZpRUTyvRr8I27RlBFZK4GxXo5S3LUfjsiPqow=="], "@clerk/clerk-react": ["@clerk/clerk-react@5.59.3", "", { "dependencies": { "@clerk/shared": "^3.42.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-r1gmAYxhXs+QkXjDwj5Eqvm0Io8PtJ4FKkA45khiAzIQXcaQLFq/wFy7d1K8OSIYAIdFbuO0bnIOU/FdgWOc+A=="], - "@clerk/shared": ["@clerk/shared@3.42.0", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-sJUur/7jnHHlAsdoDosxpOmfV05VR7K5rvqlFskj3GaAMFEJrvdOztw0hmhBGVSWiCpjTZfdGITegton8mo7mQ=="], + "@clerk/shared": ["@clerk/shared@3.43.2", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-kR8q14/9Kz8Oah6aVTSLOI64gPAV44UqxTswRdu01cVRG36oyRbnJfFl/udZpDBdnPVuu2GyDLH7AztY8Td0fw=="], - "@clerk/types": ["@clerk/types@4.101.10", "", { "dependencies": { "@clerk/shared": "^3.42.0" } }, "sha512-qlmgnAm/IeK02RKEKVN8/Glx07xw/Lcv67jBfikM8HXhHc5v7bfYLD8UiWTr6H2RGtvB09cIt9JezRRlsuVBew=="], + "@clerk/types": ["@clerk/types@4.101.13", "", { "dependencies": { "@clerk/shared": "^3.43.2" } }, "sha512-PKv85uHjNXu8KO/Vc4m4e1GByItfuib/T3wNINDrq1k+QuzKwohC+n07ENlzOzr67tfbnfa6CQSgg2HUb4RohQ=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], @@ -558,6 +565,8 @@ "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -600,23 +609,25 @@ "@microsoft/tsdoc": ["@microsoft/tsdoc@0.16.0", "", {}, "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], - "@nestjs/common": ["@nestjs/common@10.4.21", "", { "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-2nabPCrq6HAc6PlZQsdDaV16ur7rs8Z8SH/rewS0SqbrvV6hgC/D5IPjVt4NvX7UjWKapqq+bymicuiZjP5WlQ=="], + "@nestjs/common": ["@nestjs/common@10.4.22", "", { "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw=="], "@nestjs/config": ["@nestjs/config@3.3.0", "", { "dependencies": { "dotenv": "16.4.5", "dotenv-expand": "10.0.0", "lodash": "4.17.21" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "rxjs": "^7.1.0" } }, "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA=="], - "@nestjs/core": ["@nestjs/core@10.4.21", "", { "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.3.0", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/websockets": "^10.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express", "@nestjs/websockets"] }, "sha512-MhiSGplB4TkadceA7opn/NaZmJhwYYNdB8nS8I29nLNx3vU+8aGHBiueZgcphEVDETZJSfc2VA5Mn/FC3JcsrA=="], + "@nestjs/core": ["@nestjs/core@10.4.22", "", { "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.3.0", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/websockets": "^10.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express", "@nestjs/websockets"] }, "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA=="], "@nestjs/mapped-types": ["@nestjs/mapped-types@2.1.0", "", { "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "class-transformer": "^0.4.0 || ^0.5.0", "class-validator": "^0.13.0 || ^0.14.0", "reflect-metadata": "^0.1.12 || ^0.2.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw=="], - "@nestjs/platform-express": ["@nestjs/platform-express@10.4.21", "", { "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", "express": "4.22.1", "multer": "2.0.2", "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0" } }, "sha512-xIsa4h+oKf4zrHpTWN2i0gYkGaXewDqv4+KCatI1+aWoZKScFdoI82MFfuzq+z/EBpnVP2ABGqvPJWu+ZhKYvQ=="], + "@nestjs/platform-express": ["@nestjs/platform-express@10.4.22", "", { "dependencies": { "body-parser": "1.20.4", "cors": "2.8.5", "express": "4.22.1", "multer": "2.0.2", "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0" } }, "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA=="], - "@nestjs/swagger": ["@nestjs/swagger@11.2.4", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "@nestjs/mapped-types": "2.1.0", "js-yaml": "4.1.1", "lodash": "4.17.21", "path-to-regexp": "8.3.0", "swagger-ui-dist": "5.31.0" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0", "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12 || ^0.2.0" }, "optionalPeers": ["@fastify/static", "class-transformer", "class-validator"] }, "sha512-7MLqtHfD2qfhZUyg13FyX6liwigtXUL8gHXq7PaBcGo9cu8QWDDT//Fn3qzJx59+Wh+Ly/Zn+prCMpskPI5nrQ=="], + "@nestjs/swagger": ["@nestjs/swagger@11.2.5", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "@nestjs/mapped-types": "2.1.0", "js-yaml": "4.1.1", "lodash": "4.17.21", "path-to-regexp": "8.3.0", "swagger-ui-dist": "5.31.0" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0", "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12 || ^0.2.0" }, "optionalPeers": ["@fastify/static", "class-transformer", "class-validator"] }, "sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA=="], - "@nestjs/testing": ["@nestjs/testing@10.4.21", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express"] }, "sha512-mQyJvrJ4mA9nukx+zXafh0iLtbGmwalnGWdoTih6cKtANGewIVsqgfSpuxwzyR4d42uc5jqgBulEZszmUFQ/5A=="], + "@nestjs/testing": ["@nestjs/testing@10.4.22", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/microservices": "^10.0.0", "@nestjs/platform-express": "^10.0.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express"] }, "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], @@ -648,7 +659,7 @@ "@pm2/pm2-version-check": ["@pm2/pm2-version-check@1.0.4", "", { "dependencies": { "debug": "^4.3.1" } }, "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA=="], - "@posthog/core": ["@posthog/core@1.9.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw=="], + "@posthog/core": ["@posthog/core@1.14.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-havjGYHwL8Gy6LXIR911h+M/sYlJLQbepxP/cc1M7Cp3v8F92bzpqkbuvUIUyb7/izkxfGwc9wMqKAo0QxMTrg=="], "@posthog/types": ["@posthog/types@1.316.0", "", {}, "sha512-2CS2wecsv9Eafa8euXePr6ba1/eV9k7BLAREKd2SqCsTd6bYRFILvVDlkrvPCSpdZ6qOq/b6IA//HMC1Z0fN8w=="], @@ -842,75 +853,75 @@ "@shipsec/studio-worker": ["@shipsec/studio-worker@workspace:worker"], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="], "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="], - "@smithy/core": ["@smithy/core@3.20.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-wOboSEdQ85dbKAJ0zL+wQ6b0HTSBRhtGa0PYKysQXkRg+vK0tdCRRVruiFM2QMprkOQwSYOnwF4og96PAaEGag=="], + "@smithy/core": ["@smithy/core@3.21.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-NUH8R4O6FkN8HKMojzbGg/5pNjsfTjlMmeFclyPfPaXXUrbr5TzhWgbf7t92wfrpCHRgpjyz7ffASIS3wX28aA=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="], - "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.7", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.7", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g=="], + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.8", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.8", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.9", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw=="], + "@smithy/hash-node": ["@smithy/hash-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ=="], "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], - "@smithy/md5-js": ["@smithy/md5-js@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw=="], + "@smithy/md5-js": ["@smithy/md5-js@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.7", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.2", "", { "dependencies": { "@smithy/core": "^3.20.1", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-mqpAdux0BNmZu/SqkFhQEnod4fX23xxTvU2LUpmKp0JpSI+kPYCiHJMmzREr8yxbNxKL2/DU1UZm9i++ayU+2g=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.11", "", { "dependencies": { "@smithy/core": "^3.21.1", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-/WqsrycweGGfb9sSzME4CrsuayjJF6BueBmkKlcbeU5q18OhxRrvvKlmfw3tpDsK5ilx2XUJvoukwxHB0nHs/Q=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.18", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-E5hulijA59nBk/zvcwVMaS7FG7Y4l6hWA9vrW018r+8kiZef4/ETQaPI4oY+3zsy9f6KqDv3c4VKtO4DwwgpCg=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.27", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-xFUYCGRVsfgiN5EjsJJSzih9+yjStgMTCLANPlf0LVQkPDYCe0hz97qbdTZosFOiYlGBlHYityGRxrQ/hxhfVQ=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.7", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA=="], + "@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w=="], + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0" } }, "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0" } }, "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.2", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.3", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.7", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.10.3", "", { "dependencies": { "@smithy/core": "^3.20.1", "@smithy/middleware-endpoint": "^4.4.2", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-EfECiO/0fAfb590LBnUe7rI5ux7XfquQ8LBzTe7gxw0j9QW/q8UT/EHWHlxV/+jhQ3+Ssga9uUYXCQgImGMbNg=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.10.12", "", { "dependencies": { "@smithy/core": "^3.21.1", "@smithy/middleware-endpoint": "^4.4.11", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" } }, "sha512-VKO/HKoQ5OrSHW6AJUmEnUKeXI1/5LfCwO9cwyao7CmLvGnZeM1i36Lyful3LK1XU7HwTVieTqO1y2C/6t3qtA=="], - "@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + "@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.7", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg=="], + "@smithy/url-parser": ["@smithy/url-parser@4.2.8", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA=="], "@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], @@ -922,25 +933,25 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.17", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-dwN4GmivYF1QphnP3xJESXKtHvkkvKHSZI8GrSKMVoENVSKW2cFPRYC4ZgstYjUHdR3zwaDkIaTDIp26JuY7Cw=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.26", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vva0dzYUTgn7DdE0uaha10uEdAgmdLnNFowKFjpMm6p2R0XDk5FHPX3CBJLzWQkQXuEprsb0hGz9YwbicNWhjw=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.20", "", { "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-VD/I4AEhF1lpB3B//pmOIMBNLMrtdMXwy9yCOfa2QkJGDr63vH3RqPbSAKzoGMov3iryCxTXCxSsyGmEB8PDpg=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.29", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-c6D7IUBsZt/aNnTBHMTf+OVh+h/JcxUUgfTcIJaWRe6zhOum1X+pNKSZtZ+7fbOn5I99XVFtmrnXKv8yHHErTQ=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.7", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="], "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A=="], - "@smithy/util-retry": ["@smithy/util-retry@4.2.7", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg=="], + "@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.8", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.10", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.8", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg=="], "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], @@ -948,27 +959,27 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@swc/core": ["@swc/core@1.15.8", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.8", "@swc/core-darwin-x64": "1.15.8", "@swc/core-linux-arm-gnueabihf": "1.15.8", "@swc/core-linux-arm64-gnu": "1.15.8", "@swc/core-linux-arm64-musl": "1.15.8", "@swc/core-linux-x64-gnu": "1.15.8", "@swc/core-linux-x64-musl": "1.15.8", "@swc/core-win32-arm64-msvc": "1.15.8", "@swc/core-win32-ia32-msvc": "1.15.8", "@swc/core-win32-x64-msvc": "1.15.8" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw=="], + "@swc/core": ["@swc/core@1.15.10", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.10", "@swc/core-darwin-x64": "1.15.10", "@swc/core-linux-arm-gnueabihf": "1.15.10", "@swc/core-linux-arm64-gnu": "1.15.10", "@swc/core-linux-arm64-musl": "1.15.10", "@swc/core-linux-x64-gnu": "1.15.10", "@swc/core-linux-x64-musl": "1.15.10", "@swc/core-win32-arm64-msvc": "1.15.10", "@swc/core-win32-ia32-msvc": "1.15.10", "@swc/core-win32-x64-msvc": "1.15.10" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw=="], - "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg=="], + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA=="], - "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ=="], + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ=="], - "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.8", "", { "os": "linux", "cpu": "arm" }, "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg=="], + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.10", "", { "os": "linux", "cpu": "arm" }, "sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg=="], - "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q=="], + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ=="], - "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw=="], + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw=="], - "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.8", "", { "os": "linux", "cpu": "x64" }, "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ=="], + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.10", "", { "os": "linux", "cpu": "x64" }, "sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg=="], - "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.8", "", { "os": "linux", "cpu": "x64" }, "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA=="], + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.10", "", { "os": "linux", "cpu": "x64" }, "sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw=="], - "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ=="], + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw=="], - "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ=="], + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw=="], - "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.8", "", { "os": "win32", "cpu": "x64" }, "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA=="], + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.10", "", { "os": "win32", "cpu": "x64" }, "sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], @@ -978,21 +989,21 @@ "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@temporalio/activity": ["@temporalio/activity@1.14.0", "", { "dependencies": { "@temporalio/client": "1.14.0", "@temporalio/common": "1.14.0", "abort-controller": "^3.0.0" } }, "sha512-ayGqfjqW8R1nhow54Y3A5ezoVwFr4SbB8VHaQA3seDFOB+6TyOVSlulYqGgFMxl/FXBkRa/VEswEDqS/xQq7aQ=="], + "@temporalio/activity": ["@temporalio/activity@1.14.1", "", { "dependencies": { "@temporalio/client": "1.14.1", "@temporalio/common": "1.14.1", "abort-controller": "^3.0.0" } }, "sha512-wG2fTNgomhcKOzPY7mqhKqe8scawm4BvUYdgX1HJouHmVNRgtZurf2xQWJZQOTxWrsXfdoYqzohZLzxlNtcC5A=="], - "@temporalio/client": ["@temporalio/client@1.14.0", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@temporalio/common": "1.14.0", "@temporalio/proto": "1.14.0", "abort-controller": "^3.0.0", "long": "^5.2.3", "uuid": "^11.1.0" } }, "sha512-kjzJ+7M2kHj32cTTSQT5WOjEIOxY0TNV5g6Sw9PzWmKWdtIZig+d7qUIA3VjDe/TieNozxjR2wNAX5sKzYFANA=="], + "@temporalio/client": ["@temporalio/client@1.14.1", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@temporalio/common": "1.14.1", "@temporalio/proto": "1.14.1", "abort-controller": "^3.0.0", "long": "^5.2.3", "uuid": "^11.1.0" } }, "sha512-AfWSA0LYzBvDLFiFgrPWqTGGq1NGnF3d4xKnxf0PGxSmv5SLb/aqQ9lzHg4DJ5UNkHO4M/NwzdxzzoaR1J5F8Q=="], - "@temporalio/common": ["@temporalio/common@1.14.0", "", { "dependencies": { "@temporalio/proto": "1.14.0", "long": "^5.2.3", "ms": "3.0.0-canary.1", "nexus-rpc": "^0.0.1", "proto3-json-serializer": "^2.0.0" } }, "sha512-jVmurBdFHdqw/wIehzVJikS8MhavL630p88TJ64P5PH0nP8S5V8R5vhkmHZ7n0sMRO+A0QFyWYyvnccu6MQZvw=="], + "@temporalio/common": ["@temporalio/common@1.14.1", "", { "dependencies": { "@temporalio/proto": "1.14.1", "long": "^5.2.3", "ms": "3.0.0-canary.1", "nexus-rpc": "^0.0.1", "proto3-json-serializer": "^2.0.0" } }, "sha512-y49wOm3AIEKZufIQ/QU5JhTSaHJIEkiUt5bGB0/uSzCg8P4g8Cz0XoVPSbDwuCix533O9cOKcliYq7Gzjt/sIA=="], - "@temporalio/core-bridge": ["@temporalio/core-bridge@1.14.0", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@temporalio/common": "1.14.0", "arg": "^5.0.2", "cargo-cp-artifact": "^0.1.8", "which": "^4.0.0" } }, "sha512-62WRbESKVtCx1FafbikQB90EwKNF+mEAaOJKifUIU4lQnk9wlZPRfrf6pwyqr+Uqi7uZhD2YqHXWUNVYbmQU7w=="], + "@temporalio/core-bridge": ["@temporalio/core-bridge@1.14.1", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@temporalio/common": "1.14.1" } }, "sha512-mrXXIFK5yNvsSZsTejLnL64JMuMliQjFKktSGITm2Ci7cWZ/ZTOVN6u+hCsUKfadYYv83jSuOC9Xe3z3RK273w=="], - "@temporalio/nexus": ["@temporalio/nexus@1.14.0", "", { "dependencies": { "@temporalio/client": "1.14.0", "@temporalio/common": "1.14.0", "@temporalio/proto": "1.14.0", "long": "^5.2.3", "nexus-rpc": "^0.0.1" } }, "sha512-0tgf+EBuz5vgYUukaYUzVHKr27XNQejXXO1i0x8+4sjR5zN6euNKraHfRzrDWRSm3nTZ6199rCTbR+CPrqaC/g=="], + "@temporalio/nexus": ["@temporalio/nexus@1.14.1", "", { "dependencies": { "@temporalio/client": "1.14.1", "@temporalio/common": "1.14.1", "@temporalio/proto": "1.14.1", "long": "^5.2.3", "nexus-rpc": "^0.0.1" } }, "sha512-51oTeJ8nntAMF8boFSlzVdHlyC7y/LaLQPZMjEEOV2pi8O9yOI7GZvYDIAHhY8Z8AcDVgbXb8x0BbkjkwNiUiQ=="], - "@temporalio/proto": ["@temporalio/proto@1.14.0", "", { "dependencies": { "long": "^5.2.3", "protobufjs": "^7.2.5" } }, "sha512-duYVjt3x6SkuFzJr+5NlklEgookPqW065qdcvogmdfVjrgiwz4W/07AN3+fL4ufmqt1//0SyF6nyqv9RNADYNA=="], + "@temporalio/proto": ["@temporalio/proto@1.14.1", "", { "dependencies": { "long": "^5.2.3", "protobufjs": "^7.2.5" } }, "sha512-mCsUommDPXbXbBu60p1g4jpSqVb+GNR67yR0uKTU8ARb4qVZQo7SQnOUaneoxDERDXuR/yIjVCektMm+7Myb+A=="], - "@temporalio/worker": ["@temporalio/worker@1.14.0", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@swc/core": "^1.3.102", "@temporalio/activity": "1.14.0", "@temporalio/client": "1.14.0", "@temporalio/common": "1.14.0", "@temporalio/core-bridge": "1.14.0", "@temporalio/nexus": "1.14.0", "@temporalio/proto": "1.14.0", "@temporalio/workflow": "1.14.0", "abort-controller": "^3.0.0", "heap-js": "^2.6.0", "memfs": "^4.6.0", "nexus-rpc": "^0.0.1", "proto3-json-serializer": "^2.0.0", "protobufjs": "^7.2.5", "rxjs": "^7.8.1", "source-map": "^0.7.4", "source-map-loader": "^4.0.2", "supports-color": "^8.1.1", "swc-loader": "^0.2.3", "unionfs": "^4.5.1", "webpack": "^5.94.0" } }, "sha512-wo5rgPSt83aT1hLYmh/0X4yOx/6uRbIvBa9LXqGo7s9s1GJkUyJpAahRt8aMoLm4qPsiZtu1gtU5KcASOmgqtg=="], + "@temporalio/worker": ["@temporalio/worker@1.14.1", "", { "dependencies": { "@grpc/grpc-js": "^1.12.4", "@swc/core": "^1.3.102", "@temporalio/activity": "1.14.1", "@temporalio/client": "1.14.1", "@temporalio/common": "1.14.1", "@temporalio/core-bridge": "1.14.1", "@temporalio/nexus": "1.14.1", "@temporalio/proto": "1.14.1", "@temporalio/workflow": "1.14.1", "abort-controller": "^3.0.0", "heap-js": "^2.6.0", "memfs": "^4.6.0", "nexus-rpc": "^0.0.1", "proto3-json-serializer": "^2.0.0", "protobufjs": "^7.2.5", "rxjs": "^7.8.1", "source-map": "^0.7.4", "source-map-loader": "^4.0.2", "supports-color": "^8.1.1", "swc-loader": "^0.2.3", "unionfs": "^4.5.1", "webpack": "^5.94.0" } }, "sha512-wFfN5gc03eq1bYAuJNsG9a1iWBG6hL9zAfYbxiJdshPhpHa82BtHGvXD447oT2BX3zqI+Jf2b0m/N0wgkW6wyQ=="], - "@temporalio/workflow": ["@temporalio/workflow@1.14.0", "", { "dependencies": { "@temporalio/common": "1.14.0", "@temporalio/proto": "1.14.0", "nexus-rpc": "^0.0.1" } }, "sha512-hxUqCZTkdSwgy5nc/O1DIpYH0Z77cM57RfJvhK4ELmkkb1jh/Q4dshDannH1qQ1zYT0IKRBHSW7m1aMy1+dgDA=="], + "@temporalio/workflow": ["@temporalio/workflow@1.14.1", "", { "dependencies": { "@temporalio/common": "1.14.1", "@temporalio/proto": "1.14.1", "nexus-rpc": "^0.0.1" } }, "sha512-MzshcoRo8zjQYa9WHrv3XC8LVvpRNSVaW3kOSTmHuTYDh/7be48WODOgs5yUpbnkpsw6rjVCDCgtB/K02cQwDg=="], "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], @@ -1022,7 +1033,7 @@ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -1102,7 +1113,7 @@ "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], @@ -1136,7 +1147,7 @@ "@types/multer": ["@types/multer@2.0.0", "", { "dependencies": { "@types/express": "*" } }, "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw=="], - "@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -1170,25 +1181,25 @@ "@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], "@uiw/copy-to-clipboard": ["@uiw/copy-to-clipboard@1.0.19", "", {}, "sha512-AYxzFUBkZrhtExb2QC0C4lFH2+BSx6JVId9iqeGHakBuosqiQHUQaNZCvIBeM97Ucp+nJ22flOh8FBT2pKRRAA=="], @@ -1238,7 +1249,7 @@ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -1250,11 +1261,11 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@6.0.23", "", { "dependencies": { "@ai-sdk/gateway": "3.0.10", "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.4", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IV8hqp6sQvZ0XVlu8bCnFlwG7+2d40ff26RZ1k4yw/zVuk2F6SXlONURtTo9vwPOPYeF7auXvyPA+dMDoepWxg=="], + "ai": ["ai@6.0.49", "", { "dependencies": { "@ai-sdk/gateway": "3.0.22", "@ai-sdk/provider": "3.0.5", "@ai-sdk/provider-utils": "4.0.9", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], @@ -1344,7 +1355,7 @@ "bodec": ["bodec@0.1.0", "", {}, "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ=="], - "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -1366,7 +1377,7 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], @@ -1388,8 +1399,6 @@ "caniuse-lite": ["caniuse-lite@1.0.30001763", "", {}, "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ=="], - "cargo-cp-artifact": ["cargo-cp-artifact@0.1.9", "", { "bin": { "cargo-cp-artifact": "bin/cargo-cp-artifact.js" } }, "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -1448,7 +1457,7 @@ "consola": ["consola@2.15.3", "", {}, "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="], - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1670,9 +1679,13 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -1716,7 +1729,7 @@ "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -1740,7 +1753,7 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-monkey": ["fs-monkey@1.1.0", "", {}, "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw=="], @@ -1790,7 +1803,7 @@ "global-jsdom": ["global-jsdom@27.0.0", "", { "peerDependencies": { "jsdom": ">=27 <28" } }, "sha512-0lIJfZACEKdBZ1VKSY2L4T2sYhnQ4VfzbULUiEMPcUa3ZWv4ywOCNZAibZyKIDwysIr+WjRyM4U9L9vM6oQQ+Q=="], - "globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="], + "globals": ["globals@17.1.0", "", {}, "sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -1852,6 +1865,8 @@ "heap-js": ["heap-js@2.7.1", "", {}, "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA=="], + "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], @@ -1860,7 +1875,7 @@ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -1868,7 +1883,7 @@ "hyperdyperid": ["hyperdyperid@1.2.0", "", {}, "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A=="], - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1890,7 +1905,7 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "ioredis": ["ioredis@5.9.1", "", { "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ=="], + "ioredis": ["ioredis@5.9.2", "", { "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ=="], "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], @@ -1948,6 +1963,8 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], @@ -1980,6 +1997,8 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], "js-git": ["js-git@0.7.8", "", { "dependencies": { "bodec": "^0.1.0", "culvert": "^0.1.2", "git-sha1": "^0.1.2", "pako": "^0.2.5" } }, "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA=="], @@ -2002,7 +2021,9 @@ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], @@ -2108,13 +2129,13 @@ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "memfs": ["memfs@4.51.1", "", { "dependencies": { "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ=="], "meow": ["meow@10.1.5", "", { "dependencies": { "@types/minimist": "^1.2.2", "camelcase-keys": "^7.0.0", "decamelize": "^5.0.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.2", "read-pkg-up": "^8.0.0", "redent": "^4.0.0", "trim-newlines": "^4.0.2", "type-fest": "^1.2.2", "yargs-parser": "^20.2.9" } }, "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -2182,9 +2203,9 @@ "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "mimoza": ["mimoza@1.0.0", "", { "dependencies": { "mime-db": "^1.6.0" } }, "sha512-+j7SSye/hablu66K/jjeyPmk6WL8RoXfeZ+MMn37vSNDGuaWY/5wm10LpSpxAHX4kNoEwkTWYHba8ePVip+Hqg=="], @@ -2222,11 +2243,11 @@ "needle": ["needle@2.4.0", "", { "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" }, "bin": { "needle": "./bin/needle" } }, "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "nestjs-zod": ["nestjs-zod@5.1.0", "", { "dependencies": { "deepmerge": "^4.3.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/swagger": "^7.4.2 || ^8.0.0 || ^11.0.0", "rxjs": "^7.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@nestjs/swagger"] }, "sha512-BhJb8UIYyGGbZLmnKN5z/TAlejdfUQA3ytS60jaUtZQ4RIdZ8VoQhmwVPYbu/iGh0wvdamD/sR4FEMkbPNBs2A=="], + "nestjs-zod": ["nestjs-zod@5.1.1", "", { "dependencies": { "deepmerge": "^4.3.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/swagger": "^7.4.2 || ^8.0.0 || ^11.0.0", "rxjs": "^7.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@nestjs/swagger"] }, "sha512-pXa9Jrdip7iedKvGxJTvvCFVRCoIcNENPCsHjpCefPH3PcFejRgkZkUcr3TYITRyxnUk7Zy5OsLpirZGLYBfBQ=="], "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], @@ -2320,15 +2341,15 @@ "path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="], - "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], + "pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], - "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], - "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], + "pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + "pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="], "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], @@ -2346,6 +2367,8 @@ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], "pm2": ["pm2@6.0.14", "", { "dependencies": { "@pm2/agent": "~2.1.1", "@pm2/blessed": "0.1.81", "@pm2/io": "~6.1.0", "@pm2/js-api": "~0.8.0", "@pm2/pm2-version-check": "^1.0.4", "ansis": "4.0.0-node10", "async": "3.2.6", "chokidar": "3.6.0", "cli-tableau": "2.0.1", "commander": "2.15.1", "croner": "4.1.97", "dayjs": "1.11.15", "debug": "4.4.3", "enquirer": "2.3.6", "eventemitter2": "5.0.1", "fclone": "1.0.11", "js-yaml": "4.1.1", "mkdirp": "1.0.4", "needle": "2.4.0", "pidusage": "3.0.2", "pm2-axon": "~4.0.1", "pm2-axon-rpc": "~0.7.1", "pm2-deploy": "~1.0.2", "pm2-multimeter": "^0.1.2", "promptly": "2.2.0", "semver": "7.7.2", "source-map-support": "0.5.21", "sprintf-js": "1.1.2", "vizion": "~2.2.1" }, "optionalDependencies": { "pm2-sysmonit": "^1.2.8" }, "bin": { "pm2": "bin/pm2", "pm2-dev": "bin/pm2-dev", "pm2-docker": "bin/pm2-docker", "pm2-runtime": "bin/pm2-runtime" } }, "sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA=="], @@ -2386,13 +2409,13 @@ "posthog-js": ["posthog-js@1.316.0", "", { "dependencies": { "@posthog/core": "1.9.1", "@posthog/types": "1.316.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" } }, "sha512-Y8JKqv61HQKHB4savYnbp1+zUhfkSLSE4famJONaPuBq9lX+F8j9uNENvIWioe2roJa7FU3g5Ji/CjwwRh+/kA=="], - "posthog-node": ["posthog-node@5.20.0", "", { "dependencies": { "@posthog/core": "1.9.1" } }, "sha512-LkR5KfrvEQTnUtNKN97VxFB00KcYG1Iz8iKg8r0e/i7f1eQhg1WSZO+Jp1B4bvtHCmdpIE4HwYbvCCzFoCyjVg=="], + "posthog-node": ["posthog-node@5.24.2", "", { "dependencies": { "@posthog/core": "1.14.0" } }, "sha512-cywIUYtSIC9BilgLlZd1R2xNk6omKL6tywG/SCPmUJKeG2jhjvJHSrHXYx4x3uQsUjn8aB9UVI8km+W326Zm8g=="], "preact": ["preact@10.28.2", "", {}, "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], @@ -2436,7 +2459,7 @@ "rasha": ["rasha@1.2.5", "", { "bin": { "rasha": "bin/rasha.js" } }, "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw=="], - "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], @@ -2532,6 +2555,8 @@ "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-series": ["run-series@1.1.9", "", {}, "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g=="], @@ -2560,11 +2585,11 @@ "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -2766,7 +2791,7 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -2780,7 +2805,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="], + "typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="], "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], @@ -2790,7 +2815,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + "undici": ["undici@7.19.0", "", {}, "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -2918,7 +2943,9 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -2932,12 +2959,30 @@ "@ai-sdk/react/ai": ["ai@5.0.118", "", { "dependencies": { "@ai-sdk/gateway": "2.0.24", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sKJHfhJkvAyq5NC3yJJ4R8Z3tn4pSHF760/jInKAtmLwPLWTHfGo293DSO4un8QUAgJOagHd09VSXOXv+STMNQ=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + + "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + + "@aws-crypto/sha1-browser/@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + + "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.965.0", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ=="], + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.0", "", { "dependencies": { "@aws-sdk/core": "3.972.0", "@aws-sdk/types": "3.972.0", "@aws-sdk/util-arn-parser": "3.972.0", "@smithy/core": "^3.20.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w=="], + + "@aws-sdk/signature-v4-multi-region/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="], + + "@aws-sdk/util-endpoints/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="], + "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -2946,6 +2991,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@clerk/clerk-react/@clerk/shared": ["@clerk/shared@3.42.0", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-sJUur/7jnHHlAsdoDosxpOmfV05VR7K5rvqlFskj3GaAMFEJrvdOztw0hmhBGVSWiCpjTZfdGITegton8mo7mQ=="], + "@clerk/shared/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "@clerk/shared/swr": ["swr@2.3.4", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg=="], @@ -2954,6 +3001,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -2964,8 +3013,12 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@nestjs/config/dotenv": ["dotenv@16.4.5", "", {}, "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="], + "@nestjs/platform-express/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "@nestjs/swagger/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "@nuxtjs/opencollective/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -3032,21 +3085,19 @@ "@reactflow/node-toolbar/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - "@redocly/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@redocly/openapi-core/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - "@shipsec/component-sdk/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + "@shipsec/component-sdk/@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], - "@shipsec/studio-backend/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + "@shipsec/studio-backend/@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], "@shipsec/studio-frontend/ai": ["ai@5.0.118", "", { "dependencies": { "@ai-sdk/gateway": "2.0.24", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sKJHfhJkvAyq5NC3yJJ4R8Z3tn4pSHF760/jInKAtmLwPLWTHfGo293DSO4un8QUAgJOagHd09VSXOXv+STMNQ=="], "@shipsec/studio-worker/@googleapis/admin": ["@googleapis/admin@29.0.0", "", { "dependencies": { "googleapis-common": "^8.0.0" } }, "sha512-lujnbfmDn1aetoJBDeExH4IDKniMZs7Ga8hGagN/lecO8hd0fy9hu+osROp0HFk6u2wCAiA89oXi5qHVXupbOQ=="], - "@shipsec/studio-worker/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + "@shipsec/studio-worker/@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], - "@temporalio/core-bridge/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "@temporalio/worker/@swc/core": ["@swc/core@1.15.8", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.8", "@swc/core-darwin-x64": "1.15.8", "@swc/core-linux-arm-gnueabihf": "1.15.8", "@swc/core-linux-arm64-gnu": "1.15.8", "@swc/core-linux-arm64-musl": "1.15.8", "@swc/core-linux-x64-gnu": "1.15.8", "@swc/core-linux-x64-musl": "1.15.8", "@swc/core-win32-arm64-msvc": "1.15.8", "@swc/core-win32-ia32-msvc": "1.15.8", "@swc/core-win32-x64-msvc": "1.15.8" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw=="], "@temporalio/worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -3056,23 +3107,37 @@ "@tokenizer/inflate/fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "@types/adm-zip/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + + "@types/body-parser/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + + "@types/connect/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + "@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@types/node-forge/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@types/pg/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + + "@types/send/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@types/serve-static/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], - "ajv-keywords/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@types/superagent/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "bun-types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "body-parser/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "camelcase-keys/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], @@ -3092,24 +3157,24 @@ "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "eslint-plugin-react-hooks/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + "express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], @@ -3132,7 +3197,7 @@ "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "jest-worker/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -3150,10 +3215,18 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "mimoza/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "minio/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "multer/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "needle/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "needle/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "njwt/@types/node": ["@types/node@15.14.9", "", {}, "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A=="], "njwt/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -3170,16 +3243,22 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + "pm2-sysmonit/pidusage": ["pidusage@2.0.21", "", { "dependencies": { "safe-buffer": "^5.2.1" } }, "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA=="], "postcss-import/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + "posthog-js/@posthog/core": ["@posthog/core@1.9.1", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "protobufjs/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], @@ -3198,13 +3277,9 @@ "require-in-the-middle/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - "schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "send/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -3230,22 +3305,44 @@ "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@ai-sdk/react/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mflk80YF8hj8vrF9e1IHhovGKC1ubX+sY88pesSk3pUiXfH5VPO8dgzNnxjwsqsCZrnkHcztxS5cSl4TzSiEuA=="], "@ai-sdk/react/ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], + "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + + "@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + + "@aws-crypto/sha1-browser/@aws-sdk/types/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-crypto/sha256-browser/@aws-sdk/types/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-crypto/sha256-js/@aws-sdk/types/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + + "@aws-crypto/util/@aws-sdk/types/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core": ["@aws-sdk/core@3.972.0", "", { "dependencies": { "@aws-sdk/types": "3.972.0", "@aws-sdk/xml-builder": "3.972.0", "@smithy/core": "^3.20.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A=="], + + "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g=="], + "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@clerk/clerk-react/@clerk/shared/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "@clerk/clerk-react/@clerk/shared/swr": ["swr@2.3.4", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -3290,12 +3387,40 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@nestjs/platform-express/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "@nestjs/platform-express/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + + "@nestjs/platform-express/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "@nestjs/platform-express/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "@nestjs/platform-express/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@nestjs/platform-express/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "@nestjs/platform-express/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "@nestjs/platform-express/express/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "@nestjs/platform-express/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "@nestjs/platform-express/express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "@nestjs/platform-express/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "@nestjs/platform-express/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "@nestjs/platform-express/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "@nuxtjs/opencollective/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "@okta/okta-sdk-nodejs/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], @@ -3324,19 +3449,35 @@ "@shipsec/studio-frontend/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "@shipsec/studio-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@temporalio/worker/@swc/core/@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg=="], - "@temporalio/core-bridge/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "@temporalio/worker/@swc/core/@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@temporalio/worker/@swc/core/@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.8", "", { "os": "linux", "cpu": "arm" }, "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg=="], + + "@temporalio/worker/@swc/core/@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q=="], + + "@temporalio/worker/@swc/core/@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw=="], - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@temporalio/worker/@swc/core/@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.8", "", { "os": "linux", "cpu": "x64" }, "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ=="], - "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@temporalio/worker/@swc/core/@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.8", "", { "os": "linux", "cpu": "x64" }, "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA=="], + + "@temporalio/worker/@swc/core/@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ=="], + + "@temporalio/worker/@swc/core/@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ=="], + + "@temporalio/worker/@swc/core/@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.8", "", { "os": "win32", "cpu": "x64" }, "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA=="], + + "@types/express/@types/express-serve-static-core/@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "body-parser/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "body-parser/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "chalk-animation/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -3394,11 +3535,11 @@ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -3428,6 +3569,12 @@ "meow/redent/strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + "minio/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "multer/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "multer/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "needle/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -3436,14 +3583,12 @@ "refractor/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "ts-loader/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "@ai-sdk/react/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], @@ -3452,8 +3597,46 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg=="], + + "@nestjs/platform-express/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "@nestjs/platform-express/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "@nestjs/platform-express/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "@nestjs/platform-express/express/body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "@nestjs/platform-express/express/body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "@nestjs/platform-express/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@nestjs/platform-express/express/http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "@nestjs/platform-express/express/send/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "@nestjs/platform-express/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "@nestjs/platform-express/express/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@nestjs/platform-express/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "@nestjs/platform-express/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@shipsec/studio-frontend/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "body-parser/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "markdown-it-html5-embed/markdown-it/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + + "@nestjs/platform-express/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@nestjs/platform-express/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], } } diff --git a/docker/mcp-aws-cloudtrail/Dockerfile b/docker/mcp-aws-cloudtrail/Dockerfile new file mode 100644 index 00000000..43a0ccff --- /dev/null +++ b/docker/mcp-aws-cloudtrail/Dockerfile @@ -0,0 +1,11 @@ +ARG BASE_IMAGE=shipsec/mcp-stdio-proxy:latest +FROM ${BASE_IMAGE} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 python3-pip \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir --break-system-packages uv \ + && uv pip install --system --break-system-packages awslabs-cloudtrail-mcp-server + +ENV MCP_COMMAND=awslabs.cloudtrail-mcp-server +ENV MCP_ARGS='[]' diff --git a/docker/mcp-aws-cloudtrail/README.md b/docker/mcp-aws-cloudtrail/README.md new file mode 100644 index 00000000..7828a5d6 --- /dev/null +++ b/docker/mcp-aws-cloudtrail/README.md @@ -0,0 +1,22 @@ +# AWS CloudTrail MCP Proxy Image + +This image extends the MCP stdio proxy and installs the CloudTrail MCP server. + +## Build + +```bash +docker build -t shipsec/mcp-aws-cloudtrail:latest docker/mcp-aws-cloudtrail +``` + +## Run (example) + +```bash +docker run --rm -p 8080:8080 \ + -e AWS_ACCESS_KEY_ID=... \ + -e AWS_SECRET_ACCESS_KEY=... \ + -e AWS_SESSION_TOKEN=... \ + -e AWS_REGION=us-east-1 \ + shipsec/mcp-aws-cloudtrail:latest +``` + +The proxy exposes MCP on `http://localhost:8080/mcp`. diff --git a/docker/mcp-aws-cloudwatch/Dockerfile b/docker/mcp-aws-cloudwatch/Dockerfile new file mode 100644 index 00000000..4283d8c5 --- /dev/null +++ b/docker/mcp-aws-cloudwatch/Dockerfile @@ -0,0 +1,14 @@ +ARG BASE_IMAGE=shipsec/mcp-stdio-proxy:latest +FROM ${BASE_IMAGE} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + python3 python3-pip python3-dev \ + build-essential gfortran \ + libopenblas-dev liblapack-dev \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir --break-system-packages uv \ + && uv pip install --system --break-system-packages awslabs-cloudwatch-mcp-server + +ENV MCP_COMMAND=awslabs.cloudwatch-mcp-server +ENV MCP_ARGS='[]' diff --git a/docker/mcp-aws-cloudwatch/README.md b/docker/mcp-aws-cloudwatch/README.md new file mode 100644 index 00000000..4b8f57d0 --- /dev/null +++ b/docker/mcp-aws-cloudwatch/README.md @@ -0,0 +1,22 @@ +# AWS CloudWatch MCP Proxy Image + +This image extends the MCP stdio proxy and installs the CloudWatch MCP server. + +## Build + +```bash +docker build -t shipsec/mcp-aws-cloudwatch:latest docker/mcp-aws-cloudwatch +``` + +## Run (example) + +```bash +docker run --rm -p 8080:8080 \ + -e AWS_ACCESS_KEY_ID=... \ + -e AWS_SECRET_ACCESS_KEY=... \ + -e AWS_SESSION_TOKEN=... \ + -e AWS_REGION=us-east-1 \ + shipsec/mcp-aws-cloudwatch:latest +``` + +The proxy exposes MCP on `http://localhost:8080/mcp`. diff --git a/docker/mcp-stdio-proxy/Dockerfile b/docker/mcp-stdio-proxy/Dockerfile new file mode 100644 index 00000000..cfaef59b --- /dev/null +++ b/docker/mcp-stdio-proxy/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-slim + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY server.mjs ./ + +ENV PORT=8080 +EXPOSE 8080 + +CMD ["node", "server.mjs"] diff --git a/docker/mcp-stdio-proxy/README.md b/docker/mcp-stdio-proxy/README.md new file mode 100644 index 00000000..9785829b --- /dev/null +++ b/docker/mcp-stdio-proxy/README.md @@ -0,0 +1,31 @@ +# MCP Stdio Proxy + +This image wraps a stdio-based MCP server and exposes it over Streamable HTTP. + +## Build + +```bash +docker build -t shipsec/mcp-stdio-proxy:latest docker/mcp-stdio-proxy +``` + +## Run + +```bash +docker run --rm -p 8080:8080 \ + -e MCP_COMMAND=uvx \ + -e MCP_ARGS='["awslabs-cloudwatch-mcp-server"]' \ + shipsec/mcp-stdio-proxy:latest +``` + +The proxy will expose MCP on `http://localhost:8080/mcp` and a basic health endpoint at `/health`. + +## Environment + +- `MCP_COMMAND` (required): Command to launch the stdio MCP server. +- `MCP_ARGS` (optional): JSON array or space-delimited list of arguments. +- `PORT` / `MCP_PORT` (optional): Port for the HTTP server (default: 8080). + +## Notes + +- The proxy lists tools once at startup and registers them. Restart the container if tools change. +- Make sure the stdio server binary is present in the image. For third-party tools, build a derived image that installs them. diff --git a/docker/mcp-stdio-proxy/package.json b/docker/mcp-stdio-proxy/package.json new file mode 100644 index 00000000..e766511d --- /dev/null +++ b/docker/mcp-stdio-proxy/package.json @@ -0,0 +1,14 @@ +{ + "name": "mcp-stdio-proxy", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "HTTP proxy for stdio-based MCP servers", + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.3", + "express": "^5.2.1" + } +} diff --git a/docker/mcp-stdio-proxy/server.mjs b/docker/mcp-stdio-proxy/server.mjs new file mode 100644 index 00000000..b465bf6d --- /dev/null +++ b/docker/mcp-stdio-proxy/server.mjs @@ -0,0 +1,122 @@ +import express from 'express'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { + CallToolRequestSchema, + InitializeRequestSchema, + InitializedNotificationSchema, + ListToolsRequestSchema, + LATEST_PROTOCOL_VERSION, +} from '@modelcontextprotocol/sdk/types.js'; + +function parseArgs(raw) { + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) return parsed.map(String); + } catch { + // fall through + } + return raw + .split(' ') + .map((entry) => entry.trim()) + .filter(Boolean); +} + +const command = process.env.MCP_COMMAND; +const args = parseArgs(process.env.MCP_ARGS || ''); +const port = Number.parseInt(process.env.PORT || process.env.MCP_PORT || '8080', 10); + +if (!command) { + console.error('MCP_COMMAND is required to start the stdio MCP server.'); + process.exit(1); +} + +const client = new Client({ name: 'shipsec-mcp-stdio-proxy', version: '1.0.0' }); +const clientTransport = new StdioClientTransport({ + command, + args, +}); + +await client.connect(clientTransport); + +const server = new Server( + { + name: 'shipsec-mcp-stdio-proxy', + version: '1.0.0', + }, + { + capabilities: client.getServerCapabilities() ?? { + tools: { listChanged: false }, + }, + }, +); + +server.setRequestHandler(InitializeRequestSchema, async () => { + return { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: client.getServerCapabilities() ?? {}, + serverInfo: client.getServerVersion() ?? { + name: 'shipsec-mcp-stdio-proxy', + version: '1.0.0', + }, + instructions: client.getInstructions?.(), + }; +}); + +server.setNotificationHandler(InitializedNotificationSchema, () => { + // no-op +}); + +server.setRequestHandler(ListToolsRequestSchema, async () => { + return await client.listTools(); +}); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + return await client.callTool({ + name: request.params.name, + arguments: request.params.arguments ?? {}, + }); +}); + +const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, +}); + +await server.connect(transport); + +const app = express(); +app.use(express.json({ limit: '2mb' })); + +app.all('/mcp', async (req, res) => { + console.log('[mcp-proxy] incoming request', { + method: req.method, + path: req.path, + headers: { + 'mcp-session-id': req.headers['mcp-session-id'], + accept: req.headers['accept'], + 'content-type': req.headers['content-type'], + }, + body: req.body, + }); + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error('[mcp-proxy] Failed to handle MCP request', error); + if (!res.headersSent) { + res.status(500).send('MCP proxy error'); + } + } +}); + +app.get('/health', (_req, res) => { + res.json({ status: 'ok', toolCount: tools.length }); +}); + +app.listen(port, '0.0.0.0', () => { + console.log(`MCP stdio proxy listening on http://0.0.0.0:${port}/mcp`); + console.log(`Proxied MCP command: ${command} ${args.join(' ')}`); +}); diff --git a/e2e-tests/agent-tool-mode.test.ts b/e2e-tests/agent-tool-mode.test.ts new file mode 100644 index 00000000..b85a5a9b --- /dev/null +++ b/e2e-tests/agent-tool-mode.test.ts @@ -0,0 +1,309 @@ +import { describe, test, expect, beforeAll } from 'bun:test'; + +const API_BASE = 'http://127.0.0.1:3211/api/v1'; +const HEADERS = { + 'Content-Type': 'application/json', + 'x-internal-token': 'local-internal-token', +}; + +const runE2E = process.env.RUN_E2E === 'true'; +const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY; +const hasOpenRouterKey = typeof OPENROUTER_KEY === 'string' && OPENROUTER_KEY.length > 0; +const MODEL_ID = 'openai/gpt-5-mini'; + +async function pollRunStatus(runId: string, timeoutMs = 120000): Promise<{ status: string }> { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const res = await fetch(`${API_BASE}/workflows/runs/${runId}/status`, { headers: HEADERS }); + const s = await res.json(); + if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(s.status)) return s; + await new Promise(resolve => setTimeout(resolve, 2000)); + } + throw new Error(`Workflow run ${runId} timed out`); +} + +async function createWorkflow(workflow: any): Promise { + const res = await fetch(`${API_BASE}/workflows`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify(workflow), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to create workflow: ${res.status} ${text}`); + } + const { id } = await res.json(); + return id; +} + +async function runWorkflow(workflowId: string): Promise { + const res = await fetch(`${API_BASE}/workflows/${workflowId}/run`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ inputs: {} }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to run workflow: ${res.status} ${text}`); + } + const { runId } = await res.json(); + return runId; +} + +const e2eDescribe = runE2E ? describe : describe.skip; + +e2eDescribe('Agent Tool Mode Orchestration E2E', () => { + beforeAll(() => { + if (!hasOpenRouterKey) { + throw new Error('Missing OPENROUTER_API_KEY env var for agent E2E tests.'); + } + }); + + test('Agent can run with no tools', async () => { + const workflow = { + name: 'E2E: Agent No Tools', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'agent', + type: 'core.ai.agent', + position: { x: 400, y: 0 }, + data: { + label: 'Security Agent', + config: { + params: { + systemPrompt: 'Say hello.', + }, + inputOverrides: { + userInput: 'Hi', + modelApiKey: OPENROUTER_KEY, + chatModel: { + provider: 'openai', + modelId: MODEL_ID, + baseUrl: 'https://openrouter.ai/api/v1', + apiKey: OPENROUTER_KEY + } + } + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'agent' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + const result = await pollRunStatus(runId); + expect(result.status).toBe('COMPLETED'); + }, 60000); + + test('Agent discovers and calls a tool connected via graph edge', async () => { + const workflow = { + name: 'E2E: Agent Tool Discovery', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'ip_tool', + type: 'core.http.request', + position: { x: 200, y: -50 }, + data: { + label: 'IP Lookup Tool', + config: { + mode: 'tool', + params: { + method: 'GET', + }, + inputOverrides: { + url: 'https://httpbin.org/ip', + } + }, + }, + }, + { + id: 'agent', + type: 'core.ai.agent', + position: { x: 400, y: 0 }, + data: { + label: 'Security Agent', + config: { + params: { + systemPrompt: 'You are a security assistant. Call ip_tool exactly once and report the IP address.', + }, + inputOverrides: { + userInput: 'What is my current IP?', + modelApiKey: OPENROUTER_KEY, + chatModel: { + provider: 'openai', + modelId: MODEL_ID, + baseUrl: 'https://openrouter.ai/api/v1', + apiKey: OPENROUTER_KEY + } + } + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'ip_tool' }, + { id: 'e2', source: 'start', target: 'agent' }, + { id: 't1', source: 'ip_tool', target: 'agent', sourceHandle: 'tools', targetHandle: 'tools' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + console.log(`[Test] Started run ${runId}`); + + const result = await pollRunStatus(runId); + expect(result.status).toBe('COMPLETED'); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + const traceRes = await fetch(`${API_BASE}/workflows/runs/${runId}/trace`, { headers: HEADERS }); + const trace = await traceRes.json(); + + // Verify tool-mode node executed + const toolCompleted = trace.events.find((e: any) => e.nodeId === 'ip_tool' && e.type === 'COMPLETED'); + expect(toolCompleted).toBeDefined(); + + const agentCompleted = trace.events.find((e: any) => e.nodeId === 'agent' && e.type === 'COMPLETED'); + expect(agentCompleted).toBeDefined(); + if (!agentCompleted) { + throw new Error('Agent node did not complete'); + } + const responseText = agentCompleted.outputSummary.responseText; + console.log(`[Test] Agent Response: ${responseText}`); + // Agent should see the tool and either call it or describe it + const hasTool = responseText.toLowerCase().includes('tool'); + const hasIp = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(responseText); + expect(hasTool || hasIp).toBe(true); + }, 120000); + + test('Multiple agents have isolated tool sets based on graph connections', async () => { + const workflow = { + name: 'E2E: Agent Tool Isolation', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'tool_a', + type: 'core.http.request', + position: { x: 200, y: -100 }, + data: { + label: 'Tool A', + config: { + mode: 'tool', + params: { method: 'GET' }, + inputOverrides: { url: 'https://httpbin.org/get?source=tool_a' } + }, + }, + }, + { + id: 'tool_b', + type: 'core.http.request', + position: { x: 200, y: 100 }, + data: { + label: 'Tool B', + config: { + mode: 'tool', + params: { method: 'GET' }, + inputOverrides: { url: 'https://httpbin.org/get?source=tool_b' } + }, + }, + }, + { + id: 'agent_a', + type: 'core.ai.agent', + position: { x: 400, y: -100 }, + data: { + label: 'Agent A', + config: { + params: { systemPrompt: 'List the available tools by name only.' }, + inputOverrides: { + userInput: 'List your available tools.', + modelApiKey: OPENROUTER_KEY, + chatModel: { + provider: 'openai', + modelId: MODEL_ID, + baseUrl: 'https://openrouter.ai/api/v1', + apiKey: OPENROUTER_KEY + } + } + }, + }, + }, + { + id: 'agent_b', + type: 'core.ai.agent', + position: { x: 400, y: 100 }, + data: { + label: 'Agent B', + config: { + params: { systemPrompt: 'List the available tools by name only.' }, + inputOverrides: { + userInput: 'List your available tools.', + modelApiKey: OPENROUTER_KEY, + chatModel: { + provider: 'openai', + modelId: MODEL_ID, + baseUrl: 'https://openrouter.ai/api/v1', + apiKey: OPENROUTER_KEY + } + } + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'tool_a' }, + { id: 'e2', source: 'start', target: 'tool_b' }, + { id: 'e3', source: 'start', target: 'agent_a' }, + { id: 'e4', source: 'start', target: 'agent_b' }, + { id: 't_a', source: 'tool_a', target: 'agent_a', sourceHandle: 'tools', targetHandle: 'tools' }, + { id: 't_b', source: 'tool_b', target: 'agent_b', sourceHandle: 'tools', targetHandle: 'tools' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + const result = await pollRunStatus(runId); + expect(result.status).toBe('COMPLETED'); + + const traceRes = await fetch(`${API_BASE}/workflows/runs/${runId}/trace`, { headers: HEADERS }); + const trace = await traceRes.json(); + + const agentA = trace.events.find((e: any) => e.nodeId === 'agent_a' && e.type === 'COMPLETED'); + const agentB = trace.events.find((e: any) => e.nodeId === 'agent_b' && e.type === 'COMPLETED'); + if (!agentA || !agentB) { + throw new Error('Agent runs did not complete'); + } + + console.log(`[Test] Agent A Response: ${agentA.outputSummary.responseText}`); + console.log(`[Test] Agent B Response: ${agentB.outputSummary.responseText}`); + + expect(agentA.outputSummary.responseText.toLowerCase()).toContain('tool_a'); + expect(agentA.outputSummary.responseText.toLowerCase()).not.toContain('tool_b'); + + expect(agentB.outputSummary.responseText.toLowerCase()).toContain('tool_b'); + expect(agentB.outputSummary.responseText.toLowerCase()).not.toContain('tool_a'); + }, 180000); + +}); diff --git a/e2e-tests/mcp-gateway.test.ts b/e2e-tests/mcp-gateway.test.ts new file mode 100644 index 00000000..b85a2058 --- /dev/null +++ b/e2e-tests/mcp-gateway.test.ts @@ -0,0 +1,200 @@ +import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +const API_BASE = 'http://127.0.0.1:3211/api/v1'; +const HEADERS = { + 'Content-Type': 'application/json', + 'x-internal-token': 'local-internal-token', +}; + +const runE2E = process.env.RUN_E2E === 'true'; + +// Helper function to poll workflow run status +async function pollRunStatus(runId: string, timeoutMs = 60000): Promise<{ status: string }> { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const res = await fetch(`${API_BASE}/workflows/runs/${runId}/status`, { headers: HEADERS }); + const s = await res.json(); + if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(s.status)) return s; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + throw new Error(`Workflow run ${runId} timed out`); +} + +async function createWorkflow(workflow: any): Promise { + const res = await fetch(`${API_BASE}/workflows`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify(workflow), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to create workflow: ${res.status} ${text}`); + } + const { id } = await res.json(); + return id; +} + +async function runWorkflow(workflowId: string): Promise { + const res = await fetch(`${API_BASE}/workflows/${workflowId}/run`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ inputs: {} }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to run workflow: ${res.status} ${text}`); + } + const { runId } = await res.json(); + return runId; +} + +const e2eDescribe = runE2E ? describe : describe.skip; + +e2eDescribe('MCP Gateway E2E', () => { + + test('Agent can connect via SSE, list tools, and call a component tool', async () => { + // 1. Create a workflow that stays alive (using core.flow.delay) + const workflow = { + name: 'Test: MCP Gateway', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'wait', + type: 'core.logic.script', + position: { x: 200, y: 0 }, + data: { + label: 'Wait', + config: { + params: { + code: 'async function script() { console.log("Waiting 60s..."); await new Promise(resolve => setTimeout(resolve, 60000)); return { done: true }; }', + }, + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'wait' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + console.log(`[Test] Started run ${runId}`); + + // 2. Register a mock component tool via Internal API + // We'll use 'core.transform.json' as a simple tool that returns what we give it (if configured) + // Or just 'core.util.echo' if it exists. + // Let's use 'core.util.uuid' if available, or just assume we can register ANY componentId. + // The worker needs to be able to execute it. + // 'core.logic.script' is good for testing logic. + const toolName = 'test_script'; + const componentId = 'core.logic.script'; + + const regRes = await fetch(`${API_BASE}/internal/mcp/register-component`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ + runId, + nodeId: 'manual-tool-reg', + toolName, + componentId, + description: 'Test Script Tool', + inputSchema: { + type: 'object', + properties: {}, + }, + credentials: {}, + parameters: { + code: 'async function script() { return { result: { msg: "Hello from Tool" } }; }', + returns: [{ name: 'result', type: 'json' }] + } + }), + }); + + if (!regRes.ok) { + const text = await regRes.text(); + throw new Error(`Failed to register tool: ${regRes.status} ${text}`); + } + + console.log(`[Test] Registered tool ${toolName}`); + + // 2.5 Generate a session-specific MCP token + const tokenRes = await fetch(`${API_BASE}/internal/mcp/generate-token`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ + runId, + organizationId: 'local-dev', + agentId: 'test-agent' + }), + }); + + if (!tokenRes.ok) { + const text = await tokenRes.text(); + throw new Error(`Failed to generate token: ${tokenRes.status} ${text}`); + } + + const { token } = await tokenRes.json(); + console.log(`[Test] Generated session token: ${token.substring(0, 10)}...`); + + // 3. Connect via MCP Client using the Session Token + const transport = new StreamableHTTPClientTransport(new URL(`${API_BASE}/mcp/gateway`), { + requestInit: { + headers: { + ...HEADERS, + 'Authorization': `Bearer ${token}` + } + } + }); + + const client = new Client( + { name: 'test-agent', version: '1.0.0' }, + { capabilities: {} } + ); + + console.log(`[Test] Connecting to MCP Gateway...`); + await client.connect(transport); + + // 4. List Tools + const tools = await client.listTools(); + console.log(`[Test] Tools listed:`, tools.tools.map(t => t.name)); + + expect(tools.tools).toBeDefined(); + const found = tools.tools.find(t => t.name === toolName); + expect(found).toBeDefined(); + expect(found?.description).toBe('Test Script Tool'); + + // 5. Call Tool + console.log(`[Test] Calling tool ${toolName}...`); + const result = await client.callTool({ + name: toolName, + arguments: {}, + }); + + console.log(`[Test] Tool result:`, result); + + // Expect successful execution + expect(result.content).toBeDefined(); + // content is array of TextContent | ImageContent + // core.logic.script returns output object. McpGateway wraps it in JSON string. + const contentText = (result.content as any)[0].text; + const output = JSON.parse(contentText); + expect(output.result.msg).toBe('Hello from Tool'); + + // Cleanup + await client.close(); + + // Terminate workflow (optional, but good) + // API to cancel? + await fetch(`${API_BASE}/workflows/runs/${runId}/cancel`, { method: 'POST', headers: HEADERS }); + }, 60000); + +}); diff --git a/e2e-tests/mcp-tool-mode.test.ts b/e2e-tests/mcp-tool-mode.test.ts new file mode 100644 index 00000000..3897db46 --- /dev/null +++ b/e2e-tests/mcp-tool-mode.test.ts @@ -0,0 +1,321 @@ +/** + * E2E Tests - MCP Tool Mode + * + * Validates that an MCP server can be started in Docker, registered in the tool registry, + * and cleaned up properly. + */ + +import { describe, test, expect } from 'bun:test'; +import { createMCPClient } from '@ai-sdk/mcp'; + +const API_BASE = 'http://127.0.0.1:3211/api/v1'; +const HEADERS = { + 'Content-Type': 'application/json', + 'x-internal-token': 'local-internal-token', +}; + +const runE2E = process.env.RUN_E2E === 'true'; + +// Helper function to poll workflow run status +async function pollRunStatus(runId: string, timeoutMs = 60000): Promise<{ status: string }> { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const res = await fetch(`${API_BASE}/workflows/runs/${runId}/status`, { headers: HEADERS }); + const s = await res.json(); + if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(s.status)) return s; + await new Promise(resolve => setTimeout(resolve, 1000)); + } + throw new Error(`Workflow run ${runId} timed out`); +} + +async function createWorkflow(workflow: any): Promise { + const res = await fetch(`${API_BASE}/workflows`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify(workflow), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to create workflow: ${res.status} ${text}`); + } + const { id } = await res.json(); + return id; +} + +async function runWorkflow(workflowId: string): Promise { + const res = await fetch(`${API_BASE}/workflows/${workflowId}/run`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ inputs: {} }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to run workflow: ${res.status} ${text}`); + } + const { runId } = await res.json(); + return runId; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function withTimeout(promise: Promise, ms: number, label: string): Promise { + let timeoutId: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`Timeout while waiting for ${label} (${ms}ms)`)); + }, ms); + }); + + return Promise.race([promise, timeoutPromise]).finally(() => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }); +} + +async function readSseSample(response: Response, timeoutMs = 2000): Promise { + if (!response.body) { + return ''; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + try { + const result = await withTimeout(reader.read(), timeoutMs, 'SSE sample'); + if (!result || result.done || !result.value) { + return ''; + } + return decoder.decode(result.value); + } finally { + reader.releaseLock(); + await response.body.cancel().catch(() => undefined); + } +} + +async function generateGatewayToken(runId: string, allowedNodeIds: string[]): Promise { + const res = await fetch(`${API_BASE}/internal/mcp/generate-token`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ + runId, + allowedNodeIds, + }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to generate gateway token: ${res.status} ${text}`); + } + const payload = await res.json(); + if (!isRecord(payload) || typeof payload.token !== 'string') { + throw new Error('Gateway token response missing token'); + } + return payload.token; +} + +async function probeGateway(token: string, label: string): Promise { + const initPayload = { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-11-25', + capabilities: {}, + clientInfo: { name: 'shipsec-e2e', version: '0.1.0' }, + }, + }; + + const initRes = await fetch(`${API_BASE}/mcp/gateway`, { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(initPayload), + }); + + const initSample = await readSseSample(initRes); + const initSessionId = initRes.headers.get('mcp-session-id'); + console.log(` [Debug][${label}] init status=${initRes.status} content-type=${initRes.headers.get('content-type')}`); + console.log(` [Debug][${label}] init sessionId=${initSessionId ?? 'none'}`); + console.log(` [Debug][${label}] init sample=${initSample.trim() || ''}`); + + if (!initSessionId) { + return; + } + + const toolsPayload = { + jsonrpc: '2.0', + id: 2, + method: 'tools/list', + params: {}, + }; + + const toolsRes = await fetch(`${API_BASE}/mcp/gateway`, { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + 'Mcp-Session-Id': initSessionId, + 'Mcp-Protocol-Version': '2025-11-25', + }, + body: JSON.stringify(toolsPayload), + }); + + const toolsSample = await readSseSample(toolsRes); + console.log(` [Debug][${label}] tools status=${toolsRes.status} content-type=${toolsRes.headers.get('content-type')}`); + console.log(` [Debug][${label}] tools sample=${toolsSample.trim() || ''}`); +} + +const e2eDescribe = runE2E ? describe : describe.skip; + +e2eDescribe('MCP Tool Mode E2E', () => { + + test('starts an MCP server in Docker and registers it', async () => { + // We use a simple alpine image as a mock MCP server that just stays alive + // In a real scenario, this would be mcp/server-everything or similar. + const workflow = { + name: 'Test: MCP Docker Registration', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'mcp', + type: 'core.mcp.server', + // Set tool mode + mode: 'tool', + position: { x: 200, y: 0 }, + data: { + label: 'MCP Server', + config: { + params: { + image: 'alpine', + command: ['sh', '-c', 'sleep 3600'], // Just stay alive + port: 8080, + }, + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'mcp' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + const result = await pollRunStatus(runId); + expect(result.status).toBe('COMPLETED'); + + // Verify registration in backend internal API (or check Redis if we had access) + // We can use the internal health/debug endpoint if it exists, + // but for now we'll check if the trace event has the registration info. + const traceRes = await fetch(`${API_BASE}/workflows/runs/${runId}/trace`, { headers: HEADERS }); + const trace = await traceRes.json(); + + // Check for COMPLETED (mapped from NODE_COMPLETED) event for 'mcp' node + console.log(' [Debug] Fetched trace events:', trace.events.map((e: any) => `${e.nodeId}:${e.type}`)); + const mcpEvent = trace.events.find((e: any) => e.nodeId === 'mcp' && e.type === 'COMPLETED'); + expect(mcpEvent).toBeDefined(); + + if (mcpEvent) { + console.log(' [Debug] MCP Node Output:', JSON.stringify(mcpEvent.outputSummary, null, 2)); + expect(mcpEvent.outputSummary.endpoint).toBeDefined(); + expect(mcpEvent.outputSummary.containerId).toBeDefined(); + } + + // Cleanup: Kill the container after the test + const { execSync } = require('child_process'); + try { + console.log(` [Cleanup] Killing container for run ${runId}...`); + execSync(`docker rm -f $(docker ps -aq --filter "label=shipsec.runId=${runId}")`, { stdio: 'inherit' }); + console.log(' [Cleanup] Done.'); + } catch (e: any) { + console.warn(' [Cleanup] Failed to kill container (it might have already been removed):', e.message); + } + }, 120000); + + test('lists tool-mode nodes via MCP gateway (streamable HTTP)', async () => { + const workflow = { + name: 'Test: MCP Gateway Tool Listing', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'ip_tool', + type: 'core.http.request', + position: { x: 200, y: 0 }, + data: { + label: 'IP Lookup Tool', + config: { + mode: 'tool', + params: { + method: 'GET', + }, + inputOverrides: { + url: 'https://httpbin.org/ip', + } + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'ip_tool' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + const result = await pollRunStatus(runId, 120000); + expect(result.status).toBe('COMPLETED'); + + const token = await generateGatewayToken(runId, ['ip_tool']); + let mcpClient: Awaited> | undefined; + + try { + mcpClient = await withTimeout( + createMCPClient({ + transport: { + type: 'http', + url: `${API_BASE}/mcp/gateway`, + headers: { Authorization: `Bearer ${token}` }, + }, + }), + 15000, + 'createMCPClient', + ); + + const tools = await withTimeout(mcpClient.tools(), 15000, 'mcpClient.tools'); + const toolNames = Object.keys(tools); + console.log(' [Debug] Gateway tools:', toolNames); + expect(toolNames).toContain('ip_tool'); + } catch (error) { + console.log(' [Debug] MCP client failed, running manual probe...'); + try { + await probeGateway(token, 'mcpClient.tools'); + } catch (probeError) { + console.log(' [Debug] MCP probe failed:', probeError); + } + throw error; + } finally { + if (mcpClient) { + await mcpClient.close(); + } + } + }, 120000); + +}); diff --git a/e2e-tests/opencode.test.ts b/e2e-tests/opencode.test.ts new file mode 100644 index 00000000..728fdf56 --- /dev/null +++ b/e2e-tests/opencode.test.ts @@ -0,0 +1,201 @@ +import { describe, test, expect, beforeAll } from 'bun:test'; + +const API_BASE = 'http://127.0.0.1:3211/api/v1'; +const HEADERS = { + 'Content-Type': 'application/json', + 'x-internal-token': 'local-internal-token', +}; + +const runE2E = process.env.RUN_E2E === 'true'; +const ZAI_API_KEY = process.env.ZAI_API_KEY; +const hasZaiKey = typeof ZAI_API_KEY === 'string' && ZAI_API_KEY.length > 0; + +async function pollRunStatus(runId: string, timeoutMs = 300000): Promise<{ status: string }> { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const res = await fetch(`${API_BASE}/workflows/runs/${runId}/status`, { headers: HEADERS }); + const s = await res.json(); + if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(s.status)) return s; + await new Promise(resolve => setTimeout(resolve, 5000)); + } + throw new Error(`Workflow run ${runId} timed out`); +} + +async function createWorkflow(workflow: any): Promise { + const res = await fetch(`${API_BASE}/workflows`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify(workflow), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to create workflow: ${res.status} ${text}`); + } + const { id } = await res.json(); + return id; +} + +async function runWorkflow(workflowId: string): Promise { + const res = await fetch(`${API_BASE}/workflows/${workflowId}/run`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ inputs: {} }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to run workflow: ${res.status} ${text}`); + } + const { runId } = await res.json(); + return runId; +} + +const e2eDescribe = runE2E ? describe : describe.skip; + +e2eDescribe('OpenCode Agent E2E', () => { + beforeAll(() => { + if (!hasZaiKey) { + throw new Error('Missing ZAI_API_KEY env var for OpenCode E2E tests.'); + } + }); + + test('OpenCode agent runs with Z.AI GLM-4.7', async () => { + const workflow = { + name: 'E2E: OpenCode Agent Basic', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'opencode', + type: 'core.ai.opencode', + position: { x: 400, y: 0 }, + data: { + label: 'OpenCode Agent', + config: { + params: { + systemPrompt: 'You are a helpful coding assistant. Respond briefly.', + autoApprove: true, + }, + inputOverrides: { + task: 'Write a hello world function in Python. Return only the code.', + model: { + provider: 'zai-coding-plan', + modelId: 'glm-4.7', + apiKey: ZAI_API_KEY, + }, + }, + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'opencode' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + console.log(`[Test] Started OpenCode run ${runId}`); + + const result = await pollRunStatus(runId, 300000); + expect(result.status).toBe('COMPLETED'); + + // Give a moment for logs to flush + await new Promise(resolve => setTimeout(resolve, 2000)); + + const traceRes = await fetch(`${API_BASE}/workflows/runs/${runId}/trace`, { headers: HEADERS }); + const trace = await traceRes.json(); + + const opencodeCompleted = trace.events.find((e: any) => e.nodeId === 'opencode' && e.type === 'COMPLETED'); + expect(opencodeCompleted).toBeDefined(); + + if (opencodeCompleted) { + const report = opencodeCompleted.outputSummary?.report; + console.log(`[Test] OpenCode Report: ${report?.substring(0, 200)}...`); + expect(report).toBeDefined(); + expect(typeof report).toBe('string'); + expect(report.length).toBeGreaterThan(0); + } + }, 300000); + + test('OpenCode agent uses context from input', async () => { + const workflow = { + name: 'E2E: OpenCode Agent with Context', + nodes: [ + { + id: 'start', + type: 'core.workflow.entrypoint', + position: { x: 0, y: 0 }, + data: { label: 'Start', config: { params: { runtimeInputs: [] } } }, + }, + { + id: 'opencode', + type: 'core.ai.opencode', + position: { x: 400, y: 0 }, + data: { + label: 'OpenCode Agent', + config: { + params: { + systemPrompt: 'Analyze the security alert in the context and provide brief recommendations.', + autoApprove: true, + }, + inputOverrides: { + task: 'Review the security alert and provide recommendations.', + context: { + alert: { + type: 'SQL Injection Attempt', + severity: 'high', + source_ip: '192.168.1.100', + payload: "'; DROP TABLE users; --", + }, + }, + model: { + provider: 'zai-coding-plan', + modelId: 'glm-4.7', + apiKey: ZAI_API_KEY, + }, + }, + }, + }, + }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'opencode' }, + ], + }; + + const workflowId = await createWorkflow(workflow); + const runId = await runWorkflow(workflowId); + + console.log(`[Test] Started OpenCode with context run ${runId}`); + + const result = await pollRunStatus(runId, 300000); + expect(result.status).toBe('COMPLETED'); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + const traceRes = await fetch(`${API_BASE}/workflows/runs/${runId}/trace`, { headers: HEADERS }); + const trace = await traceRes.json(); + + const opencodeCompleted = trace.events.find((e: any) => e.nodeId === 'opencode' && e.type === 'COMPLETED'); + expect(opencodeCompleted).toBeDefined(); + + if (opencodeCompleted) { + const report = opencodeCompleted.outputSummary?.report; + console.log(`[Test] Security Analysis Report: ${report?.substring(0, 300)}...`); + expect(report).toBeDefined(); + expect(typeof report).toBe('string'); + // Should mention SQL injection or security-related terms + const reportLower = report.toLowerCase(); + const hasSecurityTerms = reportLower.includes('sql') || + reportLower.includes('injection') || + reportLower.includes('security') || + reportLower.includes('recommend'); + expect(hasSecurityTerms).toBe(true); + } + }, 300000); +}); diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index de7b391f..5f9b78af 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -217,6 +217,7 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) { input: 'text-blue-600 dark:text-blue-400', transform: 'text-orange-600 dark:text-orange-400', ai: 'text-purple-600 dark:text-purple-400', + mcp: 'text-teal-600 dark:text-teal-400', security: 'text-red-600 dark:text-red-400', it_ops: 'text-cyan-600 dark:text-cyan-400', notification: 'text-pink-600 dark:text-pink-400', @@ -302,6 +303,7 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) { 'output', 'notification', 'security', + 'mcp', 'ai', 'transform', 'it_ops', diff --git a/frontend/src/components/workflow/Canvas.tsx b/frontend/src/components/workflow/Canvas.tsx index f4cf02fd..b1563442 100644 --- a/frontend/src/components/workflow/Canvas.tsx +++ b/frontend/src/components/workflow/Canvas.tsx @@ -225,6 +225,7 @@ export function Canvas({ if ( edgeToRemove && edgeToRemove.targetHandle && + edgeToRemove.targetHandle !== 'tools' && (node.data.inputs as Record)?.[edgeToRemove.targetHandle] ) { const targetHandle = edgeToRemove.targetHandle; @@ -329,8 +330,14 @@ export function Canvas({ // Calculate new nodes (if input mapping update is needed) let nextNodes = nodes; - // Update target node's input mapping - if (params.target && params.targetHandle && params.source && params.sourceHandle) { + // Update target node's input mapping (SKIP for 'tools' port) + if ( + params.target && + params.targetHandle && + params.source && + params.sourceHandle && + params.targetHandle !== 'tools' + ) { const targetHandle = params.targetHandle; nextNodes = nodes.map((node) => node.id === params.target diff --git a/frontend/src/components/workflow/ConfigPanel.tsx b/frontend/src/components/workflow/ConfigPanel.tsx index 6115f4ca..4e7968f4 100644 --- a/frontend/src/components/workflow/ConfigPanel.tsx +++ b/frontend/src/components/workflow/ConfigPanel.tsx @@ -1,5 +1,5 @@ import * as LucideIcons from 'lucide-react'; -import { useEffect, useState, useRef, useCallback } from 'react'; +import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { X, ExternalLink, @@ -37,6 +37,7 @@ import type { ComponentType, KeyboardEvent } from 'react'; import { describePortType, inputSupportsManualValue, + isCredentialInput, isListOfTextPort, resolvePortType, } from '@/utils/portUtils'; @@ -69,7 +70,7 @@ function CollapsibleSection({ + )} {/* Delete button (Design Mode only, not Entry Point) */} {/* Embedded Webhook Dialog (Controlled) */} @@ -1107,14 +1193,13 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { )} @@ -1156,7 +1241,7 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { className="text-xs bg-emerald-50 text-emerald-800 border border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-700" > - Completed + {componentCategory === 'mcp' ? 'Server Ready' : 'Completed'} )} {visualState.status === 'error' && ( @@ -1392,6 +1477,20 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { )} + ) : isToolMode ? ( + /* Tool Mode - Special Export Handle */ +
+
+ Tool Export +
+ +
) : null} {/* Parameters Display - Shown above ports for non-entry-point nodes */} @@ -1405,115 +1504,141 @@ export const WorkflowNode = ({ data, selected, id }: NodeProps) => { )} {/* Input Ports (not shown for entry points) */} - {!isEntryPoint && componentInputs.length > 0 && ( -
- {componentInputs.map((input) => { - // Check if this input has a connection - const edges = getEdges(); - const connection = edges.find( - (edge) => edge.target === id && edge.targetHandle === input.id, - ); - const hasConnection = Boolean(connection); - - // Get source node and output info if connected - const manualCandidate = inputOverrides[input.id]; - const manualValueProvided = manualValueProvidedForInput(input, hasConnection); - - let sourceInfo: string | null = null; - if (!manualValueProvided && connection) { - const sourceNode = getNodes().find((n) => n.id === connection.source); - if (sourceNode) { - const sourceComponent = getComponent( - (sourceNode.data as any).componentId ?? (sourceNode.data as any).componentSlug, - ); - if (sourceComponent) { - const sourceOutput = sourceComponent.outputs.find( - (o) => o.id === connection.sourceHandle, + {!isEntryPoint && + componentInputs.length > 0 && + (() => { + const configInputs = componentInputs.filter(isCredentialInput); + + const visibleInputs = isToolMode ? configInputs : componentInputs; + + if (visibleInputs.length === 0 && !isToolMode) return null; + + return ( +
+
+ {visibleInputs.map((input) => { + // Check if this input has a connection + const edges = getEdges(); + const connection = edges.find( + (edge) => edge.target === id && edge.targetHandle === input.id, ); - sourceInfo = sourceOutput?.label || 'Connected'; - } - } - } - - const manualDisplay = - manualValueProvided && - inputSupportsManualValue(input) && - typeof manualCandidate === 'string' - ? manualCandidate.trim() - : ''; - const previewText = - manualDisplay.length > 24 ? `${manualDisplay.slice(0, 24)}…` : manualDisplay; - const handleClassName = cn( - '!w-[10px] !h-[10px] !border-2 !rounded-full', - input.required - ? '!bg-blue-500 !border-blue-500' - : '!bg-background !border-blue-500', - ); - - return ( -
- -
-
{input.label}
- {input.required && !sourceInfo && !manualValueProvided && ( - *required - )} - {manualValueProvided && manualDisplay && ( - - Manual: {previewText} - - )} - {manualValueProvided && !manualDisplay && ( - Manual value - )} - {!manualValueProvided && sourceInfo && ( - - {sourceInfo} - - )} -
+ const hasConnection = Boolean(connection); + + // Get source node and output info if connected + const manualCandidate = inputOverrides[input.id]; + const manualValueProvided = manualValueProvidedForInput(input, hasConnection); + + let sourceInfo: string | null = null; + if (!manualValueProvided && connection) { + const sourceNode = getNodes().find((n) => n.id === connection.source); + if (sourceNode) { + const sourceComponent = getComponent( + (sourceNode.data as any).componentId ?? + (sourceNode.data as any).componentSlug, + ); + if (sourceComponent) { + const sourceOutput = sourceComponent.outputs.find( + (o) => o.id === connection.sourceHandle, + ); + sourceInfo = sourceOutput?.label || 'Connected'; + } + } + } + + const manualDisplay = + manualValueProvided && + inputSupportsManualValue(input) && + typeof manualCandidate === 'string' + ? manualCandidate.trim() + : ''; + const previewText = + manualDisplay.length > 24 ? `${manualDisplay.slice(0, 24)}…` : manualDisplay; + const handleClassName = cn( + '!w-[10px] !h-[10px] !border-2 !rounded-full', + input.required + ? '!bg-blue-500 !border-blue-500' + : '!bg-background !border-blue-500', + input.id === 'tools' && + '!bg-purple-100 !border-purple-500 !rounded-sm !w-[12px] !h-[12px]', + ); + + return ( +
+ +
+
{input.label}
+ {input.required && !sourceInfo && !manualValueProvided && ( + *required + )} + {manualValueProvided && manualDisplay && ( + + Manual: {previewText} + + )} + {manualValueProvided && !manualDisplay && ( + + Manual value + + )} + {!manualValueProvided && sourceInfo && ( + + {sourceInfo} + + )} +
+
+ ); + })}
- ); - })} -
- )} + + {isToolMode && configInputs.length === 0 && ( +
+ No configuration required +
+ )} +
+ ); + })()} {/* Output Ports - Regular only (not shown for entry points - they're in the split layout) */} - {!isEntryPoint && effectiveOutputs.filter((o: any) => !o.isBranching).length > 0 && ( -
- {effectiveOutputs - .filter((o: any) => !o.isBranching) - .map((output: any) => ( -
-
-
{output.label}
+ {/* Also not shown in Tool Mode (replaced by special handle) */} + {!isEntryPoint && + !isToolMode && + effectiveOutputs.filter((o: any) => !o.isBranching).length > 0 && ( +
+ {effectiveOutputs + .filter((o: any) => !o.isBranching) + .map((output: any) => ( +
+
+
{output.label}
+
+
- -
- ))} -
- )} + ))} +
+ )} {/* Branching Outputs - Compact horizontal section */} {!isEntryPoint && diff --git a/frontend/src/schemas/component.ts b/frontend/src/schemas/component.ts index 90e1bd71..566564d1 100644 --- a/frontend/src/schemas/component.ts +++ b/frontend/src/schemas/component.ts @@ -112,6 +112,7 @@ export const ParameterSchema = z.object({ ]), required: z.boolean().optional(), default: z.any().optional(), + exposeToTool: z.boolean().optional(), options: z .array( z.object({ @@ -178,6 +179,7 @@ export const ComponentMetadataSchema = z.object({ 'input', 'transform', 'ai', + 'mcp', 'security', 'it_ops', 'notification', @@ -205,6 +207,17 @@ export const ComponentMetadataSchema = z.object({ outputs: z.array(OutputPortSchema).default([]), parameters: z.array(ParameterSchema).default([]), examples: z.array(z.string()).optional().default([]), + toolSchema: z.any().optional().nullable(), + /** + * Configuration for exposing this component as an agent-callable tool. + */ + agentTool: z + .object({ + enabled: z.boolean(), + toolName: z.string().optional(), + toolDescription: z.string().optional(), + }) + .optional(), }); export type ComponentMetadata = z.infer; diff --git a/frontend/src/store/componentStore.ts b/frontend/src/store/componentStore.ts index d9025d89..4caa4a29 100644 --- a/frontend/src/store/componentStore.ts +++ b/frontend/src/store/componentStore.ts @@ -62,6 +62,8 @@ function buildIndexes(components: any[]) { outputs: component.outputs || [], parameters: component.parameters || [], examples: component.examples || [], + agentTool: component.agentTool || null, + toolSchema: component.toolSchema ?? null, }; byId[metadata.id] = metadata; diff --git a/frontend/src/utils/categoryColors.ts b/frontend/src/utils/categoryColors.ts index ed0b47e2..d0ed4f11 100644 --- a/frontend/src/utils/categoryColors.ts +++ b/frontend/src/utils/categoryColors.ts @@ -5,6 +5,7 @@ export type ComponentCategory = | 'input' | 'transform' | 'ai' + | 'mcp' | 'security' | 'it_ops' | 'notification' @@ -29,6 +30,10 @@ export const CATEGORY_SEPARATOR_COLORS: Record 0) { - sourceOutputs = runtimeInputs.map((input: any) => { - const runtimeType = (input.type || 'text') as string; - const connectionType = runtimeInputTypeToConnectionType(runtimeType); - return { - id: input.id, - label: input.label, - connectionType, - description: input.description || `Runtime input: ${input.label}`, - }; - }); + sourceOutputs = [ + ...sourceOutputs, + ...runtimeInputs.map((input: any) => { + const runtimeType = (input.type || 'text') as string; + const connectionType = runtimeInputTypeToConnectionType(runtimeType); + return { + id: input.id, + label: input.label, + connectionType, + description: input.description || `Runtime input: ${input.label}`, + }; + }), + ]; } } catch (error) { console.error('Failed to parse runtimeInputs for validation:', error); @@ -134,7 +151,10 @@ export function validateConnection( const existingConnection = edges.find( (edge) => edge.target === target && edge.targetHandle === targetHandle, ); - if (existingConnection) { + + // Special case: 'mcp.tool' contract allows many-to-one connections (e.g., many tools to one agent port) + const isToolContract = targetType.kind === 'contract' && targetType.name === 'mcp.tool'; + if (existingConnection && !isToolContract) { return { isValid: false, error: `Input "${targetPort.label}" already has a connection`, @@ -185,8 +205,14 @@ export function getNodeValidationWarnings( // Check for required inputs that are not connected const manualParameters = (node.data.config?.params ?? {}) as Record; const inputOverrides = (node.data.config?.inputOverrides ?? {}) as Record; + const isToolMode = (node.data.config as any)?.isToolMode; component.inputs.forEach((input) => { + // In Tool Mode, skip validation for non-credential inputs + if (isToolMode && !isCredentialInput(input)) { + return; + } + if (input.required) { const hasConnection = edges.some( (edge) => edge.target === node.id && edge.targetHandle === input.id, diff --git a/frontend/src/utils/portUtils.ts b/frontend/src/utils/portUtils.ts index 4b221209..fc46bcf4 100644 --- a/frontend/src/utils/portUtils.ts +++ b/frontend/src/utils/portUtils.ts @@ -150,6 +150,15 @@ export const inputSupportsManualValue = (input: InputPort): boolean => { ); }; +export const isCredentialInput = (input: InputPort): boolean => { + const resolved = resolvePortType(input); + return ( + (resolved.kind === 'contract' && resolved.credential) || + input.editor === 'secret' || + input.id === 'connection' + ); +}; + export const runtimeInputTypeToConnectionType = (type: string): ConnectionType => { const normalized = type.toLowerCase(); diff --git a/openapi.json b/openapi.json index ebf331d8..622141c7 100644 --- a/openapi.json +++ b/openapi.json @@ -2825,6 +2825,20 @@ ], "additionalProperties": true }, + "editor": { + "type": "string", + "enum": [ + "text", + "textarea", + "number", + "boolean", + "select", + "multi-select", + "json", + "secret" + ], + "nullable": true + }, "required": { "type": "boolean" }, @@ -3120,6 +3134,20 @@ ], "additionalProperties": true }, + "editor": { + "type": "string", + "enum": [ + "text", + "textarea", + "number", + "boolean", + "select", + "multi-select", + "json", + "secret" + ], + "nullable": true + }, "required": { "type": "boolean" }, @@ -4677,6 +4705,198 @@ ] } }, + "/api/v1/mcp/gateway": { + "get": { + "operationId": "McpGatewayController_handleGateway_get", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "post": { + "operationId": "McpGatewayController_handleGateway_post", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "put": { + "operationId": "McpGatewayController_handleGateway_put", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "delete": { + "operationId": "McpGatewayController_handleGateway_delete", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "patch": { + "operationId": "McpGatewayController_handleGateway_patch", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "options": { + "operationId": "McpGatewayController_handleGateway_options", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "head": { + "operationId": "McpGatewayController_handleGateway_head", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + }, + "search": { + "operationId": "McpGatewayController_handleGateway_search", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "summary": "Unified MCP Gateway endpoint (Streamable HTTP)", + "tags": [ + "mcp" + ] + } + }, + "/api/v1/internal/mcp/generate-token": { + "post": { + "operationId": "InternalMcpController_generateToken", + "parameters": [], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "InternalMcp" + ] + } + }, + "/api/v1/internal/mcp/register-component": { + "post": { + "operationId": "InternalMcpController_registerComponent", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterComponentToolInput" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "InternalMcp" + ] + } + }, + "/api/v1/internal/mcp/register-remote": { + "post": { + "operationId": "InternalMcpController_registerRemote", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRemoteMcpInput" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "InternalMcp" + ] + } + }, + "/api/v1/internal/mcp/register-local": { + "post": { + "operationId": "InternalMcpController_registerLocal", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterLocalMcpInput" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "InternalMcp" + ] + } + }, "/api/v1/testing/webhooks": { "post": { "operationId": "TestingWebhookController_acceptWebhook", @@ -4835,12 +5055,48 @@ "type": "string" }, "config": { - "default": {}, - "type": "object", - "propertyNames": { - "type": "string" + "default": { + "params": {}, + "inputOverrides": {} }, - "additionalProperties": {} + "type": "object", + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "joinStrategy": { + "type": "string", + "enum": [ + "all", + "any", + "first" + ] + }, + "streamId": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "maxConcurrency": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + } }, "dynamicInputs": { "type": "array", @@ -5004,12 +5260,48 @@ "type": "string" }, "config": { - "default": {}, - "type": "object", - "propertyNames": { - "type": "string" + "default": { + "params": {}, + "inputOverrides": {} }, - "additionalProperties": {} + "type": "object", + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "joinStrategy": { + "type": "string", + "enum": [ + "all", + "any", + "first" + ] + }, + "streamId": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "maxConcurrency": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + } }, "dynamicInputs": { "type": "array", @@ -5204,12 +5496,48 @@ "type": "string" }, "config": { - "default": {}, - "type": "object", - "propertyNames": { - "type": "string" + "default": { + "params": {}, + "inputOverrides": {} }, - "additionalProperties": {} + "type": "object", + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "joinStrategy": { + "type": "string", + "enum": [ + "all", + "any", + "first" + ] + }, + "streamId": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "maxConcurrency": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + } }, "dynamicInputs": { "type": "array", @@ -5356,7 +5684,8 @@ "json", "array", "file", - "boolean" + "boolean", + "secret" ] }, "required": { @@ -5441,12 +5770,48 @@ "type": "string" }, "config": { - "default": {}, - "type": "object", - "propertyNames": { - "type": "string" + "default": { + "params": {}, + "inputOverrides": {} }, - "additionalProperties": {} + "type": "object", + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "joinStrategy": { + "type": "string", + "enum": [ + "all", + "any", + "first" + ] + }, + "streamId": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "maxConcurrency": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + } }, "dynamicInputs": { "type": "array", @@ -5907,10 +6272,24 @@ }, "additionalProperties": { "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + } } }, "trigger": { @@ -6373,8 +6752,8 @@ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" }, "plainKey": { - "description": "The plaintext API key (only returned on creation)", - "type": "string" + "type": "string", + "description": "The plaintext API key (only returned on creation)" } }, "required": [ @@ -6977,10 +7356,24 @@ }, "additionalProperties": { "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + } } } } @@ -7064,10 +7457,24 @@ }, "additionalProperties": { "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} + "properties": { + "params": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "inputOverrides": { + "default": {}, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + } } } } @@ -7763,6 +8170,18 @@ "message", "input" ] + }, + "RegisterComponentToolInput": { + "type": "object", + "properties": {} + }, + "RegisterRemoteMcpInput": { + "type": "object", + "properties": {} + }, + "RegisterLocalMcpInput": { + "type": "object", + "properties": {} } } } diff --git a/package.json b/package.json index 3266df46..0acdc050 100644 --- a/package.json +++ b/package.json @@ -36,14 +36,16 @@ "lint:fix": "bun run lint:frontend --fix && bun run lint:backend --fix && bun run lint:worker --fix" }, "devDependencies": { - "@types/bun": "^1.3.5", - "@types/node": "^24.10.4", - "bun-types": "^1.3.5", + "@ai-sdk/mcp": "^1.0.13", + "@modelcontextprotocol/sdk": "^1.25.3", + "@types/bun": "^1.3.6", + "@types/node": "^24.10.9", + "bun-types": "^1.3.6", "openapi-typescript": "^7.10.1", "pm2": "^6.0.14", "tsx": "^4.21.0", "typescript": "^5.9.3", - "undici": "^7.18.2" + "undici": "^7.19.0" }, "dependencies": { "@googleapis/admin": "^30.2.0", diff --git a/packages/component-sdk/package.json b/packages/component-sdk/package.json index 1d8d2607..eb6df4fa 100644 --- a/packages/component-sdk/package.json +++ b/packages/component-sdk/package.json @@ -25,6 +25,7 @@ "node-pty": "^1.1.0" }, "devDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2", "@types/har-format": "^1.2.16", "@types/node": "^20.16.11", "@typescript-eslint/eslint-plugin": "^8.53.0", diff --git a/packages/component-sdk/src/__tests__/tool-helpers.test.ts b/packages/component-sdk/src/__tests__/tool-helpers.test.ts new file mode 100644 index 00000000..b11a4517 --- /dev/null +++ b/packages/component-sdk/src/__tests__/tool-helpers.test.ts @@ -0,0 +1,327 @@ +import { describe, it, expect } from 'bun:test'; +import { z } from 'zod'; +import { + isAgentCallable, + inferBindingType, + getCredentialInputIds, + getActionInputIds, + getExposedParameterIds, + getToolInputShape, + getToolSchema, + getToolName, + getToolDescription, + getToolMetadata, +} from '../tool-helpers'; +import { inputs, outputs, port, param, parameters } from '../schema-builders'; +import { extractPorts } from '../zod-ports'; +import type { ComponentDefinition, ComponentPortMetadata } from '../types'; + +// Helper to create a minimal component definition +function createComponent( + overrides: Partial> = {} +): ComponentDefinition { + return { + id: 'test.component', + label: 'Test Component', + category: 'security', + runner: { kind: 'inline' }, + inputs: inputs({}), + outputs: outputs({}), + docs: 'Test component documentation', + execute: async () => ({}), + ...overrides, + }; +} + +describe('tool-helpers', () => { + describe('isAgentCallable', () => { + it('returns false when agentTool is not configured', () => { + const component = createComponent(); + expect(isAgentCallable(component)).toBe(false); + }); + + it('returns false when agentTool.enabled is false', () => { + const component = createComponent({ + ui: { + slug: 'test', + version: '1.0.0', + type: 'process', + category: 'security', + agentTool: { enabled: false }, + }, + }); + expect(isAgentCallable(component)).toBe(false); + }); + + it('returns true when agentTool.enabled is true', () => { + const component = createComponent({ + ui: { + slug: 'test', + version: '1.0.0', + type: 'process', + category: 'security', + agentTool: { enabled: true }, + }, + }); + expect(isAgentCallable(component)).toBe(true); + }); + }); + + describe('inferBindingType', () => { + it('returns explicit bindingType when set', () => { + const portWithExplicit: ComponentPortMetadata = { + id: 'test', + label: 'Test', + connectionType: { kind: 'primitive', name: 'text' }, + bindingType: 'config', + }; + expect(inferBindingType(portWithExplicit)).toBe('config'); + }); + + it('infers credential for secret ports', () => { + const secretPort: ComponentPortMetadata = { + id: 'apiKey', + label: 'API Key', + connectionType: { kind: 'primitive', name: 'secret' }, + }; + expect(inferBindingType(secretPort)).toBe('credential'); + }); + + it('infers credential for contract ports with credential flag', () => { + const contractPort: ComponentPortMetadata = { + id: 'awsCreds', + label: 'AWS Credentials', + connectionType: { kind: 'contract', name: 'aws', credential: true }, + }; + expect(inferBindingType(contractPort)).toBe('credential'); + }); + + it('infers action for text ports', () => { + const textPort: ComponentPortMetadata = { + id: 'target', + label: 'Target', + connectionType: { kind: 'primitive', name: 'text' }, + }; + expect(inferBindingType(textPort)).toBe('action'); + }); + + it('infers action for number ports', () => { + const numberPort: ComponentPortMetadata = { + id: 'count', + label: 'Count', + connectionType: { kind: 'primitive', name: 'number' }, + }; + expect(inferBindingType(numberPort)).toBe('action'); + }); + }); + + describe('getCredentialInputIds', () => { + it('returns IDs of credential inputs', () => { + const component = createComponent({ + inputs: inputs({ + apiKey: port(z.string(), { label: 'API Key', editor: 'secret' }), + target: port(z.string(), { label: 'Target' }), + awsCreds: port(z.any(), { label: 'AWS', isCredential: true, schemaName: 'aws', allowAny: true }), + }), + }); + const credIds = getCredentialInputIds(component); + expect(credIds).toEqual(['apiKey', 'awsCreds']); + }); + }); + + describe('getActionInputIds', () => { + it('returns IDs of action inputs', () => { + const component = createComponent({ + inputs: inputs({ + apiKey: port(z.string(), { label: 'API Key', editor: 'secret' }), + target: port(z.string(), { label: 'Target' }), + count: port(z.number(), { label: 'Count' }), + }), + }); + expect(getActionInputIds(component)).toEqual(['target', 'count']); + }); + }); + + describe('getToolSchema', () => { + it('returns schema with action inputs only', () => { + const component = createComponent({ + inputs: inputs({ + apiKey: port(z.string(), { label: 'API Key', editor: 'secret' }), + ipAddress: port(z.string(), { label: 'IP Address', description: 'IP to check' }), + verbose: port(z.boolean().default(false), { label: 'Verbose' }), + }), + }); + + const schema = getToolSchema(component); + + expect(schema.type).toBe('object'); + expect(Object.keys(schema.properties!)).toEqual(['ipAddress', 'verbose']); + expect(schema.properties!.ipAddress).toEqual({ + type: 'string', + description: 'IP to check', + }); + // Zod's toJSONSchema() correctly includes default values - this is better for MCP tools + expect(schema.properties!.verbose).toEqual({ + type: 'boolean', + description: 'Verbose', + default: false, + }); + // Note: Zod's toJSONSchema marks fields with defaults as required + // (the default is applied at runtime, not by JSON Schema) + expect(schema.required).toEqual(['ipAddress', 'verbose']); + }); + + it('includes exposed parameters in tool schema', () => { + const component = createComponent({ + inputs: inputs({ + apiKey: port(z.string(), { label: 'API Key', editor: 'secret' }), + url: port(z.string(), { label: 'URL' }), + }), + parameters: parameters({ + timeoutMs: param(z.number().min(100).default(2000), { + label: 'Timeout (ms)', + editor: 'number', + exposeToTool: true, + }), + apiSecret: param(z.string(), { + label: 'API Secret', + editor: 'secret', + exposeToTool: true, + }), + }), + }); + + const schema = getToolSchema(component); + + expect(Object.keys(schema.properties!)).toEqual(['url', 'timeoutMs']); + expect(schema.properties!.timeoutMs).toMatchObject({ + type: 'number', + default: 2000, + minimum: 100, + description: 'Timeout (ms)', + }); + }); + }); + + describe('getToolInputShape', () => { + it('returns Zod shape with action inputs only', () => { + const component = createComponent({ + inputs: inputs({ + apiKey: port(z.string(), { label: 'API Key', editor: 'secret' }), + url: port(z.string(), { label: 'URL' }), + count: port(z.number().optional(), { label: 'Count' }), + }), + }); + + const shape = getToolInputShape(component); + const shapeKeys = Object.keys(shape); + + expect(shapeKeys).toEqual(['url', 'count']); + + const parsed = z.object(shape).safeParse({ url: 'https://example.com' }); + expect(parsed.success).toBe(true); + }); + + it('includes exposed parameters in input shape', () => { + const component = createComponent({ + inputs: inputs({ + target: port(z.string(), { label: 'Target' }), + }), + parameters: parameters({ + mode: param(z.enum(['fast', 'safe']).default('fast'), { + label: 'Mode', + editor: 'select', + exposeToTool: true, + }), + }), + }); + + const shape = getToolInputShape(component); + expect(Object.keys(shape)).toEqual(['target', 'mode']); + const parsed = z.object(shape).safeParse({ target: 'example.com', mode: 'safe' }); + expect(parsed.success).toBe(true); + }); + }); + + describe('getExposedParameterIds', () => { + it('returns only parameters marked exposeToTool', () => { + const component = createComponent({ + parameters: parameters({ + mode: param(z.string().default('fast'), { + label: 'Mode', + editor: 'select', + exposeToTool: true, + }), + token: param(z.string(), { + label: 'Token', + editor: 'secret', + exposeToTool: true, + }), + }), + }); + + expect(getExposedParameterIds(component)).toEqual(['mode']); + }); + }); + + describe('getToolName', () => { + it('uses agentTool.toolName when specified', () => { + const component = createComponent({ + ui: { + slug: 'abuseipdb-lookup', + version: '1.0.0', + type: 'process', + category: 'security', + agentTool: { + enabled: true, + toolName: 'check_ip_reputation', + }, + }, + }); + expect(getToolName(component)).toBe('check_ip_reputation'); + }); + + it('derives from slug when toolName not specified', () => { + const component = createComponent({ + ui: { + slug: 'abuseipdb-lookup', + version: '1.0.0', + type: 'process', + category: 'security', + agentTool: { enabled: true }, + }, + }); + expect(getToolName(component)).toBe('abuseipdb_lookup'); + }); + }); + + describe('getToolMetadata', () => { + it('returns complete tool metadata for MCP', () => { + const component = createComponent({ + ui: { + slug: 'abuseipdb-lookup', + version: '1.0.0', + type: 'process', + category: 'security', + description: 'Look up IP reputation', + agentTool: { + enabled: true, + toolName: 'check_ip_reputation', + toolDescription: 'Check if an IP address is malicious', + }, + }, + inputs: inputs({ + apiKey: port(z.string(), { label: 'API Key', editor: 'secret' }), + ipAddress: port(z.string(), { label: 'IP Address' }), + }), + }); + + const metadata = getToolMetadata(component); + + expect(metadata.name).toBe('check_ip_reputation'); + expect(metadata.description).toBe('Check if an IP address is malicious'); + expect(metadata.inputSchema.properties).toHaveProperty('ipAddress'); + expect(metadata.inputSchema.properties).not.toHaveProperty('apiKey'); + }); + }); +}); diff --git a/packages/component-sdk/src/context.ts b/packages/component-sdk/src/context.ts index cfdd0c0f..63185b3a 100644 --- a/packages/component-sdk/src/context.ts +++ b/packages/component-sdk/src/context.ts @@ -216,15 +216,9 @@ function createMetadata( metadata?: Partial>, ): ExecutionContextMetadata { const scoped: ExecutionContextMetadata = { + ...metadata, runId, componentRef, - activityId: metadata?.activityId, - attempt: metadata?.attempt, - correlationId: metadata?.correlationId, - streamId: metadata?.streamId, - joinStrategy: metadata?.joinStrategy, - triggeredBy: metadata?.triggeredBy, - failure: metadata?.failure, }; return Object.freeze(scoped); diff --git a/packages/component-sdk/src/index.ts b/packages/component-sdk/src/index.ts index 33671e09..7e914b2a 100644 --- a/packages/component-sdk/src/index.ts +++ b/packages/component-sdk/src/index.ts @@ -19,6 +19,7 @@ export * from './registry'; export * from './context'; export * from './runner'; export * from './errors'; +export * from './tool-helpers'; export * from './http/types'; export * from './http/har-builder'; export * from './http/instrumented-fetch'; diff --git a/packages/component-sdk/src/interfaces.ts b/packages/component-sdk/src/interfaces.ts index e6673e8b..bc82ad46 100644 --- a/packages/component-sdk/src/interfaces.ts +++ b/packages/component-sdk/src/interfaces.ts @@ -141,6 +141,8 @@ export interface ExecutionContextMetadata { joinStrategy?: 'all' | 'any' | 'first'; triggeredBy?: string; failure?: ExecutionFailureMetadata; + connectedToolNodeIds?: string[]; + organizationId?: string | null; } export interface TraceEventData { diff --git a/packages/component-sdk/src/json-schema.ts b/packages/component-sdk/src/json-schema.ts index ece37de7..70c180e5 100644 --- a/packages/component-sdk/src/json-schema.ts +++ b/packages/component-sdk/src/json-schema.ts @@ -52,7 +52,7 @@ function getObjectShape(schema: z.ZodTypeAny): Record { * @param schema - Zod schema to convert * @returns JSON Schema object */ -export function getToolSchema(schema: z.ZodTypeAny): Record { +export function generateJsonSchema(schema: z.ZodTypeAny): Record { const jsonSchema = zodToJsonSchema(schema); return jsonSchema; diff --git a/packages/component-sdk/src/param-meta.ts b/packages/component-sdk/src/param-meta.ts index 99e3e9f9..3ad2c1bb 100644 --- a/packages/component-sdk/src/param-meta.ts +++ b/packages/component-sdk/src/param-meta.ts @@ -17,6 +17,8 @@ export interface ParamMeta { helpText?: string; /** Form field editor type */ editor: ComponentParameterType; + /** Expose this parameter as a tool argument */ + exposeToTool?: boolean; /** Placeholder text */ placeholder?: string; /** Select options */ diff --git a/packages/component-sdk/src/port-meta.ts b/packages/component-sdk/src/port-meta.ts index 851f004a..a905837f 100644 --- a/packages/component-sdk/src/port-meta.ts +++ b/packages/component-sdk/src/port-meta.ts @@ -35,6 +35,8 @@ export interface PortMeta { schemaName?: string; /** Mark this schema as a credential type */ isCredential?: boolean; + /** True if this port should be hidden from the UI (defaults to false) */ + hidden?: boolean; } const METADATA_STORE = new WeakMap(); diff --git a/packages/component-sdk/src/runner.ts b/packages/component-sdk/src/runner.ts index 6a98c38e..00079543 100644 --- a/packages/component-sdk/src/runner.ts +++ b/packages/component-sdk/src/runner.ts @@ -114,6 +114,8 @@ async function runComponentInDocker( '--rm', '-i', '--network', network, + '--label', `shipsec.runId=${context.runId}`, + '--label', `shipsec.nodeRef=${context.componentRef}`, // Mount the directory containing both input and output '-v', `${outputDir}:${CONTAINER_OUTPUT_PATH}`, ]; @@ -130,6 +132,12 @@ async function runComponentInDocker( } } + if (runner.ports) { + for (const [hostPort, containerPort] of Object.entries(runner.ports)) { + dockerArgs.push('-p', `${hostPort}:${containerPort}`); + } + } + for (const [key, value] of Object.entries(env)) { dockerArgs.push('-e', `${key}=${value}`); } @@ -147,7 +155,27 @@ async function runComponentInDocker( const useTerminal = Boolean(context.terminalCollector); let capturedStdout = ''; - + + if (runner.detached) { + // For detached mode, we use -d instead of -i and return the container ID + const detachedArgs = dockerArgs.map(arg => arg === '-i' ? '-d' : arg); + if (!detachedArgs.includes('-d')) { + detachedArgs.splice(1, 0, '-d'); + } + + // In detached mode, we don't want --rm because we want the container to persist for the registry + const persistentArgs = detachedArgs.filter(arg => arg !== '--rm'); + + capturedStdout = await runDockerWithStandardIO(persistentArgs, params, context, timeoutSeconds, runner.stdinJson, true); + + // In detached mode, we return the container ID as part of a specialized output + return { + containerId: capturedStdout.trim(), + status: 'running', + endpoint: env.ENDPOINT || `http://localhost:${env.PORT || 8080}` + } as unknown as O; + } + if (useTerminal) { // Remove -i flag for PTY mode (stdin not needed with TTY) const argsWithoutStdin = dockerArgs.filter(arg => arg !== '-i'); @@ -180,7 +208,7 @@ async function runComponentInDocker( * @param context Execution context for logging */ async function readOutputFromFile( - filePath: string, + filePath: string, stdout: string, context: ExecutionContext ): Promise { @@ -208,7 +236,7 @@ async function readOutputFromFile( // This allows components that just write to stdout to continue working. if (stdout.trim().length > 0) { context.logger.info(`[Docker] No output file found, using stdout fallback (${stdout.length} bytes)`); - + // Try to parse stdout as JSON try { const output = JSON.parse(stdout.trim()); @@ -236,6 +264,7 @@ async function runDockerWithStandardIO( context: ExecutionContext, timeoutSeconds: number, stdinJson?: boolean, + detached?: boolean, ): Promise { const dockerPath = await resolveDockerPath(context); return new Promise((resolve, reject) => { @@ -263,7 +292,7 @@ async function runDockerWithStandardIO( stdoutEmitter(data); const chunk = data.toString(); stdout += chunk; // Capture for fallback - + // Send to log collector (which has chunking support) const logEntry = { runId: context.runId, @@ -274,7 +303,7 @@ async function runDockerWithStandardIO( timestamp: new Date().toISOString(), }; context.logCollector?.(logEntry); - + // NOTE: We intentionally do NOT emit stdout as trace progress events. // Output data is written to /shipsec-output/result.json by the container. // Stdout should only contain logs and progress messages from the component. @@ -337,7 +366,7 @@ async function runDockerWithStandardIO( context.logger.info(`[Docker] Completed successfully`); context.emitProgress('Docker container completed'); - + // Return captured stdout for fallback processing resolve(stdout); }); @@ -411,7 +440,7 @@ async function runDockerWithPty( code: error.code, } : String(error) }; - + console.log('diag', diag); context.logger.warn( `[Docker][PTY] Failed to spawn PTY: ${error instanceof Error ? error.message : String(error)}. Diagnostic: ${JSON.stringify(diag)}`, diff --git a/packages/component-sdk/src/tool-helpers.ts b/packages/component-sdk/src/tool-helpers.ts new file mode 100644 index 00000000..95b48846 --- /dev/null +++ b/packages/component-sdk/src/tool-helpers.ts @@ -0,0 +1,359 @@ +/** + * Helper functions for working with agent-callable components (tool mode). + * + * Uses Zod's built-in toJSONSchema() for accurate type conversion. + * This correctly handles z.any(), z.union(), z.enum(), etc. + */ + +import type { Tool } from '@modelcontextprotocol/sdk/types.js'; +import type { AnySchema, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js'; +import type { ComponentDefinition, ComponentPortMetadata, PortBindingType } from './types'; +import { extractPorts } from './zod-ports'; +import { getParamMeta } from './param-meta'; + +/** + * Tool input schema - matches the MCP SDK's Tool.inputSchema type. + * This is a JSON Schema object with type: 'object'. + */ +// export type ToolInputSchema = Tool['inputSchema']; +export type ToolInputSchema = Tool['inputSchema']; + +/** + * Tool input shape for MCP server registration. + * This is a Zod raw shape (record of schemas). + */ +export type ToolInputShape = ZodRawShapeCompat; + +/** + * Metadata for an agent-callable tool, suitable for MCP tools/list response. + * This is compatible with the MCP SDK's Tool type. + */ +export interface ToolMetadata { + name: string; + description: string; + inputSchema: ToolInputSchema; +} + +/** + * Check if a component is configured as an agent-callable tool. + */ +export function isAgentCallable(component: ComponentDefinition): boolean { + return component.ui?.agentTool?.enabled === true; +} + +/** + * Infer the binding type for a port based on its connection type. + * - secret, contract with credential flag → 'credential' + * - everything else → 'action' + */ +export function inferBindingType(port: ComponentPortMetadata): PortBindingType { + // Explicit binding type takes precedence + if (port.bindingType) { + return port.bindingType; + } + + // Check editor type + if (port.editor === 'secret') { + return 'credential'; + } + + const connectionType = port.connectionType; + + // Secret ports are always credentials + if (connectionType.kind === 'primitive' && connectionType.name === 'secret') { + return 'credential'; + } + + // Contract ports with credential flag are credentials + if (connectionType.kind === 'contract' && connectionType.credential) { + return 'credential'; + } + + // Everything else is an action input + return 'action'; +} + +/** + * Get the IDs of all credential inputs for a component. + * These are inputs that should be pre-bound from the workflow, not exposed to the agent. + */ +export function getCredentialInputIds(component: ComponentDefinition): string[] { + const inputs = extractPorts(component.inputs); + return inputs + .filter(input => inferBindingType(input) === 'credential') + .map(input => input.id); +} + +/** + * Get the IDs of all action inputs for a component. + * These are inputs that the agent provides at runtime. + */ +export function getActionInputIds(component: ComponentDefinition): string[] { + const inputs = extractPorts(component.inputs); + return inputs + .filter(input => inferBindingType(input) === 'action') + .map(input => input.id); +} + +/** + * Get the IDs of parameters explicitly exposed to the agent. + * Secret parameters are always excluded. + */ +export function getExposedParameterIds(component: ComponentDefinition): string[] { + if (!component.parameters) { + return []; + } + + const shape = getObjectShape(component.parameters); + if (!shape) { + return []; + } + + return Object.entries(shape) + .filter(([id, schema]) => { + if (id.startsWith('__')) { + return false; + } + const meta = getParamMeta(schema as any); + if (!meta?.exposeToTool) { + return false; + } + if (meta.editor === 'secret') { + return false; + } + return true; + }) + .map(([id]) => id); +} + +// ============================================================================ +// Schema Generation Helpers +// ============================================================================ + +/** + * Pick specific keys from an object. + */ +function pick, K extends string>( + obj: T, + keys: K[] +): Pick { + const result = {} as Pick; + for (const key of keys) { + if (key in obj) { + (result as Record)[key] = obj[key]; + } + } + return result; +} + +type ZodObjectLike = { + shape?: Record | (() => Record); + _def?: { + shape?: Record | (() => Record); + }; +}; + +function getObjectShape(schema: unknown): Record | null { + if (!schema || typeof schema !== 'object') { + return null; + } + + const objectSchema = schema as ZodObjectLike; + const shape = objectSchema.shape ?? objectSchema._def?.shape; + if (!shape) { + return null; + } + + return typeof shape === 'function' ? shape() : shape; +} + +/** + * Get the Zod raw shape for the action inputs only (inputs exposed to the agent). + * This is used to register tools with the MCP server for input validation. + */ +export function getToolInputShape(component: ComponentDefinition): ToolInputShape { + const shape = getObjectShape(component.inputs); + if (!shape) { + return {}; + } + + const actionInputIds = getActionInputIds(component); + const filtered: ToolInputShape = {}; + + for (const id of actionInputIds) { + const schema = shape[id]; + if (schema) { + filtered[id] = schema; + } + } + + const exposedParamIds = getExposedParameterIds(component); + if (exposedParamIds.length > 0) { + const paramShape = getObjectShape(component.parameters); + if (paramShape) { + for (const id of exposedParamIds) { + if (filtered[id]) { + continue; + } + const schema = paramShape[id]; + if (schema) { + filtered[id] = schema; + } + } + } + } + + return filtered; +} + +/** + * Get the JSON Schema for the action inputs only (inputs exposed to the agent). + * This is used for the MCP tools/list inputSchema field. + * + * Uses Zod's built-in toJSONSchema() for accurate type conversion. + * This correctly handles: + * - z.any() → {} (empty schema = any JSON value) + * - z.union([...]) → { anyOf: [...] } + * - z.enum([...]) → { type: 'string', enum: [...] } + * - z.literal('X') → { type: 'string', const: 'X' } + * - z.record(...) → { type: 'object', additionalProperties: {...} } + */ +export function getToolSchema(component: ComponentDefinition): ToolInputSchema { + const inputsSchema = component.inputs; + const parametersSchema = component.parameters; + + // 1. Generate full JSON Schema using Zod's built-in + const fullSchema = ( + inputsSchema as { toJSONSchema(): Record } + ).toJSONSchema() as { + properties?: Record; + required?: string[]; + }; + + // 2. Get action input IDs (credentials excluded) - reuse existing function! + const actionInputIds = getActionInputIds(component); + const exposedParamIds = getExposedParameterIds(component); + + // 3. Filter properties to only include action inputs + const filteredProperties = pick( + (fullSchema.properties ?? {}) as Record, + actionInputIds + ); + + // 4. Filter required array + const filteredRequired = (fullSchema.required ?? []).filter((id: string) => + actionInputIds.includes(id) + ); + + // 5. Add descriptions from port metadata + const inputs = extractPorts(component.inputs); + for (const input of inputs) { + if (actionInputIds.includes(input.id)) { + const prop = filteredProperties[input.id] as Record | undefined; + if (prop && !prop.description && (input.description ?? input.label)) { + prop.description = input.description ?? input.label; + } + } + } + + // 6. Add exposed parameters (if any) + if (parametersSchema && exposedParamIds.length > 0) { + const paramSchema = ( + parametersSchema as { toJSONSchema(): Record } + ).toJSONSchema() as { + properties?: Record; + required?: string[]; + }; + + const paramProperties = pick( + (paramSchema.properties ?? {}) as Record, + exposedParamIds + ); + + const paramRequired = (paramSchema.required ?? []).filter((id: string) => + exposedParamIds.includes(id) + ); + + // Avoid collisions: inputs take precedence + for (const [key, value] of Object.entries(paramProperties)) { + if (!(key in filteredProperties)) { + filteredProperties[key] = value; + } + } + + const requiredSet = new Set(filteredRequired); + for (const id of paramRequired) { + if (!(id in filteredProperties)) { + continue; + } + requiredSet.add(id); + } + + // Add descriptions from parameter metadata when missing + const paramShape = getObjectShape(parametersSchema); + if (paramShape) { + for (const id of exposedParamIds) { + if (!(id in filteredProperties)) { + continue; + } + const prop = filteredProperties[id] as Record | undefined; + if (!prop || prop.description) { + continue; + } + const meta = getParamMeta(paramShape[id] as any); + if (meta?.description ?? meta?.label) { + prop.description = meta?.description ?? meta?.label; + } + } + } + + return { + type: 'object' as const, + properties: filteredProperties as Record, + required: Array.from(requiredSet), + }; + } + + return { + type: 'object' as const, + properties: filteredProperties as Record, + required: filteredRequired, + }; +} + +/** + * Get the tool name for a component. + * Uses agentTool.toolName if specified, otherwise derives from component slug. + */ +export function getToolName(component: ComponentDefinition): string { + if (component.ui?.agentTool?.toolName) { + return component.ui.agentTool.toolName; + } + + // Derive from slug: 'abuseipdb-lookup' → 'abuseipdb_lookup' + const slug = component.ui?.slug ?? component.id; + return slug.replace(/-/g, '_').replace(/\./g, '_'); +} + +/** + * Get the tool description for a component. + * Uses agentTool.toolDescription if specified, otherwise uses component docs/description. + */ +export function getToolDescription(component: ComponentDefinition): string { + if (component.ui?.agentTool?.toolDescription) { + return component.ui.agentTool.toolDescription; + } + + return component.ui?.description ?? component.docs ?? component.label; +} + +/** + * Get complete tool metadata for MCP tools/list response. + */ +export function getToolMetadata(component: ComponentDefinition): ToolMetadata { + return { + name: getToolName(component), + description: getToolDescription(component), + inputSchema: getToolSchema(component), + }; +} diff --git a/packages/component-sdk/src/types.ts b/packages/component-sdk/src/types.ts index 0cbad769..463dffd0 100644 --- a/packages/component-sdk/src/types.ts +++ b/packages/component-sdk/src/types.ts @@ -36,6 +36,8 @@ export interface DockerRunnerConfig { }>; // Optional volume mounts timeoutSeconds?: number; stdinJson?: boolean; // Whether to write params as JSON to container's stdin (default: true) + detached?: boolean; // If true, start container and return immediately without waiting for exit + ports?: Record; // Port mapping host -> container } @@ -251,6 +253,8 @@ export interface ComponentPortMetadata { isBranching?: boolean; /** Custom color for branching ports: 'green' | 'red' | 'amber' | 'blue' | 'purple' | 'slate' */ branchColor?: 'green' | 'red' | 'amber' | 'blue' | 'purple' | 'slate'; + /** True if this port should be hidden from the UI */ + hidden?: boolean; } export type ComponentParameterType = @@ -278,6 +282,7 @@ export interface ComponentParameterMetadata { type: ComponentParameterType; required?: boolean; default?: unknown; + exposeToTool?: boolean; placeholder?: string; description?: string; helpText?: string; @@ -302,6 +307,7 @@ export type ComponentCategory = | 'input' | 'transform' | 'ai' + | 'mcp' | 'security' | 'it_ops' | 'notification' @@ -315,6 +321,25 @@ export type ComponentUiType = | 'process' | 'output'; +/** + * Configuration for exposing a component as an agent-callable tool. + */ +export interface AgentToolConfig { + /** Whether this component can be used as an agent tool */ + enabled: boolean; + /** + * Tool name exposed to the agent. Defaults to component slug with underscores. + * Should be descriptive and follow snake_case convention. + * @example 'check_ip_reputation', 'query_cloudtrail' + */ + toolName?: string; + /** + * Description of what the tool does, shown to the agent. + * Should clearly explain the tool's purpose and when to use it. + */ + toolDescription?: string; +} + export interface ComponentUiMetadata { slug: string; version: string; @@ -332,6 +357,12 @@ export interface ComponentUiMetadata { examples?: string[]; /** UI-only component - should not be included in workflow execution */ uiOnly?: boolean; + /** + * Configuration for exposing this component as an agent-callable tool. + * When enabled, the component can be used in tool mode within workflows, + * allowing AI agents to invoke it via the MCP gateway. + */ + agentTool?: AgentToolConfig; } export interface ExecutionContext { diff --git a/packages/component-sdk/src/zod-parameters.ts b/packages/component-sdk/src/zod-parameters.ts index 4c6088d7..1f194814 100644 --- a/packages/component-sdk/src/zod-parameters.ts +++ b/packages/component-sdk/src/zod-parameters.ts @@ -48,6 +48,7 @@ export function extractParameters(schema: z.ZodTypeAny): ComponentParameterMetad type: paramMeta.editor, required, default: defaultValue, + exposeToTool: paramMeta.exposeToTool, placeholder: paramMeta.placeholder, description: paramMeta.description, helpText: paramMeta.helpText, diff --git a/packages/component-sdk/src/zod-ports.ts b/packages/component-sdk/src/zod-ports.ts index 8aea54a6..edec932c 100644 --- a/packages/component-sdk/src/zod-ports.ts +++ b/packages/component-sdk/src/zod-ports.ts @@ -14,7 +14,7 @@ export interface ValidationResult { error?: string; } -type ZodDef = { type?: string; typeName?: string; [key: string]: any }; +type ZodDef = { type?: string; typeName?: string;[key: string]: any }; const LEGACY_TYPE_MAP: Record = { ZodString: 'string', @@ -131,6 +131,7 @@ export function extractPorts( valuePriority: metadata.valuePriority, isBranching: metadata.isBranching, branchColor: metadata.branchColor, + hidden: metadata.hidden, }); } @@ -160,6 +161,15 @@ export function deriveConnectionType(schema: z.ZodTypeAny): ConnectionType { const unwrapped = unwrapEffects(schema); const defType = getSchemaType(unwrapped); + // Check for schemaName (named contract) - takes precedence over generic types + if (portMeta?.schemaName) { + return { + kind: 'contract', + name: portMeta.schemaName, + credential: portMeta.isCredential, + }; + } + // Handle explicit any/unknown with allowAny flag if (defType === 'any' || defType === 'unknown') { if (portMeta?.allowAny) { @@ -170,15 +180,6 @@ export function deriveConnectionType(schema: z.ZodTypeAny): ConnectionType { ); } - // Check for schemaName (named contract) - if (portMeta?.schemaName) { - return { - kind: 'contract', - name: portMeta.schemaName, - credential: portMeta.isCredential, - }; - } - const editorConnectionType = editorToConnectionType(portMeta?.editor); if (editorConnectionType) { return editorConnectionType; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 3cd22bd6..9d29fa7f 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -44,6 +44,14 @@ const buildLlmProviderSchema = () => baseUrl: z.string().optional(), headers: z.record(z.string(), z.string()).optional(), }), + z.object({ + provider: z.literal('zai-coding-plan'), + modelId: z.string(), + apiKey: z.string().optional(), + apiKeySecretId: z.string().optional(), + baseUrl: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), + }), ]); export const LLMProviderSchema = () => diff --git a/scripts/view-debug-logs.ts b/scripts/view-debug-logs.ts new file mode 100644 index 00000000..796ac3dd --- /dev/null +++ b/scripts/view-debug-logs.ts @@ -0,0 +1,80 @@ +#!/usr/bin/env bun +import * as fs from 'fs'; + +const DEBUG_LOG_FILE = '/tmp/shipsec-debug/worker.log'; + +const args = process.argv.slice(2); +const filter = args[0]; +const lineCount = parseInt(args[1]) || 50; + +if (!fs.existsSync(DEBUG_LOG_FILE)) { + console.log('No debug logs found at', DEBUG_LOG_FILE); + process.exit(0); +} + +const content = fs.readFileSync(DEBUG_LOG_FILE, 'utf-8'); +const lines = content.split('\n').filter(l => l.trim()); + +let filtered = lines; + +if (filter) { + if (filter.includes(':')) { + const [key, value] = filter.split(':'); + filtered = lines.filter(line => { + try { + const json = JSON.parse(line); + return json[key]?.toString().includes(value); + } catch { + return false; + } + }); + } else { + // Filter by substring in message + filtered = lines.filter(line => { + try { + const json = JSON.parse(line); + return json.message?.toLowerCase().includes(filter.toLowerCase()) || + json.context?.toLowerCase().includes(filter.toLowerCase()); + } catch { + return false; + } + }); + } +} + +// Get last N lines +const recent = filtered.slice(-lineCount); + +console.log(`\n📋 Recent Debug Logs (${recent.length} lines)\n`); +console.log(`Log file: ${DEBUG_LOG_FILE}`); +if (filter) { + console.log(`Filter: ${filter}`); +} +console.log('-'.repeat(100)); + +recent.forEach(line => { + try { + const entry = JSON.parse(line); + const time = entry.timestamp?.split('T')[1]?.split('.')[0] || '??:??:??'; + const level = entry.level?.toUpperCase().padEnd(5) || 'INFO '; + const context = entry.context?.padEnd(30) || 'unknown'.padEnd(30); + const msg = entry.message || ''; + + console.log(`${time} [${level}] ${context} ${msg}`); + + if (entry.data && typeof entry.data === 'object' && Object.keys(entry.data).length > 0) { + console.log(` └─ Data: ${JSON.stringify(entry.data)}`); + } + } catch { + // Malformed line, skip + } +}); + +console.log('-'.repeat(100)); +console.log(`\nTotal logs in file: ${lines.length}`); +console.log(`Shown: ${recent.length}`); +console.log(`\nUsage: bun scripts/view-debug-logs.ts [filter] [line-count]`); +console.log(`Examples:`); +console.log(` bun scripts/view-debug-logs.ts # Last 50 lines`); +console.log(` bun scripts/view-debug-logs.ts "tool discovery" 100 # Search for "tool discovery", show 100 lines`); +console.log(` bun scripts/view-debug-logs.ts "level:error" # Show only errors\n`); diff --git a/worker/package.json b/worker/package.json index 0657c102..317184f9 100644 --- a/worker/package.json +++ b/worker/package.json @@ -19,57 +19,58 @@ "test": "bun test" }, "dependencies": { - "@ai-sdk/google": "^3.0.0-beta.27", - "@ai-sdk/openai": "^3.0.0-beta.34", - "@aws-sdk/client-s3": "^3.642.0", + "@ai-sdk/google": "^3.0.13", + "@ai-sdk/mcp": "^1.0.13", + "@ai-sdk/openai": "^3.0.18", + "@aws-sdk/client-s3": "^3.975.0", "@googleapis/admin": "^29.0.0", - "@grpc/grpc-js": "^1.14.0", + "@grpc/grpc-js": "^1.14.3", "@okta/okta-sdk-nodejs": "^7.3.0", "@shipsec/component-sdk": "*", "@shipsec/contracts": "*", "@shipsec/shared": "*", - "@temporalio/activity": "^1.13.1", - "@temporalio/client": "^1.11.3", - "@temporalio/common": "^1.13.1", - "@temporalio/worker": "^1.11.3", - "@temporalio/workflow": "^1.11.3", + "@temporalio/activity": "^1.14.1", + "@temporalio/client": "^1.14.1", + "@temporalio/common": "^1.14.1", + "@temporalio/worker": "^1.14.1", + "@temporalio/workflow": "^1.14.1", "adm-zip": "^0.5.16", - "ai": "^6.0.0-beta.68", + "ai": "^6.0.49", "dotenv": "^17.2.3", - "drizzle-orm": "^0.44.6", - "google-auth-library": "^10.4.2", - "ioredis": "^5.4.1", + "drizzle-orm": "^0.44.7", + "google-auth-library": "^10.5.0", + "ioredis": "^5.9.2", "js-yaml": "^4.1.1", "kafkajs": "^2.2.4", "long": "^5.3.2", "minio": "^8.0.6", "node-pty": "^1.1.0", - "pg": "^8.16.3", - "zod": "^4.1.12" + "pg": "^8.17.2", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.39.2", "@types/adm-zip": "^0.5.7", "@types/js-yaml": "^4.0.9", "@types/minio": "^7.1.1", - "@types/node": "^20.16.11", - "@types/pg": "^8.15.5", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", - "bun-types": "^1.2.23", + "@types/node": "^25.1.0", + "@types/pg": "^8.16.0", + "@typescript-eslint/eslint-plugin": "^8.53.1", + "@typescript-eslint/parser": "^8.53.1", + "bun-types": "^1.3.6", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", - "globals": "^17.0.0", - "prettier": "^3.8.0", - "ts-loader": "^9.5.1", - "tsx": "^4.20.6", - "typescript": "^5.6.3", - "typescript-eslint": "^8.53.0" + "globals": "^17.1.0", + "prettier": "^3.8.1", + "ts-loader": "^9.5.4", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1" }, "optionalDependencies": { - "@swc/core": "^1.13.21", - "@swc/core-darwin-arm64": "^1.13.21", + "@swc/core": "^1.15.10", + "@swc/core-darwin-arm64": "^1.15.10", "@swc/core-darwin-universal": "^1.13.21", "@swc/core-wasm32-wasi": "^1.13.21" } diff --git a/worker/src/components/ai/__tests__/ai-agent.test.ts b/worker/src/components/ai/__tests__/ai-agent.test.ts index 0e09ab0e..6d1864a1 100644 --- a/worker/src/components/ai/__tests__/ai-agent.test.ts +++ b/worker/src/components/ai/__tests__/ai-agent.test.ts @@ -1,60 +1,52 @@ import { beforeAll, beforeEach, describe, expect, test, vi } from 'bun:test'; -import '../../index'; +import type { GenerateTextResult, LanguageModelUsage, Output as AIOutput, ToolSet } from 'ai'; import type { ExecutionContext } from '@shipsec/component-sdk'; import { componentRegistry, runComponentWithRunner } from '@shipsec/component-sdk'; -import type { - ToolLoopAgentClass, - StepCountIsFn, - ToolFn, - CreateOpenAIFn, - CreateGoogleGenerativeAIFn, - GenerateObjectFn, - GenerateTextFn, -} from '../ai-agent'; +import type { AiAgentInput, AiAgentOutput } from '../ai-agent'; -const makeAgentResult = (overrides: Record = {}) => ({ - text: 'Agent final answer', - steps: [ - { - text: 'Reasoning without tools', - finishReason: 'stop', - toolCalls: [], - toolResults: [], - }, - ], - toolResults: [], - usage: { - promptTokens: 12, - completionTokens: 24, - totalTokens: 36, - }, - totalUsage: { - promptTokens: 12, - completionTokens: 24, - totalTokens: 36, - }, - finishReason: 'stop', - content: [], - reasoning: [], - reasoningText: undefined, - files: [], - sources: [], - toolCalls: [], - staticToolCalls: [], - dynamicToolCalls: [], - staticToolResults: [], - dynamicToolResults: [], - warnings: undefined, - request: {}, - response: { messages: [] }, - providerMetadata: undefined, - ...overrides, -}); +const stepCountIsMock = vi.fn((limit: number) => ({ type: 'step-count', limit })); +const createOpenAIMock = vi.fn(() => (modelId: string) => ({ provider: 'openai', modelId })); +const createGoogleGenerativeAIMock = vi.fn(() => (modelId: string) => ({ + provider: 'gemini', + modelId, +})); +const createMCPClientMock = vi.fn(); + +let toolLoopAgentSettings: unknown; +let lastGenerateMessages: unknown; +type GenerationResult = GenerateTextResult>; +let nextGenerateResult = createGenerationResult(); + +class MockToolLoopAgent { + settings: unknown; + + constructor(settings: unknown) { + this.settings = settings; + toolLoopAgentSettings = settings; + } -const OPENAI_SECRET_ID = 'secret-openai'; -const GEMINI_SECRET_ID = 'secret-gemini'; + async generate({ messages }: { messages: unknown }) { + lastGenerateMessages = messages; + return nextGenerateResult; + } +} -const workflowContext: ExecutionContext = { +vi.mock('ai', () => ({ + ToolLoopAgent: MockToolLoopAgent, + generateText: vi.fn(), + stepCountIs: stepCountIsMock, +})); +vi.mock('@ai-sdk/openai', () => ({ + createOpenAI: createOpenAIMock, +})); +vi.mock('@ai-sdk/google', () => ({ + createGoogleGenerativeAI: createGoogleGenerativeAIMock, +})); +vi.mock('@ai-sdk/mcp', () => ({ + createMCPClient: createMCPClientMock, +})); + +const baseContext: ExecutionContext = { runId: 'test-run', componentRef: 'core.ai.agent', logger: { @@ -64,9 +56,6 @@ const workflowContext: ExecutionContext = { warn: () => {}, }, emitProgress: () => {}, - agentTracePublisher: { - publish: () => {}, - }, metadata: { runId: 'test-run', componentRef: 'core.ai.agent', @@ -75,704 +64,264 @@ const workflowContext: ExecutionContext = { fetch: async () => new Response(), toCurl: () => '', }, - secrets: { - async get(id) { - if (id === OPENAI_SECRET_ID) { - return { value: 'sk-openai-from-secret', version: 1 }; - } - if (id === GEMINI_SECRET_ID) { - return { value: 'gm-gemini-from-secret', version: 1 }; - } - return null; - }, - async list() { - return [OPENAI_SECRET_ID, GEMINI_SECRET_ID]; - }, - }, }; -// Create mocks for the AI dependencies -const createdTools: Record[] = []; -const stepCountIsMock = vi.fn((limit: number) => ({ type: 'step-count', limit })); -interface AgentGenerateArgs { - messages: { role: string; content: string }[]; +function createUsage(overrides: Partial = {}): LanguageModelUsage { + return { + inputTokens: 1, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokens: 1, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, + totalTokens: 2, + ...overrides, + }; } -let agentGenerateMock = vi.fn((_args: AgentGenerateArgs) => {}); -const toolLoopAgentConstructorMock = vi.fn((settings: any) => settings); -let nextAgentResult = makeAgentResult(); -let toolLoopAgentGenerateImpl: ((instance: any, args: any) => Promise) | null = null; - -// Mock ToolLoopAgent class -class MockToolLoopAgent { - settings: any; - tools: Record; - id: string | undefined; - version = 'agent-v1'; - constructor(settings: any) { - this.settings = settings; - this.tools = settings?.tools ?? {}; - this.id = settings?.id; - toolLoopAgentConstructorMock(settings); - } +function createGenerationResult(overrides: Partial = {}): GenerationResult { + const usage = createUsage(); + return { + content: [], + text: 'Agent final answer', + reasoning: [], + reasoningText: undefined, + files: [], + sources: [], + toolCalls: [], + staticToolCalls: [], + dynamicToolCalls: [], + toolResults: [], + staticToolResults: [], + dynamicToolResults: [], + finishReason: 'stop', + rawFinishReason: 'stop', + usage, + totalUsage: usage, + warnings: undefined, + request: {}, + response: { + id: 'resp-1', + timestamp: new Date(), + modelId: 'mock-model', + messages: [], + }, + providerMetadata: undefined, + steps: [], + experimental_output: '', + output: '', + ...overrides, + }; +} - async generate(args: any) { - agentGenerateMock(args); - if (toolLoopAgentGenerateImpl) { - return await toolLoopAgentGenerateImpl(this, args); - } - return nextAgentResult; - } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} - async stream() { - throw new Error('stream not implemented in test mock'); +function expectRecord(value: unknown, label: string): Record { + if (!isRecord(value)) { + throw new Error(`Expected ${label} to be an object`); } + return value; } -const openAiFactoryMock = vi.fn( - (options: { apiKey: string; baseURL?: string }) => (model: string) => ({ - provider: 'openai', - modelId: model, - options, - }), -); - -const googleFactoryMock = vi.fn( - (options: { apiKey?: string; baseURL?: string }) => (model: string) => ({ - provider: 'gemini', - modelId: model, - options, - }), -); +beforeEach(() => { + toolLoopAgentSettings = undefined; + lastGenerateMessages = undefined; + nextGenerateResult = createGenerationResult(); + stepCountIsMock.mockClear(); + createOpenAIMock.mockClear(); + createGoogleGenerativeAIMock.mockClear(); + createMCPClientMock.mockClear(); + process.env.INTERNAL_SERVICE_TOKEN = 'internal-token'; +}); beforeAll(async () => { await import('../../index'); }); -beforeEach(() => { - createdTools.length = 0; - agentGenerateMock = vi.fn((_args: AgentGenerateArgs) => {}); - toolLoopAgentConstructorMock.mockReset(); - stepCountIsMock.mockReset(); - nextAgentResult = makeAgentResult(); - toolLoopAgentGenerateImpl = null; -}); - -describe('core.ai.agent component', () => { - test('runs with OpenAI provider and updates conversation state', async () => { - const component = componentRegistry.get('core.ai.agent'); +describe('core.ai.agent (refactor)', () => { + test('runs without tool discovery when no connected tools', async () => { + const component = componentRegistry.get('core.ai.agent'); expect(component).toBeDefined(); - nextAgentResult = makeAgentResult(); - - const executePayload = { - inputs: { - userInput: 'Summarise the status update.', - conversationState: { - sessionId: 'session-1', - messages: [], - toolInvocations: [], - }, - chatModel: { - provider: 'openai' as const, - modelId: 'gpt-4o-mini', - }, - modelApiKey: 'sk-openai-from-secret', - }, - params: { - systemPrompt: 'You are a concise assistant.', - temperature: 0.2, - maxTokens: 256, - memorySize: 8, - stepLimit: 2, - }, - }; + nextGenerateResult = createGenerationResult({ text: 'Hello agent' }); - const result = (await runComponentWithRunner( + const result = await runComponentWithRunner( component!.runner, - (payload: any, context: any) => - (component!.execute as any)(payload, context, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => { - createdTools.push(definition); - return definition; - }) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - }), - executePayload, - workflowContext, - )) as any; - - expect(toolLoopAgentConstructorMock).toHaveBeenCalledTimes(1); - const agentSettings = toolLoopAgentConstructorMock.mock.calls[0][0]; - expect(agentSettings).toMatchObject({ - instructions: 'You are a concise assistant.', - temperature: 0.2, - maxOutputTokens: 256, - }); - expect(agentSettings.model).toMatchObject({ - provider: 'openai', - modelId: 'gpt-4o-mini', - options: expect.objectContaining({ apiKey: 'sk-openai-from-secret' }), - }); - - expect(agentGenerateMock).toHaveBeenCalledTimes(1); - const callArgs = agentGenerateMock.mock.calls[0][0]; - expect(callArgs.messages.at(-1)).toEqual({ - role: 'user', - content: 'Summarise the status update.', - }); - - expect(result.responseText).toBe('Agent final answer'); - expect(result.conversationState.sessionId).toBe('session-1'); - const assistantMessage = result.conversationState.messages.at(-1); - expect(assistantMessage).toEqual({ - role: 'assistant', - content: 'Agent final answer', - }); - expect(result.toolInvocations).toHaveLength(0); - expect(result.reasoningTrace).toHaveLength(1); - expect(typeof result.agentRunId).toBe('string'); - expect(result.agentRunId.length).toBeGreaterThan(0); - }); - - test('wires MCP tool output into reasoning trace for Gemini provider', async () => { - const fetchMock = vi.fn(async () => { - return new Response(JSON.stringify({ answer: 'Evidence' }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - }); - - // Create a context with the mock fetch - const contextWithMockFetch: ExecutionContext = { - ...workflowContext, - http: { - fetch: fetchMock as unknown as ExecutionContext['http']['fetch'], - toCurl: () => '', - }, - }; - - toolLoopAgentGenerateImpl = async (instance) => { - const _toolResult = await instance.settings.tools.call_mcp_tool.execute({ - toolName: 'lookup', - arguments: { question: 'Lookup reference' }, - }); - - const component = componentRegistry.get('core.ai.agent'); - expect(component).toBeDefined(); - - const executePayload = { + component!.execute, + { inputs: { - userInput: 'What does the MCP tool return?', + userInput: 'Hi', conversationState: undefined, chatModel: { - provider: 'gemini' as const, - modelId: 'gemini-2.5-flash', - baseUrl: 'https://generativelanguage.googleapis.com/v1beta', + provider: 'openai', + modelId: 'gpt-4o-mini', }, - modelApiKey: 'gm-gemini-from-secret', - mcpTools: [ - { - id: 'call-mcp', - title: 'Lookup', - endpoint: 'https://mcp.test/api', - metadata: { - toolName: 'call_mcp_tool', - }, - }, - ], + modelApiKey: 'sk-test', }, params: { - systemPrompt: '', - temperature: 0.6, - maxTokens: 512, - memorySize: 6, - stepLimit: 3, + systemPrompt: 'Say hello', + temperature: 0.2, + maxTokens: 128, + memorySize: 4, + stepLimit: 2, }, - }; + }, + baseContext, + ); - const _result = (await runComponentWithRunner( - component!.runner, - (payload: any, context: any) => - (component!.execute as any)(payload, context, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => { - createdTools.push(definition); - return definition; - }) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - }), - executePayload, - workflowContext, - )) as any; + expect(result.responseText).toBe('Hello agent'); + expect(createMCPClientMock).not.toHaveBeenCalled(); - const params = { - userInput: 'What does the MCP tool return?', - conversationState: undefined, - chatModel: { - provider: 'gemini', - modelId: 'gemini-2.5-flash', - baseUrl: 'https://generativelanguage.googleapis.com/v1beta', - }, - modelApiKey: 'gm-gemini-from-secret', - mcpTools: [ - { - id: 'call-mcp', - title: 'Lookup', - endpoint: 'https://mcp.test/api', - metadata: { - toolName: 'call_mcp_tool', - }, - }, - ], - systemPrompt: '', - temperature: 0.6, - maxTokens: 512, - memorySize: 6, - stepLimit: 3, - }; + const settings = expectRecord(toolLoopAgentSettings, 'agent settings'); + expect(settings.tools).toBeUndefined(); + expect(settings.temperature).toBe(0.2); + expect(stepCountIsMock).toHaveBeenCalledWith(2); - const result2 = (await runComponentWithRunner( - component!.runner, - (params: any, context: any) => - (component!.execute as any)(params, context, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => { - createdTools.push(definition); - return definition; - }) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - }), - params, - contextWithMockFetch, - )) as any; - - expect(createdTools).toHaveLength(1); - expect(stepCountIsMock).toHaveBeenCalledWith(3); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(result2.toolInvocations).toHaveLength(1); - expect(result2.toolInvocations[0]).toMatchObject({ - toolName: 'call_mcp_tool', - result: { answer: 'Evidence' }, - }); - expect(result2.reasoningTrace[0]).toMatchObject({ - thought: 'Consulting MCP', - }); - const toolMessage = result2.conversationState.messages.find( - (msg: any) => msg.role === 'tool', - ); - expect(toolMessage?.content).toMatchObject({ - toolName: 'call_mcp_tool', - result: { answer: 'Evidence' }, - }); - const agentSettings = toolLoopAgentConstructorMock.mock.calls[0][0]; - expect(agentSettings.model).toMatchObject({ - provider: 'gemini', - modelId: 'gemini-2.5-flash', - }); - expect(result2.responseText).toBe('Final resolved answer'); - expect(result2.agentRunId).toBeTruthy(); - }; + const messages = Array.isArray(lastGenerateMessages) ? lastGenerateMessages : []; + expect(messages.at(-1)).toMatchObject({ + role: 'user', + content: 'Hi', + }); }); - test('emits agent trace events via publisher and fallback progress stream', async () => { - const component = componentRegistry.get('core.ai.agent'); + test('discovers gateway tools and passes them to the agent', async () => { + const component = componentRegistry.get('core.ai.agent'); expect(component).toBeDefined(); - nextAgentResult = makeAgentResult({ - text: 'Tool enriched answer', - steps: [ - { - text: 'Consider calling lookup_fact', - finishReason: 'tool-calls', - toolCalls: [ - { - toolCallId: 'call-1', - toolName: 'lookup_fact', - args: { topic: 'zebra stripes' }, - }, - ], - toolResults: [ - { - toolCallId: 'call-1', - toolName: 'lookup_fact', - result: { fact: 'Zebra stripes help confuse predators.' }, - }, - ], - }, - ], - }); - - const executePayload = { - inputs: { - userInput: 'Explain zebra stripes', - conversationState: undefined, - chatModel: { - provider: 'openai' as const, - modelId: 'gpt-4o-mini', - }, - modelApiKey: 'sk-openai-from-secret', - }, - params: { - systemPrompt: 'You are a biologist.', - temperature: 0.2, - maxTokens: 256, - memorySize: 5, - stepLimit: 3, - }, + let fetchCalls = 0; + const originalFetch = globalThis.fetch; + const fetchMock: typeof fetch = async () => { + fetchCalls += 1; + return new Response(JSON.stringify({ token: 'gateway-token' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); }; + fetchMock.preconnect = () => {}; + globalThis.fetch = fetchMock; - const publishMock = vi.fn().mockResolvedValue(undefined); - const emitProgressMock = vi.fn(); - const contextWithPublisher: ExecutionContext = { - ...workflowContext, - agentTracePublisher: { publish: publishMock }, - emitProgress: emitProgressMock, + const mockTools = { + ping: { + inputSchema: { type: 'object', properties: {} }, + execute: async () => ({ type: 'json', value: { ok: true } }), + }, }; - await runComponentWithRunner( - component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - }), - executePayload, - contextWithPublisher, - ); - - expect(publishMock).toHaveBeenCalled(); - const publishedEnvelope = publishMock.mock.calls[0][0]; - expect(publishedEnvelope).toMatchObject({ - workflowRunId: 'test-run', - nodeRef: 'core.ai.agent', - agentRunId: expect.any(String), + createMCPClientMock.mockResolvedValue({ + tools: async () => mockTools, + close: async () => {}, }); - const fallbackDuringPublisher = emitProgressMock.mock.calls.some(([payload]) => - payload?.message?.includes('[AgentTraceFallback]'), - ); - expect(fallbackDuringPublisher).toBe(false); - - publishMock.mockReset(); - emitProgressMock.mockReset(); - const contextWithoutPublisher: ExecutionContext = { - ...workflowContext, - agentTracePublisher: undefined, - emitProgress: emitProgressMock, + const contextWithTools: ExecutionContext = { + ...baseContext, + metadata: { + ...baseContext.metadata, + connectedToolNodeIds: ['tool-node-1'], + }, }; - await runComponentWithRunner( - component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - }), - executePayload, - contextWithoutPublisher, - ); - - expect(publishMock).not.toHaveBeenCalled(); - const fallbackCall = emitProgressMock.mock.calls.find(([payload]) => - payload?.message?.includes('[AgentTraceFallback]'), - ); - expect(fallbackCall).toBeTruthy(); - expect(fallbackCall?.[0]?.data).toMatchObject({ - workflowRunId: 'test-run', - nodeRef: 'core.ai.agent', - agentRunId: expect.any(String), - }); - }); - - describe('Structured Output', () => { - const generateObjectMock = vi.fn(); - const generateTextMock = vi.fn(); - - beforeEach(() => { - generateObjectMock.mockReset(); - generateTextMock.mockReset(); - }); - - test('generates structured output from JSON example', async () => { - const component = componentRegistry.get('core.ai.agent'); - expect(component).toBeDefined(); - - generateObjectMock.mockResolvedValue({ - object: { name: 'Test User', age: 30 }, - usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 }, - }); - - const executePayload = { - inputs: { - userInput: 'Generate user data', - conversationState: undefined, - chatModel: { - provider: 'openai' as const, - modelId: 'gpt-4o-mini', - }, - modelApiKey: 'sk-openai-from-secret', - }, - params: { - systemPrompt: '', - temperature: 0.7, - maxTokens: 256, - memorySize: 8, - stepLimit: 4, - structuredOutputEnabled: true, - schemaType: 'json-example' as const, - jsonExample: '{"name": "example", "age": 0}', - autoFixFormat: false, - }, - }; - - const result = (await runComponentWithRunner( + try { + const result = await runComponentWithRunner( component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - generateObject: generateObjectMock as unknown as GenerateObjectFn, - generateText: generateTextMock as unknown as GenerateTextFn, - }), - executePayload, - workflowContext, - )) as any; - - expect(generateObjectMock).toHaveBeenCalledTimes(1); - expect(result.structuredOutput).toEqual({ name: 'Test User', age: 30 }); - expect(result.responseText).toBe(JSON.stringify({ name: 'Test User', age: 30 }, null, 2)); - // ToolLoopAgent should NOT be called when structured output is enabled - expect(toolLoopAgentConstructorMock).not.toHaveBeenCalled(); - }); - - test('generates structured output from JSON Schema', async () => { - const component = componentRegistry.get('core.ai.agent'); - expect(component).toBeDefined(); - - generateObjectMock.mockResolvedValue({ - object: { title: 'Hello World', count: 42 }, - usage: { promptTokens: 15, completionTokens: 25, totalTokens: 40 }, - }); - - const executePayload = { - inputs: { - userInput: 'Generate article data', - conversationState: undefined, - chatModel: { - provider: 'gemini' as const, - modelId: 'gemini-2.5-flash', - }, - modelApiKey: 'gm-gemini-from-secret', - }, - params: { - systemPrompt: '', - temperature: 0.5, - maxTokens: 512, - memorySize: 8, - stepLimit: 4, - structuredOutputEnabled: true, - schemaType: 'json-schema' as const, - jsonSchema: JSON.stringify({ - type: 'object', - properties: { - title: { type: 'string' }, - count: { type: 'integer' }, + component!.execute, + { + inputs: { + userInput: 'Use tools', + conversationState: undefined, + chatModel: { + provider: 'openai', + modelId: 'gpt-4o-mini', }, - required: ['title', 'count'], - }), - autoFixFormat: false, - }, - }; - - const result = (await runComponentWithRunner( - component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - generateObject: generateObjectMock as unknown as GenerateObjectFn, - generateText: generateTextMock as unknown as GenerateTextFn, - }), - executePayload, - workflowContext, - )) as any; - - expect(generateObjectMock).toHaveBeenCalledTimes(1); - expect(result.structuredOutput).toEqual({ title: 'Hello World', count: 42 }); - }); - - test('uses auto-fix when generateObject fails', async () => { - const component = componentRegistry.get('core.ai.agent'); - expect(component).toBeDefined(); - - generateObjectMock.mockRejectedValue(new Error('Schema validation failed')); - generateTextMock.mockResolvedValue({ - text: '```json\n{"name": "Fixed User", "age": 25}\n```', - usage: { promptTokens: 20, completionTokens: 30, totalTokens: 50 }, - }); - - const executePayload = { - inputs: { - userInput: 'Generate user data', - conversationState: undefined, - chatModel: { - provider: 'openai' as const, - modelId: 'gpt-4o-mini', + modelApiKey: 'sk-test', + }, + params: { + systemPrompt: '', + temperature: 0.3, + maxTokens: 64, + memorySize: 3, + stepLimit: 1, }, - modelApiKey: 'sk-openai-from-secret', - }, - params: { - systemPrompt: '', - temperature: 0.7, - maxTokens: 256, - memorySize: 8, - stepLimit: 4, - structuredOutputEnabled: true, - schemaType: 'json-example' as const, - jsonExample: '{"name": "example", "age": 0}', - autoFixFormat: true, }, - }; - - const result = (await runComponentWithRunner( - component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - generateObject: generateObjectMock as unknown as GenerateObjectFn, - generateText: generateTextMock as unknown as GenerateTextFn, - }), - executePayload, - workflowContext, - )) as any; + contextWithTools, + ); - expect(generateObjectMock).toHaveBeenCalledTimes(1); - expect(generateTextMock).toHaveBeenCalledTimes(1); - expect(result.structuredOutput).toEqual({ name: 'Fixed User', age: 25 }); - }); + expect(result.responseText).toBe('Agent final answer'); + expect(fetchCalls).toBeGreaterThan(0); + expect(createMCPClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + transport: { + type: 'http', + url: 'http://localhost:3211/mcp/gateway', + headers: { Authorization: 'Bearer gateway-token' }, + }, + }), + ); - test('returns null structuredOutput when not enabled', async () => { - const component = componentRegistry.get('core.ai.agent'); - expect(component).toBeDefined(); + const settings = expectRecord(toolLoopAgentSettings, 'agent settings'); + const tools = expectRecord(settings.tools, 'agent tools'); + expect(Object.keys(tools)).toEqual(['ping']); + } finally { + globalThis.fetch = originalFetch; + } + }); - nextAgentResult = makeAgentResult(); + test('stores tool outputs in conversation state', async () => { + const component = componentRegistry.get('core.ai.agent'); + expect(component).toBeDefined(); - const executePayload = { - inputs: { - userInput: 'Regular text query', - conversationState: undefined, - chatModel: { - provider: 'openai' as const, - modelId: 'gpt-4o-mini', - }, - modelApiKey: 'sk-openai-from-secret', - }, - params: { - systemPrompt: '', - temperature: 0.7, - maxTokens: 256, - memorySize: 8, - stepLimit: 4, - structuredOutputEnabled: false, + nextGenerateResult = createGenerationResult({ + text: 'Tool done', + toolResults: [ + { + type: 'tool-result', + toolCallId: 'call-1', + toolName: 'ping', + input: { target: 'example.com' }, + output: { type: 'json', value: { ok: true } }, + dynamic: true, }, - }; - - const result = (await runComponentWithRunner( - component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - generateObject: generateObjectMock as unknown as GenerateObjectFn, - generateText: generateTextMock as unknown as GenerateTextFn, - }), - executePayload, - workflowContext, - )) as any; - - expect(generateObjectMock).not.toHaveBeenCalled(); - expect(toolLoopAgentConstructorMock).toHaveBeenCalled(); - expect(result.structuredOutput).toBeNull(); - expect(result.responseText).toBe('Agent final answer'); + ], }); - test('throws error when auto-fix fails to parse', async () => { - const component = componentRegistry.get('core.ai.agent'); - expect(component).toBeDefined(); - - generateObjectMock.mockRejectedValue(new Error('Schema validation failed')); - generateTextMock.mockResolvedValue({ - text: 'This is not valid JSON at all', - usage: { promptTokens: 20, completionTokens: 30, totalTokens: 50 }, - }); - - const executePayload = { + const result = await runComponentWithRunner( + component!.runner, + component!.execute, + { inputs: { - userInput: 'Generate user data', + userInput: 'Run the tool', conversationState: undefined, chatModel: { - provider: 'openai' as const, + provider: 'openai', modelId: 'gpt-4o-mini', }, - modelApiKey: 'sk-openai-from-secret', + modelApiKey: 'sk-test', }, params: { systemPrompt: '', - temperature: 0.7, - maxTokens: 256, - memorySize: 8, - stepLimit: 4, - structuredOutputEnabled: true, - schemaType: 'json-example' as const, - jsonExample: '{"name": "example", "age": 0}', - autoFixFormat: true, + temperature: 0.2, + maxTokens: 128, + memorySize: 5, + stepLimit: 2, }, - }; + }, + baseContext, + ); - await expect( - runComponentWithRunner( - component!.runner, - (payload: any, ctx: any) => - (component!.execute as any)(payload, ctx, { - ToolLoopAgent: MockToolLoopAgent as unknown as ToolLoopAgentClass, - stepCountIs: stepCountIsMock as unknown as StepCountIsFn, - tool: ((definition: any) => definition) as unknown as ToolFn, - createOpenAI: openAiFactoryMock as unknown as CreateOpenAIFn, - createGoogleGenerativeAI: googleFactoryMock as unknown as CreateGoogleGenerativeAIFn, - generateObject: generateObjectMock as unknown as GenerateObjectFn, - generateText: generateTextMock as unknown as GenerateTextFn, - }), - executePayload, - workflowContext, - ), - ).rejects.toThrow('auto-fix could not parse'); + const toolMessage = result.conversationState.messages.find( + (message: { role: string }) => message.role === 'tool', + ); + expect(toolMessage).toBeDefined(); + expect(toolMessage?.content).toMatchObject({ + toolCallId: 'call-1', + toolName: 'ping', + output: { type: 'json', value: { ok: true } }, }); }); }); diff --git a/worker/src/components/ai/__tests__/opencode.test.ts b/worker/src/components/ai/__tests__/opencode.test.ts new file mode 100644 index 00000000..77dc6806 --- /dev/null +++ b/worker/src/components/ai/__tests__/opencode.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, vi, beforeEach, beforeAll } from 'bun:test'; +import { componentRegistry } from '@shipsec/component-sdk'; +import * as SDK from '@shipsec/component-sdk'; // Import for spying +import { IsolatedContainerVolume } from '../../../utils/isolated-volume'; +import '../opencode'; // Register the component + +// Mock IsolatedContainerVolume +vi.mock('../../../utils/isolated-volume', () => { + return { + IsolatedContainerVolume: vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue('mock-volume-name'), + cleanup: vi.fn().mockResolvedValue(undefined), + getVolumeConfig: vi + .fn() + .mockReturnValue({ source: 'mock', target: '/workspace', readOnly: false }), + })), + }; +}); + +// Mock getGatewaySessionToken +vi.mock('../utils', () => { + return { + DEFAULT_GATEWAY_URL: 'http://localhost:3211/mcp/gateway', + getGatewaySessionToken: vi.fn().mockResolvedValue('mock-session-token'), + }; +}); + +describe('shipsec.opencode.agent', () => { + let runSpy: any; + + beforeAll(() => { + // Spy on runComponentWithRunner + // Note: This relies on how Bun/Vitest handles ESM spying. + // If runComponentWithRunner is a read-only export, this might fail or not affect opencode.ts + runSpy = vi.spyOn(SDK, 'runComponentWithRunner').mockResolvedValue({ + stdout: '# Report\n\nInvestigation complete.', + stderr: '', + exitCode: 0, + results: [], + raw: '', + } as any); + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should be registered', () => { + const component = componentRegistry.get('core.ai.opencode'); + expect(component).toBeDefined(); + expect(component?.id).toBe('core.ai.opencode'); + }); + + it('should execute with valid inputs', async () => { + const component = componentRegistry.get('core.ai.opencode'); + if (!component) throw new Error('Component not found'); + + const context = { + runId: 'test-run', + componentRef: 'test-ref', + metadata: { + connectedToolNodeIds: ['tool-1'], + organizationId: 'org-1', + }, + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + emitProgress: vi.fn(), + }; + + const inputs = { + task: 'Find the bug', + context: { alertId: '123' }, + model: { provider: 'openai', modelId: 'gpt-4o', apiKey: 'sk-test' }, + }; + + const params = { + systemPrompt: 'You are a detective.', + autoApprove: true, + }; + + const result = await component.execute({ inputs, params }, context as any); + + expect(result.report).toContain('# Report'); + + expect(IsolatedContainerVolume).toHaveBeenCalled(); + const volumeInstance = (IsolatedContainerVolume as any).mock.results[0].value; + const initCall = volumeInstance.initialize.mock.calls[0][0]; + + expect(initCall['context.json']).toContain('"alertId": "123"'); + expect(initCall['opencode.jsonc']).toContain('shipsec-gateway'); + + expect(runSpy).toHaveBeenCalled(); + const runnerCall = runSpy.mock.calls[0][0]; + expect(runnerCall.image).toBe('ghcr.io/anomalyco/opencode'); + expect(runnerCall.network).toBe('host'); + expect(runnerCall.env.OPENAI_API_KEY).toBe('sk-test'); + }); + + it('should merge providerConfig into opencode.jsonc', async () => { + const component = componentRegistry.get('core.ai.opencode'); + if (!component) throw new Error('Component not found'); + + const context = { + runId: 'test-run-config', + componentRef: 'test-ref-config', + metadata: { + connectedToolNodeIds: [], + organizationId: 'org-1', + }, + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + emitProgress: vi.fn(), + }; + + const inputs = { + task: 'Test config merge', + }; + + const params = { + providerConfig: { + githubToken: 'gh-token', + extraSetting: 123, + }, + }; + + await component.execute({ inputs, params }, context as any); + + expect(IsolatedContainerVolume).toHaveBeenCalled(); + const volumeInstance = (IsolatedContainerVolume as any).mock.results[0].value; + const initCall = volumeInstance.initialize.mock.calls[0][0]; + + const config = JSON.parse(initCall['opencode.jsonc']); + expect(config.githubToken).toBe('gh-token'); + expect(config.extraSetting).toBe(123); + }); +}); diff --git a/worker/src/components/ai/agent-stream-recorder.ts b/worker/src/components/ai/agent-stream-recorder.ts new file mode 100644 index 00000000..112c5db1 --- /dev/null +++ b/worker/src/components/ai/agent-stream-recorder.ts @@ -0,0 +1,125 @@ +import type { ExecutionContext, AgentTraceEvent } from '@shipsec/component-sdk'; + +export type AgentStreamPart = + | { + type: 'message-start'; + messageId: string; + role: 'assistant' | 'user'; + metadata?: Record; + } + | { type: 'text-delta'; textDelta: string } + | { + type: 'tool-input-available'; + toolCallId: string; + toolName: string; + input: Record; + } + | { type: 'tool-output-available'; toolCallId: string; toolName: string; output: unknown } + | { type: 'finish'; finishReason: string; responseText: string } + | { type: `data-${string}`; data: unknown }; + +export class AgentStreamRecorder { + private sequence = 0; + private activeTextId: string | null = null; + + constructor( + private readonly context: ExecutionContext, + private readonly agentRunId: string, + ) {} + + emitMessageStart(role: 'assistant' | 'user' = 'assistant'): void { + this.emitPart({ + type: 'message-start', + messageId: this.agentRunId, + role, + }); + } + + emitToolInput(toolCallId: string, toolName: string, input: Record): void { + this.emitPart({ + type: 'tool-input-available', + toolCallId, + toolName, + input, + }); + } + + emitToolOutput(toolCallId: string, toolName: string, output: unknown): void { + this.emitPart({ + type: 'tool-output-available', + toolCallId, + toolName, + output, + }); + } + + emitToolError(toolCallId: string, toolName: string, error: string): void { + this.emitPart({ + type: 'data-tool-error', + data: { toolCallId, toolName, error }, + }); + } + + private ensureTextStream(): string { + if (this.activeTextId) { + return this.activeTextId; + } + const textId = `${this.agentRunId}:text`; + this.emitPart({ + type: 'data-text-start', + data: { id: textId }, + }); + this.activeTextId = textId; + return textId; + } + + emitTextDelta(textDelta: string): void { + if (!textDelta.trim()) { + return; + } + this.ensureTextStream(); + this.emitPart({ + type: 'text-delta', + textDelta, + }); + } + + emitFinish(finishReason: string, responseText: string): void { + if (this.activeTextId) { + this.emitPart({ + type: 'data-text-end', + data: { id: this.activeTextId }, + }); + this.activeTextId = null; + } + this.emitPart({ + type: 'finish', + finishReason, + responseText, + }); + } + + private emitPart(part: AgentStreamPart): void { + const timestamp = new Date().toISOString(); + const sequence = ++this.sequence; + const envelope: AgentTraceEvent = { + agentRunId: this.agentRunId, + workflowRunId: this.context.runId, + nodeRef: this.context.componentRef, + sequence, + timestamp, + part, + }; + + if (this.context.agentTracePublisher) { + void this.context.agentTracePublisher.publish(envelope); + return; + } + + this.context.emitProgress({ + level: 'info', + message: `[AgentTraceFallback] ${part.type}`, + data: envelope, + }); + } +} diff --git a/worker/src/components/ai/ai-agent.ts b/worker/src/components/ai/ai-agent.ts index da2e9ef9..22972731 100644 --- a/worker/src/components/ai/ai-agent.ts +++ b/worker/src/components/ai/ai-agent.ts @@ -1,24 +1,24 @@ import { randomUUID } from 'crypto'; -import { z, ZodTypeAny } from 'zod'; +import { z } from 'zod'; import { - ToolLoopAgent as ToolLoopAgentImpl, - stepCountIs as stepCountIsImpl, - tool as toolImpl, - generateObject as generateObjectImpl, - generateText as generateTextImpl, - jsonSchema as createJsonSchema, - type Tool, + ToolLoopAgent, + stepCountIs, + type GenerateTextResult, + type JSONValue, + type ModelMessage, + type StepResult, + type ToolLoopAgentSettings, + type ToolResultPart, + type ToolSet, } from 'ai'; -import { createOpenAI as createOpenAIImpl } from '@ai-sdk/openai'; -import { createGoogleGenerativeAI as createGoogleGenerativeAIImpl } from '@ai-sdk/google'; +import { createOpenAI } from '@ai-sdk/openai'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createMCPClient } from '@ai-sdk/mcp'; import { componentRegistry, ComponentRetryPolicy, - type ExecutionContext, - type AgentTraceEvent, ConfigurationError, ValidationError, - fromHttpResponse, defineComponent, inputs, outputs, @@ -26,24 +26,10 @@ import { port, param, } from '@shipsec/component-sdk'; -import { - LLMProviderSchema, - McpToolArgumentSchema, - McpToolDefinitionSchema, - llmProviderContractName, - type McpToolDefinition, -} from '@shipsec/contracts'; - -// Define types for dependencies to enable dependency injection for testing -export type ToolLoopAgentClass = typeof ToolLoopAgentImpl; -export type StepCountIsFn = typeof stepCountIsImpl; -export type ToolFn = typeof toolImpl; -export type CreateOpenAIFn = typeof createOpenAIImpl; -export type CreateGoogleGenerativeAIFn = typeof createGoogleGenerativeAIImpl; -export type GenerateObjectFn = typeof generateObjectImpl; -export type GenerateTextFn = typeof generateTextImpl; - -type ModelProvider = 'openai' | 'gemini' | 'openrouter'; +import { LLMProviderSchema, llmProviderContractName } from '@shipsec/contracts'; +import { AgentStreamRecorder } from './agent-stream-recorder'; + +type ModelProvider = 'openai' | 'gemini' | 'openrouter' | 'zai-coding-plan'; const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL ?? ''; const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL ?? ''; @@ -51,11 +37,14 @@ const OPENROUTER_BASE_URL = process.env.OPENROUTER_BASE_URL ?? 'https://openrout const DEFAULT_OPENAI_MODEL = 'gpt-4o-mini'; const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash'; -const DEFAULT_OPENROUTER_MODEL = 'openrouter/auto'; +const DEFAULT_OPENROUTER_MODEL = 'openai/gpt-4o-mini'; const DEFAULT_TEMPERATURE = 0.7; const DEFAULT_MAX_TOKENS = 1024; const DEFAULT_MEMORY_SIZE = 8; const DEFAULT_STEP_LIMIT = 4; +const LOG_TRUNCATE_LIMIT = 2000; + +import { DEFAULT_GATEWAY_URL, getGatewaySessionToken } from './utils'; const agentMessageSchema = z.object({ role: z.enum(['system', 'user', 'assistant', 'tool']), @@ -64,54 +53,16 @@ const agentMessageSchema = z.object({ type AgentMessage = z.infer; -interface _CoreMessage { - role: 'system' | 'user' | 'assistant' | 'tool'; - content: string; -} - -const toolInvocationMetadataSchema = z.object({ - toolId: z.string().optional(), - title: z.string().optional(), - description: z.string().optional(), - source: z.string().optional(), - endpoint: z.string().optional(), -}); - -const toolInvocationSchema = z.object({ - id: z.string(), - toolName: z.string(), - args: z.unknown(), - result: z.unknown().nullable(), - timestamp: z.string(), - metadata: toolInvocationMetadataSchema.optional(), -}); - const conversationStateSchema = z.object({ sessionId: z.string(), messages: z.array(agentMessageSchema).default([]), - toolInvocations: z.array(toolInvocationSchema).default([]), }); -const reasoningActionSchema = z.object({ - toolCallId: z.string(), - toolName: z.string(), - args: z.unknown(), -}); - -const reasoningObservationSchema = z.object({ - toolCallId: z.string(), - toolName: z.string(), - args: z.unknown(), - result: z.unknown(), -}); - -const reasoningStepSchema = z.object({ - step: z.number().int(), - thought: z.string(), - finishReason: z.string(), - actions: z.array(reasoningActionSchema), - observations: z.array(reasoningObservationSchema), -}); +type ConversationState = z.infer; +type AgentTools = ToolSet; +type AgentStepResult = StepResult; +type AgentGenerationResult = GenerateTextResult; +type ToolResultOutput = ToolResultPart['output']; const inputSchema = inputs({ userInput: port( @@ -157,14 +108,17 @@ const inputSchema = inputs({ connectionType: { kind: 'primitive', name: 'secret' }, }, ), - mcpTools: port( + tools: port( z - .array(McpToolDefinitionSchema()) + .unknown() .optional() - .describe('Normalized MCP tool definitions emitted by provider components.'), + .describe('Anchor port for tool-mode nodes; data is not consumed by the agent.'), { - label: 'MCP Tools', - description: 'Connect outputs from MCP tool providers or mergers.', + label: 'Connected Tools', + description: 'Connect tool-mode nodes here to scope gateway tool discovery for this agent.', + allowAny: true, + reason: 'Tool-mode port acts as a graph anchor; payloads are not consumed by the agent.', + connectionType: { kind: 'contract', name: 'mcp.tool' }, }, ), }); @@ -245,306 +199,24 @@ const parameterSchema = parameters({ description: 'Maximum reasoning/tool steps before the agent stops automatically.', }, ), - structuredOutputEnabled: param( - z - .boolean() - .default(false) - .describe('Enable structured JSON output that adheres to a defined schema.'), - { - label: 'Structured Output', - editor: 'boolean', - description: 'Enable to enforce a specific JSON output structure from the AI model.', - }, - ), - schemaType: param( - z - .enum(['json-example', 'json-schema']) - .default('json-example') - .describe('How to define the output schema: from a JSON example or a full JSON Schema.'), - { - label: 'Schema Type', - editor: 'select', - options: [ - { label: 'Generate From JSON Example', value: 'json-example' }, - { label: 'Define Using JSON Schema', value: 'json-schema' }, - ], - description: 'Choose how to define the output structure.', - visibleWhen: { structuredOutputEnabled: true }, - }, - ), - jsonExample: param( - z - .string() - .optional() - .describe('Example JSON object to generate schema from. All properties become required.'), - { - label: 'JSON Example', - editor: 'json', - description: - 'Provide an example JSON object. Property types and names will be used to generate the schema. All fields are treated as required.', - helpText: 'Example: { "name": "John", "age": 30, "skills": ["security", "architecture"] }', - visibleWhen: { structuredOutputEnabled: true, schemaType: 'json-example' }, - }, - ), - jsonSchema: param( - z.string().optional().describe('Full JSON Schema definition for structured output validation.'), - { - label: 'JSON Schema', - editor: 'json', - description: 'Provide a full JSON Schema definition. Refer to json-schema.org for syntax.', - helpText: - 'Example: { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] }', - visibleWhen: { structuredOutputEnabled: true, schemaType: 'json-schema' }, - }, - ), - autoFixFormat: param( - z.boolean().default(false).describe('Attempt to fix malformed JSON responses from the model.'), - { - label: 'Auto-Fix Format', - editor: 'boolean', - description: 'Attempt to fix malformed JSON responses from the model.', - helpText: - 'When enabled, tries to extract valid JSON from responses that contain extra text or formatting issues.', - visibleWhen: { structuredOutputEnabled: true }, - }, - ), }); -type ConversationState = z.infer; -type ToolInvocationEntry = z.infer; - -type McpToolArgument = z.infer; - -type ReasoningStep = z.infer; - const outputSchema = outputs({ responseText: port(z.string(), { label: 'Agent Response', description: 'Final assistant message produced by the agent.', }), - structuredOutput: port(z.unknown().nullable(), { - label: 'Structured Output', - description: 'Parsed JSON object when structured output is enabled. Null otherwise.', - allowAny: true, - reason: 'Structured output is user-defined JSON.', - connectionType: { kind: 'primitive', name: 'json' }, - }), conversationState: port(conversationStateSchema, { label: 'Conversation State', description: 'Updated conversation memory for subsequent agent turns.', connectionType: { kind: 'primitive', name: 'json' }, }), - toolInvocations: port(z.array(toolInvocationSchema), { - label: 'Tool Invocations', - description: 'Array of MCP tool calls executed during this run.', - connectionType: { kind: 'primitive', name: 'json' }, - }), - reasoningTrace: port(z.array(reasoningStepSchema), { - label: 'Reasoning Trace', - description: 'Sequence of Think → Act → Observe steps executed by the agent.', - connectionType: { kind: 'primitive', name: 'json' }, - }), - usage: port(z.unknown().optional(), { - label: 'Usage', - description: 'Token usage metadata returned by the provider, if available.', - allowAny: true, - reason: 'Usage payloads vary by model provider.', - connectionType: { kind: 'primitive', name: 'json' }, - }), - rawResponse: port(z.unknown(), { - label: 'Raw Response', - description: 'Raw provider response payload for debugging.', - allowAny: true, - reason: 'Provider responses vary by model provider.', - connectionType: { kind: 'primitive', name: 'json' }, - }), agentRunId: port(z.string(), { label: 'Agent Run ID', description: 'Unique identifier for streaming and replaying this agent session.', }), }); -type AgentStreamPart = - | { - type: 'message-start'; - messageId: string; - role: 'assistant' | 'user'; - metadata?: Record; - } - | { type: 'text-delta'; textDelta: string } - | { - type: 'tool-input-available'; - toolCallId: string; - toolName: string; - input: Record; - } - | { type: 'tool-output-available'; toolCallId: string; toolName: string; output: unknown } - | { type: 'finish'; finishReason: string; responseText: string } - | { type: `data-${string}`; data: unknown }; - -class AgentStreamRecorder { - private sequence = 0; - private activeTextId: string | null = null; - - constructor( - private readonly context: ExecutionContext, - private readonly agentRunId: string, - ) {} - - emitMessageStart(role: 'assistant' | 'user' = 'assistant'): void { - this.emitPart({ - type: 'message-start', - messageId: this.agentRunId, - role, - }); - } - - emitReasoningStep(step: ReasoningStep): void { - this.emitPart({ - type: 'data-reasoning-step', - data: step, - }); - } - - emitToolInput(toolCallId: string, toolName: string, input: Record): void { - this.emitPart({ - type: 'tool-input-available', - toolCallId, - toolName, - input, - }); - } - - emitToolOutput(toolCallId: string, toolName: string, output: unknown): void { - this.emitPart({ - type: 'tool-output-available', - toolCallId, - toolName, - output, - }); - } - - emitToolError(toolCallId: string, toolName: string, error: string): void { - this.emitPart({ - type: 'data-tool-error', - data: { toolCallId, toolName, error }, - }); - } - - private ensureTextStream(): string { - if (this.activeTextId) { - return this.activeTextId; - } - const textId = `${this.agentRunId}:text`; - this.emitPart({ - type: 'data-text-start', - data: { id: textId }, - }); - this.activeTextId = textId; - return textId; - } - - emitTextDelta(textDelta: string): void { - if (!textDelta.trim()) { - return; - } - const _textId = this.ensureTextStream(); - this.emitPart({ - type: 'text-delta', - textDelta, - }); - } - - emitFinish(finishReason: string, responseText: string): void { - if (this.activeTextId) { - this.emitPart({ - type: 'data-text-end', - data: { id: this.activeTextId }, - }); - this.activeTextId = null; - } - this.emitPart({ - type: 'finish', - finishReason, - responseText, - }); - } - - private emitPart(part: AgentStreamPart): void { - const timestamp = new Date().toISOString(); - const sequence = ++this.sequence; - const envelope: AgentTraceEvent = { - agentRunId: this.agentRunId, - workflowRunId: this.context.runId, - nodeRef: this.context.componentRef, - sequence, - timestamp, - part, - }; - - if (this.context.agentTracePublisher) { - void this.context.agentTracePublisher.publish(envelope); - return; - } - - this.context.emitProgress({ - level: 'info', - message: `[AgentTraceFallback] ${part.type}`, - data: envelope, - }); - } -} - -class MCPClient { - private readonly endpoint: string; - private readonly sessionId: string; - private readonly headers?: Record; - private readonly fetcher: ExecutionContext['http']['fetch']; - - constructor(options: { - endpoint: string; - sessionId: string; - headers?: Record; - fetcher: ExecutionContext['http']['fetch']; - }) { - this.endpoint = options.endpoint.replace(/\/+$/, ''); - this.sessionId = options.sessionId; - this.headers = sanitizeHeaders(options.headers); - this.fetcher = options.fetcher; - } - - async execute(toolName: string, args: unknown): Promise { - const payload = { - sessionId: this.sessionId, - toolName, - arguments: args ?? {}, - }; - - const response = await this.fetcher(this.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-MCP-Session': this.sessionId, - 'X-MCP-Tool': toolName, - ...(this.headers ?? {}), - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => ''); - throw fromHttpResponse(response, `MCP request failed: ${errorText}`); - } - - const contentType = response.headers.get('content-type') ?? ''; - if (contentType.includes('application/json')) { - return await response.json(); - } - - return await response.text(); - } -} - function ensureModelName(provider: ModelProvider, modelId?: string | null): string { const trimmed = modelId?.trim(); if (trimmed && trimmed.length > 0) { @@ -591,7 +263,7 @@ function ensureSystemMessage(history: AgentMessage[], systemPrompt: string): Age } if (firstMessage.content !== systemPrompt.trim()) { - return [{ role: 'system', content: systemPrompt.trim() as string }, ...rest]; + return [{ role: 'system', content: systemPrompt.trim() }, ...rest]; } return history; @@ -629,336 +301,189 @@ function sanitizeHeaders( return Object.keys(entries).length > 0 ? entries : undefined; } -type RegisteredToolMetadata = z.infer; - -interface RegisteredMcpTool { - name: string; - tool: Tool; - metadata: RegisteredToolMetadata; +function normalizeMessageContent(content: unknown): string { + if (typeof content === 'string') { + return content; + } + return JSON.stringify(content ?? ''); } -interface RegisterMcpToolParams { - tools?: McpToolDefinition[]; - sessionId: string; - toolFactory: ToolFn; - agentStream: AgentStreamRecorder; - fetcher: ExecutionContext['http']['fetch']; - logger?: { - warn?: (...args: unknown[]) => void; - }; +function truncateText(value: string, maxLength: number): string { + if (value.length <= maxLength) { + return value; + } + const remaining = value.length - maxLength; + return `${value.slice(0, maxLength)}...(+${remaining} chars)`; } -function registerMcpTools({ - tools, - sessionId, - toolFactory, - agentStream, - fetcher, - logger, -}: RegisterMcpToolParams): RegisteredMcpTool[] { - if (!Array.isArray(tools) || tools.length === 0) { - return []; +function safeStringify(value: unknown, maxLength: number): string { + try { + return truncateText(JSON.stringify(value), maxLength); + } catch { + return truncateText(String(value), maxLength); } +} - const seenIds = new Set(); - const usedNames = new Set(); - const registered: RegisteredMcpTool[] = []; - - tools.forEach((tool, index) => { - if (!tool || typeof tool !== 'object') { - return; - } - - if (seenIds.has(tool.id)) { - logger?.warn?.( - `[AIAgent] Skipping MCP tool "${tool.id}" because a duplicate id was detected.`, - ); - return; - } - seenIds.add(tool.id); - - const endpoint = typeof tool.endpoint === 'string' ? tool.endpoint.trim() : ''; - if (!endpoint) { - logger?.warn?.( - `[AIAgent] Skipping MCP tool "${tool.id}" because the endpoint is missing or empty.`, - ); - return; - } - - const remoteToolName = (tool.metadata?.toolName ?? tool.id).trim() || tool.id; - const toolName = ensureUniqueToolName(remoteToolName, usedNames, index); - - const client = new MCPClient({ - endpoint, - sessionId, - headers: tool.headers, - fetcher, - }); - - const description = - tool.description ?? - (tool.title ? `Invoke ${tool.title}` : `Invoke MCP tool ${remoteToolName}`); - - const metadata: RegisteredToolMetadata = { - toolId: tool.id, - title: tool.title ?? remoteToolName, - description: tool.description, - source: tool.metadata?.source, - endpoint, - }; - - const registeredTool = toolFactory, unknown>({ - type: 'dynamic', - description, - inputSchema: buildToolArgumentSchema(tool.arguments), - execute: async (args: Record) => { - const invocationId = `${tool.id}-${randomUUID()}`; - const normalizedArgs = args ?? {}; - agentStream.emitToolInput(invocationId, toolName, normalizedArgs); - - try { - const result = await client.execute(remoteToolName, normalizedArgs); - agentStream.emitToolOutput(invocationId, toolName, result); - return result; - } catch (error) { - agentStream.emitToolError( - invocationId, - toolName, - error instanceof Error ? error.message : String(error), - ); - throw error; - } - }, - }); - - registered.push({ - name: toolName, - tool: registeredTool, - metadata, - }); - }); - - return registered; +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } -function ensureUniqueToolName(baseName: string, usedNames: Set, index: number): string { - const sanitized = sanitizeToolKey(baseName); - let candidate = sanitized.length > 0 ? sanitized : `mcp_tool_${index + 1}`; - let suffix = 2; +function toRecord(value: unknown): Record { + return isRecord(value) ? value : {}; +} - while (usedNames.has(candidate)) { - const prefix = sanitized.length > 0 ? sanitized : `mcp_tool_${index + 1}`; - candidate = `${prefix}_${suffix++}`; +function isJsonValue(value: unknown): value is JSONValue { + if (value === null) { + return true; } - - usedNames.add(candidate); - return candidate; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return true; + } + if (Array.isArray(value)) { + return value.every(isJsonValue); + } + if (isRecord(value)) { + return Object.values(value).every(isJsonValue); + } + return false; } -function sanitizeToolKey(value: string): string { - return value - .trim() - .replace(/\s+/g, '_') - .replace(/[^a-zA-Z0-9_-]/g, '_') - .replace(/_+/g, '_') - .replace(/^_|_$/g, '') - .toLowerCase(); +function isToolResultOutput(value: unknown): value is ToolResultOutput { + if (!isRecord(value)) { + return false; + } + const typeValue = value.type; + if (typeof typeValue !== 'string') { + return false; + } + return ['text', 'json', 'execution-denied', 'error-text', 'error-json', 'content'].includes( + typeValue, + ); } -function buildToolArgumentSchema(args?: McpToolArgument[]) { - if (!Array.isArray(args) || args.length === 0) { - return z.object({}).passthrough(); +function toToolResultOutput(value: unknown): ToolResultOutput { + if (isToolResultOutput(value)) { + return value; } - - const shape = args.reduce>((acc, arg) => { - const key = arg.name.trim(); - if (!key) { - return acc; - } - - let field: ZodTypeAny; - switch (arg.type) { - case 'number': - field = z.number(); - break; - case 'boolean': - field = z.boolean(); - break; - case 'json': - field = z.any(); - break; - case 'string': - default: - field = z.string(); - break; - } - - if (Array.isArray(arg.enum) && arg.enum.length > 0) { - const stringValues = arg.enum.filter((value): value is string => typeof value === 'string'); - if (stringValues.length === arg.enum.length && stringValues.length > 0) { - const enumValues = stringValues as [string, ...string[]]; - field = z.enum(enumValues); - } - } - - if (arg.description) { - field = field.describe(arg.description); - } - - if (!arg.required) { - field = field.optional(); - } - - acc[key] = field; - return acc; - }, {}); - - return z.object(shape).passthrough(); + if (isJsonValue(value)) { + return { type: 'json', value }; + } + return { type: 'text', value: JSON.stringify(value) }; } -function mapStepToReasoning(step: any, index: number, sessionId: string): ReasoningStep { - const getArgs = (entity: any) => - entity?.args !== undefined ? entity.args : (entity?.input ?? null); - const getOutput = (entity: any) => - entity?.result !== undefined ? entity.result : (entity?.output ?? null); +function toToolResultPart(content: unknown): ToolResultPart { + const record = isRecord(content) ? content : {}; + const toolCallId = + typeof record.toolCallId === 'string' && record.toolCallId.trim().length > 0 + ? record.toolCallId + : 'unknown'; + const toolName = + typeof record.toolName === 'string' && record.toolName.trim().length > 0 + ? record.toolName + : 'tool'; + const rawOutput = record.output ?? record.result; return { - step: index + 1, - thought: typeof step?.text === 'string' ? step.text : JSON.stringify(step?.text ?? ''), - finishReason: typeof step?.finishReason === 'string' ? step.finishReason : 'other', - actions: Array.isArray(step?.toolCalls) - ? step.toolCalls.map((toolCall: any) => ({ - toolCallId: toolCall?.toolCallId ?? `${sessionId}-tool-${index + 1}`, - toolName: toolCall?.toolName ?? 'tool', - args: getArgs(toolCall), - })) - : [], - observations: Array.isArray(step?.toolResults) - ? step.toolResults.map((toolResult: any) => ({ - toolCallId: toolResult?.toolCallId ?? `${sessionId}-tool-${index + 1}`, - toolName: toolResult?.toolName ?? 'tool', - args: getArgs(toolResult), - result: getOutput(toolResult), - })) - : [], + type: 'tool-result', + toolCallId, + toolName, + output: toToolResultOutput(rawOutput), }; } -/** - * Converts a JSON example object to a JSON Schema. - * All properties are treated as required (matching n8n behavior). - */ -function jsonExampleToJsonSchema(example: unknown): object { - if (example === null) { - return { type: 'null' }; - } - - if (Array.isArray(example)) { - const items = example.length > 0 ? jsonExampleToJsonSchema(example[0]) : {}; - return { type: 'array', items }; +function formatErrorForLog(error: unknown, maxLength: number): Record { + if (error instanceof Error) { + return { + name: error.name, + message: truncateText(error.message, maxLength), + stack: error.stack ? truncateText(error.stack, maxLength) : undefined, + cause: + 'cause' in error + ? safeStringify((error as { cause?: unknown }).cause, maxLength) + : undefined, + }; } - if (typeof example === 'object') { - const properties: Record = {}; - const required: string[] = []; - - for (const [key, value] of Object.entries(example as Record)) { - properties[key] = jsonExampleToJsonSchema(value); - required.push(key); - } - + if (isRecord(error)) { + const message = + typeof error.message === 'string' ? error.message : safeStringify(error, maxLength); return { - type: 'object', - properties, - required, - additionalProperties: false, + name: typeof error.name === 'string' ? error.name : undefined, + message: truncateText(message, maxLength), + keys: Object.keys(error).slice(0, 12), }; } - if (typeof example === 'string') return { type: 'string' }; - if (typeof example === 'number') { - return Number.isInteger(example) ? { type: 'integer' } : { type: 'number' }; - } - if (typeof example === 'boolean') return { type: 'boolean' }; + return { message: truncateText(String(error), maxLength) }; +} - return {}; +function getToolInput(entity: { input?: unknown } | null | undefined): unknown { + return entity?.input ?? null; } -/** - * Resolves the structured output schema from user input. - * Returns null if structured output is disabled or no valid schema provided. - */ -function resolveStructuredOutputSchema(params: { - structuredOutputEnabled?: boolean; - schemaType?: 'json-example' | 'json-schema'; - jsonExample?: string; - jsonSchema?: string; -}): object | null { - if (!params.structuredOutputEnabled) { - return null; - } +function getToolOutput(entity: { output?: unknown } | null | undefined): unknown { + return entity?.output ?? null; +} - if (params.schemaType === 'json-example' && params.jsonExample) { - try { - const example = JSON.parse(params.jsonExample); - return jsonExampleToJsonSchema(example); - } catch (e) { - throw new ValidationError('Invalid JSON example: unable to parse JSON.', { - cause: e instanceof Error ? e : undefined, - details: { field: 'jsonExample' }, - }); +function toModelMessages(messages: AgentMessage[]): ModelMessage[] { + const result: ModelMessage[] = []; + for (const message of messages) { + if (message.role === 'system') { + continue; } - } - if (params.schemaType === 'json-schema' && params.jsonSchema) { - try { - return JSON.parse(params.jsonSchema); - } catch (e) { - throw new ValidationError('Invalid JSON Schema: unable to parse JSON.', { - cause: e instanceof Error ? e : undefined, - details: { field: 'jsonSchema' }, + if (message.role === 'tool') { + result.push({ + role: 'tool', + content: [toToolResultPart(message.content)], }); + continue; } - } - return null; -} + if (message.role === 'user') { + result.push({ + role: 'user', + content: normalizeMessageContent(message.content), + }); + continue; + } -/** - * Attempts to fix malformed JSON by extracting valid JSON from text. - * Handles common issues like markdown code blocks, extra text before/after JSON. - */ -function attemptJsonFix(text: string): unknown | null { - try { - return JSON.parse(text); - } catch { - // Continue to fixes + result.push({ + role: 'assistant', + content: normalizeMessageContent(message.content), + }); } - let cleaned = text.replace(/```json\s*/gi, '').replace(/```\s*/g, ''); - - const objectMatch = cleaned.match(/\{[\s\S]*\}/); - const arrayMatch = cleaned.match(/\[[\s\S]*\]/); - const jsonCandidate = objectMatch?.[0] ?? arrayMatch?.[0]; + return result; +} - if (jsonCandidate) { - try { - return JSON.parse(jsonCandidate); - } catch { - // Continue - } - } +interface RegisterGatewayToolsParams { + gatewayUrl: string; + sessionToken: string; +} - cleaned = cleaned - .trim() - .replace(/^(Here'?s?|The|Output:?|Result:?|Response:?)\s*/i, '') - .trim(); +async function registerGatewayTools({ + gatewayUrl, + sessionToken, +}: RegisterGatewayToolsParams): Promise<{ + tools: ToolSet; + close: () => Promise; +}> { + const mcpClient = await createMCPClient({ + transport: { + type: 'http', + url: gatewayUrl, + headers: { Authorization: `Bearer ${sessionToken}` }, + }, + }); - try { - return JSON.parse(cleaned); - } catch { - return null; - } + const tools = await mcpClient.tools(); + return { + tools, + close: async () => { + await mcpClient.close(); + }, + }; } const definition = defineComponent({ @@ -976,345 +501,176 @@ const definition = defineComponent({ inputs: inputSchema, outputs: outputSchema, parameters: parameterSchema, - docs: `An AI SDK-powered agent that maintains conversation memory, calls MCP tools, and returns both the final answer and a reasoning trace. + docs: `An AI SDK-powered agent that maintains conversation memory, calls MCP tools via the gateway, and streams progress events. How it behaves: - Memory → The agent maintains a conversation state object you can persist between turns. - Model → Connect a chat model configuration output into the Chat Model input or customise the defaults below. -- MCP → Supply an MCP endpoint through the MCP input to expose your external tools. +- MCP → Connect tool-mode nodes to the tools port; the gateway resolves the tool set for this agent. Typical workflow: 1. Entry Point (or upstream Chat Model) → wire its text output into User Input. -2. AI SDK Agent (this node) → loops with Think/Act/Observe, logging tool calls and keeping state. -3. Downstream node (Console Log, Storage, etc.) → consume responseText or reasoningTrace. +2. AI SDK Agent (this node) → loops with tool calling, logging tool calls as it goes. +3. Downstream node (Console Log, Storage, etc.) → consume responseText. Loop the Conversation State output back into the next agent invocation to keep multi-turn context.`, ui: { slug: 'ai-agent', - version: '1.0.0', + version: '1.1.0', type: 'process', category: 'ai', - description: - 'AI SDK agent with conversation memory, MCP tool calling, and reasoning trace output.', + description: 'AI SDK agent with conversation memory and MCP tool calling via gateway.', icon: 'Bot', author: { name: 'ShipSecAI', type: 'shipsecai', }, }, - async execute( - { inputs, params }, - context, - // Optional dependencies for testing - in production these will use the default implementations - dependencies?: { - ToolLoopAgent?: ToolLoopAgentClass; - stepCountIs?: StepCountIsFn; - tool?: ToolFn; - createOpenAI?: CreateOpenAIFn; - createGoogleGenerativeAI?: CreateGoogleGenerativeAIFn; - generateObject?: GenerateObjectFn; - generateText?: GenerateTextFn; - }, - ) { - const { userInput, conversationState, chatModel, mcpTools, modelApiKey } = inputs; - const { - systemPrompt, - temperature, - maxTokens, - memorySize, - stepLimit, - structuredOutputEnabled, - schemaType, - jsonExample, - jsonSchema, - autoFixFormat, - } = params; - - const debugLog = (...args: unknown[]) => - context.logger.debug(`[AIAgent Debug] ${args.join(' ')}`); - const agentRunId = `${context.runId}:${context.componentRef}:${randomUUID()}`; - const agentStream = new AgentStreamRecorder(context as ExecutionContext, agentRunId); - agentStream.emitMessageStart(); - context.emitProgress({ - level: 'info', - message: 'AI agent session started', - data: { - agentRunId, - agentStatus: 'started', - }, - }); - - debugLog('Incoming params', { - userInput, - conversationState, - chatModel, - mcpTools, - systemPrompt, - temperature, - maxTokens, - memorySize, - stepLimit, - }); - - const trimmedInput = userInput.trim(); - debugLog('Trimmed input', trimmedInput); - - if (!trimmedInput) { - throw new ValidationError('AI Agent requires a non-empty user input.', { - fieldErrors: { userInput: ['Input cannot be empty'] }, - }); - } - - const effectiveProvider = (chatModel?.provider ?? 'openai') as ModelProvider; - const effectiveModel = ensureModelName(effectiveProvider, chatModel?.modelId ?? null); + async execute({ inputs, params }, context) { + const { userInput, conversationState, chatModel, modelApiKey } = inputs; + const { systemPrompt, temperature, maxTokens, memorySize, stepLimit } = params; - let overrideApiKey = chatModel?.apiKey ?? null; - if (modelApiKey && modelApiKey.trim().length > 0) { - overrideApiKey = modelApiKey.trim(); - } + const agentRunId = `${context.runId}:${context.componentRef}:${randomUUID()}`; - const effectiveApiKey = resolveApiKey(effectiveProvider, overrideApiKey); - debugLog('Resolved model configuration', { - effectiveProvider, - effectiveModel, - hasExplicitApiKey: Boolean(chatModel?.apiKey) || Boolean(modelApiKey), - apiKeyProvided: Boolean(effectiveApiKey), - }); + const agentStream = new AgentStreamRecorder(context, agentRunId); + const { connectedToolNodeIds, organizationId } = context.metadata; - const explicitBaseUrl = chatModel?.baseUrl?.trim(); - const baseUrl = - explicitBaseUrl && explicitBaseUrl.length > 0 - ? explicitBaseUrl - : effectiveProvider === 'gemini' - ? GEMINI_BASE_URL - : effectiveProvider === 'openrouter' - ? OPENROUTER_BASE_URL - : OPENAI_BASE_URL; - - debugLog('Resolved base URL', { explicitBaseUrl, baseUrl }); - - const sanitizedHeaders = - chatModel && (chatModel.provider === 'openai' || chatModel.provider === 'openrouter') - ? sanitizeHeaders(chatModel.headers) - : undefined; - debugLog('Sanitized headers', sanitizedHeaders); - - const incomingState = conversationState; - debugLog('Incoming conversation state', incomingState); - - const sessionId = incomingState?.sessionId ?? randomUUID(); - const existingMessages = Array.isArray(incomingState?.messages) ? incomingState.messages : []; - const existingToolHistory = Array.isArray(incomingState?.toolInvocations) - ? incomingState.toolInvocations - : []; - debugLog('Session details', { - sessionId, - existingMessagesCount: existingMessages.length, - existingToolHistoryCount: existingToolHistory.length, - }); + let discoveredTools: ToolSet = {}; + let closeDiscovery: (() => Promise) | undefined; - let history: AgentMessage[] = ensureSystemMessage([...existingMessages], systemPrompt ?? ''); - history = trimConversation(history, memorySize); - debugLog('History after ensuring system message and trimming', history); - - const userMessage: AgentMessage = { role: 'user', content: trimmedInput }; - const historyWithUser = trimConversation([...history, userMessage], memorySize); - debugLog('History with user message', historyWithUser); - - const toolFn = dependencies?.tool ?? toolImpl; - const toolMetadataByName = new Map(); - const registeredTools: Record> = {}; - - const registeredMcpTools = registerMcpTools({ - tools: mcpTools, - sessionId, - toolFactory: toolFn, - agentStream, - fetcher: context.http.fetch, - logger: context.logger, - }); - for (const entry of registeredMcpTools) { - registeredTools[entry.name] = entry.tool; - toolMetadataByName.set(entry.name, entry.metadata); + if (connectedToolNodeIds && connectedToolNodeIds.length > 0) { + context.logger.info( + `Discovering tools from gateway for nodes: ${connectedToolNodeIds.join(', ')}`, + ); + try { + const sessionToken = await getGatewaySessionToken( + context.runId, + organizationId ?? null, + connectedToolNodeIds, + ); + const discoveryResult = await registerGatewayTools({ + gatewayUrl: DEFAULT_GATEWAY_URL, + sessionToken, + }); + discoveredTools = discoveryResult.tools; + closeDiscovery = discoveryResult.close; + } catch (error) { + context.logger.error(`Failed to discover tools from gateway: ${error}`); + } } - const availableToolsCount = Object.keys(registeredTools).length; - const toolsConfig = availableToolsCount > 0 ? registeredTools : undefined; - debugLog('Tools configuration', { - availableToolsCount, - toolsConfigKeys: toolsConfig ? Object.keys(toolsConfig) : [], - }); - - const systemMessageEntry = historyWithUser.find((message) => message.role === 'system'); - const resolvedSystemPrompt = systemPrompt?.trim()?.length - ? systemPrompt.trim() - : systemMessageEntry && typeof systemMessageEntry.content === 'string' - ? systemMessageEntry.content - : systemMessageEntry && systemMessageEntry.content !== undefined - ? JSON.stringify(systemMessageEntry.content) - : ''; - debugLog('Resolved system prompt', resolvedSystemPrompt); - - const messagesForModel = historyWithUser - .filter((message) => message.role !== 'system') - .map((message) => ({ - role: message.role, - content: - typeof message.content === 'string' ? message.content : JSON.stringify(message.content), - })); - debugLog('Messages for model', messagesForModel); - - const createGoogleGenerativeAI = - dependencies?.createGoogleGenerativeAI ?? createGoogleGenerativeAIImpl; - const createOpenAI = dependencies?.createOpenAI ?? createOpenAIImpl; - const openAIOptions = { - apiKey: effectiveApiKey, - ...(baseUrl ? { baseURL: baseUrl } : {}), - ...(sanitizedHeaders && Object.keys(sanitizedHeaders).length > 0 - ? { headers: sanitizedHeaders } - : {}), - }; - const model = - effectiveProvider === 'gemini' - ? createGoogleGenerativeAI({ - apiKey: effectiveApiKey, - ...(baseUrl ? { baseURL: baseUrl } : {}), - })(effectiveModel) - : createOpenAI(openAIOptions)(effectiveModel); - debugLog('Model factory created', { - provider: effectiveProvider, - modelId: effectiveModel, - baseUrl, - headers: sanitizedHeaders, - temperature, - maxTokens, - stepLimit, - }); - - // Resolve structured output schema if enabled - const structuredSchema = resolveStructuredOutputSchema({ - structuredOutputEnabled, - schemaType, - jsonExample, - jsonSchema, - }); - - let responseText: string; - let structuredOutput: unknown = null; - let generationResult: any; - - if (structuredSchema) { - // Use generateObject for structured output mode - context.logger.info('[AIAgent] Using structured output mode with JSON Schema.'); + try { + agentStream.emitMessageStart(); context.emitProgress({ level: 'info', - message: 'AI agent generating structured output...', + message: 'AI agent session started', data: { agentRunId, - agentStatus: 'running', + agentStatus: 'started', }, }); - const generateObject = dependencies?.generateObject ?? generateObjectImpl; - const generateText = dependencies?.generateText ?? generateTextImpl; + const trimmedInput = userInput.trim(); - try { - const objectResult = await generateObject({ - model, - schema: createJsonSchema(structuredSchema), - system: resolvedSystemPrompt || undefined, - messages: messagesForModel as any, - temperature, - maxOutputTokens: maxTokens, + if (!trimmedInput) { + throw new ValidationError('AI Agent requires a non-empty user input.', { + fieldErrors: { userInput: ['Input cannot be empty'] }, }); + } - structuredOutput = objectResult.object; - responseText = JSON.stringify(structuredOutput, null, 2); - generationResult = { - text: responseText, - steps: [], - toolResults: [], - finishReason: 'stop', - usage: objectResult.usage, - }; - debugLog('Structured output generated successfully', structuredOutput); - } catch (error) { - // If generateObject fails and auto-fix is enabled, try text generation + fix - if (autoFixFormat) { - context.logger.warn( - '[AIAgent] Structured output failed, attempting auto-fix via text generation.', - ); - - const textResult = await generateText({ - model, - system: resolvedSystemPrompt || undefined, - messages: [ - ...messagesForModel, - { - role: 'user' as const, - content: `Respond with valid JSON matching this schema: ${JSON.stringify(structuredSchema)}`, - }, - ] as any, - temperature, - maxOutputTokens: maxTokens, - }); - - const fixedOutput = attemptJsonFix(textResult.text); - if (fixedOutput !== null) { - structuredOutput = fixedOutput; - responseText = JSON.stringify(fixedOutput, null, 2); - generationResult = { - text: responseText, - steps: [], - toolResults: [], - finishReason: 'stop', - usage: textResult.usage, - }; - debugLog('Auto-fix succeeded', fixedOutput); - } else { - throw new ValidationError( - `Structured output failed and auto-fix could not parse response`, - { - cause: error instanceof Error ? error : undefined, - details: { - field: 'structuredOutput', - originalError: error instanceof Error ? error.message : String(error), - responseSnippet: textResult.text.slice(0, 500), - fullResponseLength: textResult.text.length, - }, - }, - ); - } - } else { - throw error; - } + const effectiveProvider: ModelProvider = chatModel?.provider ?? 'openai'; + const effectiveModel = ensureModelName(effectiveProvider, chatModel?.modelId ?? null); + + let overrideApiKey = chatModel?.apiKey ?? null; + if (modelApiKey && modelApiKey.trim().length > 0) { + overrideApiKey = modelApiKey.trim(); } - } else { - // Use ToolLoopAgent for standard text generation with tools - const ToolLoopAgent = dependencies?.ToolLoopAgent ?? ToolLoopAgentImpl; - const stepCountIs = dependencies?.stepCountIs ?? stepCountIsImpl; - let streamedStepCount = 0; - const agent = new ToolLoopAgent({ + + const effectiveApiKey = resolveApiKey(effectiveProvider, overrideApiKey); + const explicitBaseUrl = chatModel?.baseUrl?.trim(); + const baseUrl = + explicitBaseUrl && explicitBaseUrl.length > 0 + ? explicitBaseUrl + : effectiveProvider === 'gemini' + ? GEMINI_BASE_URL + : effectiveProvider === 'openrouter' + ? OPENROUTER_BASE_URL + : OPENAI_BASE_URL; + + const sanitizedHeaders = + chatModel && (chatModel.provider === 'openai' || chatModel.provider === 'openrouter') + ? sanitizeHeaders(chatModel.headers) + : undefined; + + const incomingState = conversationState; + + const sessionId = incomingState?.sessionId ?? randomUUID(); + const existingMessages = Array.isArray(incomingState?.messages) ? incomingState.messages : []; + + let history: AgentMessage[] = ensureSystemMessage([...existingMessages], systemPrompt ?? ''); + history = trimConversation(history, memorySize); + + const userMessage: AgentMessage = { role: 'user', content: trimmedInput }; + const historyWithUser = trimConversation([...history, userMessage], memorySize); + + const availableToolsCount = Object.keys(discoveredTools).length; + const toolsConfig = availableToolsCount > 0 ? discoveredTools : undefined; + + const systemMessageEntry = historyWithUser.find((message) => message.role === 'system'); + const resolvedSystemPrompt = systemPrompt?.trim()?.length + ? systemPrompt.trim() + : systemMessageEntry && typeof systemMessageEntry.content === 'string' + ? systemMessageEntry.content + : systemMessageEntry && systemMessageEntry.content !== undefined + ? JSON.stringify(systemMessageEntry.content) + : ''; + const messagesForModel = toModelMessages(historyWithUser); + + const openAIOptions = { + apiKey: effectiveApiKey, + ...(baseUrl ? { baseURL: baseUrl } : {}), + ...(sanitizedHeaders && Object.keys(sanitizedHeaders).length > 0 + ? { headers: sanitizedHeaders } + : {}), + }; + const isOpenRouter = + effectiveProvider === 'openrouter' || + (typeof baseUrl === 'string' && baseUrl.includes('openrouter.ai')); + const openAIProvider = createOpenAI({ + ...openAIOptions, + ...(isOpenRouter ? { name: 'openrouter' } : {}), + }); + const model = + effectiveProvider === 'gemini' + ? createGoogleGenerativeAI({ + apiKey: effectiveApiKey, + ...(baseUrl ? { baseURL: baseUrl } : {}), + })(effectiveModel) + : isOpenRouter + ? openAIProvider.chat(effectiveModel) + : openAIProvider(effectiveModel); + const agentSettings: ToolLoopAgentSettings = { id: `${sessionId}-agent`, model, instructions: resolvedSystemPrompt || undefined, - ...(toolsConfig ? { tools: toolsConfig } : {}), temperature, maxOutputTokens: maxTokens, stopWhen: stepCountIs(stepLimit), - onStepFinish: (stepResult: unknown) => { - const mappedStep = mapStepToReasoning(stepResult, streamedStepCount, sessionId); - streamedStepCount += 1; - agentStream.emitReasoningStep(mappedStep); + onStepFinish: (stepResult: AgentStepResult) => { + for (const call of stepResult.toolCalls) { + const input = getToolInput(call); + agentStream.emitToolInput(call.toolCallId, call.toolName, toRecord(input)); + } + + for (const result of stepResult.toolResults) { + const output = getToolOutput(result); + agentStream.emitToolOutput(result.toolCallId, result.toolName, output); + } }, - }); - debugLog('ToolLoopAgent instantiated', { - id: `${sessionId}-agent`, - temperature, - maxTokens, - stepLimit, - toolKeys: toolsConfig ? Object.keys(toolsConfig) : [], - }); + ...(toolsConfig ? { tools: toolsConfig } : {}), + }; + + const agent = new ToolLoopAgent(agentSettings); context.logger.info( `[AIAgent] Using ${effectiveProvider} model "${effectiveModel}" with ${availableToolsCount} connected tool(s).`, @@ -1327,114 +683,82 @@ Loop the Conversation State output back into the next agent invocation to keep m agentStatus: 'running', }, }); - debugLog('Invoking ToolLoopAgent.generate with payload', { - messages: messagesForModel, - }); - generationResult = await agent.generate({ - messages: messagesForModel as any, - }); - debugLog('Generation result', generationResult); + let generationResult: AgentGenerationResult; + try { + generationResult = await agent.generate({ + messages: messagesForModel, + }); + } catch (genError) { + const errorSummary = formatErrorForLog(genError, LOG_TRUNCATE_LIMIT); + context.logger.error( + `[AIAgent] agent.generate() FAILED (truncated): ${safeStringify( + errorSummary, + LOG_TRUNCATE_LIMIT, + )}`, + ); + throw genError; + } - responseText = - typeof generationResult.text === 'string' - ? generationResult.text - : String(generationResult.text ?? ''); - } - debugLog('Response text', responseText); - - const currentTimestamp = new Date().toISOString(); - debugLog('Current timestamp', currentTimestamp); - - const getToolArgs = (entity: any) => - entity?.args !== undefined ? entity.args : (entity?.input ?? null); - const getToolOutput = (entity: any) => - entity?.result !== undefined ? entity.result : (entity?.output ?? null); - - const reasoningTrace: ReasoningStep[] = Array.isArray(generationResult.steps) - ? generationResult.steps.map((step: any, index: number) => - mapStepToReasoning(step, index, sessionId), - ) - : []; - debugLog('Reasoning trace', reasoningTrace); - - const toolLogEntries: ToolInvocationEntry[] = Array.isArray(generationResult.toolResults) - ? generationResult.toolResults.map((toolResult: any, index: number) => { - const toolName = toolResult?.toolName ?? 'tool'; - return { - id: `${sessionId}-${toolResult?.toolCallId ?? index + 1}`, - toolName, - args: getToolArgs(toolResult), - result: getToolOutput(toolResult), - timestamp: currentTimestamp, - metadata: toolMetadataByName.get(toolName), - }; - }) - : []; - debugLog('Tool log entries', toolLogEntries); - - const toolMessages: AgentMessage[] = Array.isArray(generationResult.toolResults) - ? generationResult.toolResults.map((toolResult: any) => ({ - role: 'tool', - content: { - toolCallId: toolResult?.toolCallId ?? '', - toolName: toolResult?.toolName ?? 'tool', - args: getToolArgs(toolResult), - result: getToolOutput(toolResult), - }, - })) - : []; - debugLog('Tool messages appended to history', toolMessages); - - const assistantMessage: AgentMessage = { - role: 'assistant', - content: responseText, - }; - debugLog('Assistant message', assistantMessage); + const responseText = generationResult.text; - let updatedMessages = trimConversation([...historyWithUser, ...toolMessages], memorySize); - updatedMessages = trimConversation([...updatedMessages, assistantMessage], memorySize); - debugLog('Updated messages after trimming', updatedMessages); + const toolMessages: AgentMessage[] = generationResult.toolResults.map((toolResult) => ({ + role: 'tool', + content: { + toolCallId: toolResult.toolCallId, + toolName: toolResult.toolName, + input: getToolInput(toolResult), + output: getToolOutput(toolResult), + }, + })); - const combinedToolHistory = [...existingToolHistory, ...toolLogEntries]; - debugLog('Combined tool history', combinedToolHistory); + const assistantMessage: AgentMessage = { + role: 'assistant', + content: responseText, + }; - const nextState: ConversationState = { - sessionId, - messages: updatedMessages, - toolInvocations: combinedToolHistory, - }; - debugLog('Next conversation state', nextState); - - agentStream.emitTextDelta(responseText); - agentStream.emitFinish(generationResult.finishReason ?? 'stop', responseText); - context.emitProgress({ - level: 'info', - message: 'AI agent completed.', - data: { - agentRunId, - agentStatus: 'completed', - }, - }); - debugLog('Final output payload', { - responseText, - conversationState: nextState, - toolInvocations: toolLogEntries, - reasoningTrace, - usage: generationResult.usage, - }); + let updatedMessages = trimConversation([...historyWithUser, ...toolMessages], memorySize); + updatedMessages = trimConversation([...updatedMessages, assistantMessage], memorySize); - return { - responseText, - structuredOutput, - conversationState: nextState, - toolInvocations: toolLogEntries, - reasoningTrace, - usage: generationResult.usage, - rawResponse: generationResult, - agentRunId, - }; + const nextState: ConversationState = { + sessionId, + messages: updatedMessages, + }; + + agentStream.emitTextDelta(responseText); + agentStream.emitFinish(generationResult?.finishReason ?? 'stop', responseText); + context.emitProgress({ + level: 'info', + message: 'AI agent completed.', + data: { + agentRunId, + agentStatus: 'completed', + }, + }); + + return { + responseText, + conversationState: nextState, + agentRunId, + }; + } finally { + if (closeDiscovery) { + await closeDiscovery(); + } + } }, }); componentRegistry.register(definition); + +// Create local type aliases for internal use (inferred types) +type Input = (typeof inputSchema)['__inferred']; +type Output = (typeof outputSchema)['__inferred']; +type Params = (typeof parameterSchema)['__inferred']; + +// Export schema types for the registry +export type AiAgentInput = typeof inputSchema; +export type AiAgentOutput = typeof outputSchema; +export type AiAgentParams = typeof parameterSchema; + +export type { Input as AiAgentInputData, Output as AiAgentOutputData, Params as AiAgentParamsData }; diff --git a/worker/src/components/ai/opencode.ts b/worker/src/components/ai/opencode.ts new file mode 100644 index 00000000..c7842bcc --- /dev/null +++ b/worker/src/components/ai/opencode.ts @@ -0,0 +1,332 @@ +import { z } from 'zod'; +import { + componentRegistry, + ComponentRetryPolicy, + runComponentWithRunner, + defineComponent, + inputs, + outputs, + parameters, + port, + param, +} from '@shipsec/component-sdk'; +import { LLMProviderSchema, llmProviderContractName } from '@shipsec/contracts'; +import { IsolatedContainerVolume } from '../../utils/isolated-volume'; +import { DEFAULT_GATEWAY_URL, getGatewaySessionToken } from './utils'; + +const inputSchema = inputs({ + task: port( + z.string().min(1, 'Task cannot be empty').describe('The investigation task to perform.'), + { + label: 'Task', + description: 'The main objective for the OpenCode agent (e.g. "Investigate these alerts").', + }, + ), + context: port( + z.unknown().optional().describe('Contextual data (JSON) to assist the investigation.'), + { + label: 'Context', + description: 'Optional JSON data providing context (alerts, logs, previous findings).', + connectionType: { kind: 'primitive', name: 'json' }, + allowAny: true, + reason: 'Context is a dynamic JSON object.', + }, + ), + model: port( + LLMProviderSchema() + .default({ + provider: 'openai', + modelId: 'gpt-4o', + }) + .describe('Model configuration for the agent.'), + { + label: 'Model', + description: 'LLM provider configuration.', + connectionType: { kind: 'contract', name: llmProviderContractName, credential: true }, + }, + ), + tools: port(z.unknown().optional().describe('Anchor for tool-mode nodes.'), { + label: 'Connected Tools', + description: 'Connect tool-mode nodes here to expose them to the agent.', + allowAny: true, + reason: 'Tool-mode port acts as a graph anchor; payloads are not consumed directly.', + connectionType: { kind: 'primitive', name: 'json' }, + }), +}); + +const parameterSchema = parameters({ + systemPrompt: param( + z.string().default('').describe('Optional investigator prompt template override.'), + { + label: 'System Prompt', + editor: 'textarea', + rows: 5, + description: 'Override the default investigator prompt template.', + }, + ), + autoApprove: param(z.boolean().default(true).describe('Automatically approve agent actions.'), { + label: 'Auto Approve', + editor: 'boolean', + description: 'If true, the agent runs without user intervention.', + }), + providerConfig: param( + z + .record(z.string(), z.unknown()) + .default({}) + .describe('Additional OpenCode provider configuration.'), + { + label: 'Provider Config', + editor: 'json', + description: 'Additional configuration merged into opencode.jsonc.', + }, + ), +}); + +const outputSchema = outputs({ + report: port(z.string(), { + label: 'Report', + description: 'The final markdown report generated by the agent.', + }), + rawOutput: port(z.string(), { + label: 'Raw Output', + description: 'Full stdout/stderr logs from the agent execution.', + }), +}); + +const definition = defineComponent({ + id: 'core.ai.opencode', + label: 'OpenCode Agent', + category: 'ai', + runner: { + kind: 'docker', + image: 'ghcr.io/anomalyco/opencode', + entrypoint: 'opencode', // We will override this in execution + network: 'host' as const, // Required to access localhost gateway + command: ['help'], + }, + inputs: inputSchema, + outputs: outputSchema, + parameters: parameterSchema, + docs: 'Runs the OpenCode agent to perform autonomous investigations using connected tools.', + retryPolicy: { + maxAttempts: 1, // Agents are expensive/impotent, normally don't retry automatically + initialIntervalSeconds: 2, + maximumIntervalSeconds: 10, + backoffCoefficient: 2, + nonRetryableErrorTypes: ['ValidationError', 'ConfigurationError'], + } satisfies ComponentRetryPolicy, + ui: { + slug: 'opencode-agent', + version: '1.0.0', + type: 'process', + category: 'ai', + description: 'Autonomous coding and investigation agent.', + icon: 'Bot', + author: { + name: 'ShipSecAI', + type: 'shipsecai', + }, + }, + async execute({ inputs, params }, context) { + const { task, context: taskContext, model } = inputs; + const { systemPrompt, autoApprove, providerConfig } = params; + + const { connectedToolNodeIds, organizationId } = context.metadata; + + context.logger.info('[OpenCode] Preparing agent execution...'); + + // 1. Resolve Gateway Token for MCP + let gatewayToken = ''; + if (connectedToolNodeIds && connectedToolNodeIds.length > 0) { + try { + gatewayToken = await getGatewaySessionToken( + context.runId, + organizationId ?? null, + connectedToolNodeIds, + ); + context.logger.info( + `[OpenCode] Generated gateway token for ${connectedToolNodeIds.length} tools.`, + ); + } catch (error) { + context.logger.warn(`[OpenCode] Failed to generate gateway token: ${error}`); + } + } + + // Helper to map provider to OpenCode model string format + const getOpenCodeModelString = ( + model: { provider: string; modelId: string } | undefined, + ): string => { + if (!model) return 'gpt-4o'; + // OpenCode expects models in format: provider/modelId + // Most providers follow this pattern + return `${model.provider}/${model.modelId}`; + }; + + // 2. Prepare opencode.json config + // Note: We use 'host' networking, so we can reach localhost + const mcpConfig = gatewayToken + ? { + mcp: { + 'shipsec-gateway': { + type: 'remote' as const, + url: DEFAULT_GATEWAY_URL, + headers: { + Authorization: `Bearer ${gatewayToken}`, + }, + }, + }, + } + : {}; + + // Build provider config for OpenCode + // Z.AI requires the API key to be in provider.options.apiKey + const providerConfigForOpenCode: Record = { + ...(model?.provider === 'zai-coding-plan' && model.apiKey + ? { + 'zai-coding-plan': { + options: { + apiKey: model.apiKey, + }, + }, + } + : {}), + }; + + const opencodeConfig = { + ...mcpConfig, + ...providerConfigForOpenCode, + autoApprove: autoApprove, + model: getOpenCodeModelString(model), + // Merge in any additional provider config from parameters + ...providerConfig, + }; + + // 3. Prepare Context and Prompt + const contextJson = JSON.stringify(taskContext ?? {}, null, 2); + + // Default investigator prompt if none provided + const defaultPrompt = ` +# Investigation Task +{{TASK}} + +# Context +The following context is available in /workspace/context.json. +Please investigate the issue and generate a detailed report. +`; + + // Build final prompt: use systemPrompt if provided, otherwise use default template + // Always append the task to ensure it's included + let finalPrompt: string; + if (systemPrompt?.trim()) { + finalPrompt = `${systemPrompt}\n\n# Task\n${task}`; + if (taskContext && Object.keys(taskContext).length > 0) { + finalPrompt += + '\n\n# Context\nThe following context is available in /workspace/context.json.'; + } + } else { + finalPrompt = defaultPrompt.replace('{{TASK}}', task); + } + + // 4. Setup Isolated Volume + const tenantId = (context as any).tenantId ?? 'default-tenant'; + const volume = new IsolatedContainerVolume(tenantId, context.runId); + + try { + // 5. Execute Docker Container + // Write a wrapper script to properly execute opencode with file reading + // The script runs inside the container, so $(cat /workspace/prompt.txt) works correctly + // Note: --quiet flag doesn't exist in opencode 1.1.34, use --log-level ERROR instead + const wrapperScript = + '#!/bin/sh\nopencode run --log-level ERROR "$(cat /workspace/prompt.txt)"\n'; + + // Initialize workspace with config, context, prompt, and wrapper script + await volume.initialize({ + 'context.json': contextJson, + 'opencode.jsonc': JSON.stringify(opencodeConfig, null, 2), + 'prompt.txt': finalPrompt, + 'run.sh': wrapperScript, + }); + + const runnerConfig = { + ...definition.runner, + // Override entrypoint to /bin/sh to avoid the image's default 'opencode' entrypoint + // The command will be executed as: /bin/sh /workspace/run.sh + entrypoint: '/bin/sh', + command: ['/workspace/run.sh'], + // Use host network to access localhost gateway + network: 'host' as const, + env: { + // OpenCode loads provider configuration from opencode.jsonc + // API keys are passed via provider config, not env vars + }, + volumes: [ + volume.getVolumeConfig('/workspace', false), // Read-write, mounted at /workspace + ], + workingDir: '/workspace', + }; + + // If we are using host network, we might need to handle port collisions or security? + // For a worker, allow host network is a privileged operation. + // Assumption: The worker environment allows this / is trusted. + + context.emitProgress({ + message: 'Running OpenCode agent...', + level: 'info', + }); + + const runnerResult = await runComponentWithRunner( + runnerConfig, + async (raw) => raw, // Pass through raw result + {}, + context, + ); + + // Parse output + // The runner result handling in runComponentWithRunner usually involves parsing JSON + // if the entrypoint returns JSON. + // But here we are using a custom command that prints markdown to stdout. + + // We expect the result to be in the 'raw' or 'stdout' property of the result object + // depending on how runComponentWithRunner captures it. + // Based on httpx.ts it seems we get an object with { exitCode, stdout, stderr, raw } or similar + // if using a standard docker runner adaptor. + + // Let's assume the raw output from the runner wrapper is what we get. + // In httpx.ts: + // const httpxRunnerOutputSchema = z.object({ + // results: z.array(z.unknown()).optional().default([]), + // raw: z.string().optional().default(''), + // stderr: z.string().optional().default(''), + // exitCode: z.number().optional().default(0), + // }); + + // We should inspect what the runner returns. + // Assuming a generic docker runner returns { stdout, stderr, exitCode } + + let stdout = ''; + let stderr = ''; + + if (typeof runnerResult === 'string') { + stdout = runnerResult; + } else if (isRecord(runnerResult)) { + stdout = (runnerResult.stdout as string) || (runnerResult.raw as string) || ''; + stderr = (runnerResult.stderr as string) || ''; + } + + context.logger.info(`[OpenCode] Finished. Stdout length: ${stdout.length}`); + + return outputSchema.parse({ + report: stdout, // The markdown report is expected in stdout + rawOutput: `STDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`, + }); + } finally { + await volume.cleanup(); + } + }, +}); + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +componentRegistry.register(definition); diff --git a/worker/src/components/ai/utils.ts b/worker/src/components/ai/utils.ts new file mode 100644 index 00000000..cdf118af --- /dev/null +++ b/worker/src/components/ai/utils.ts @@ -0,0 +1,55 @@ +import { ConfigurationError } from '@shipsec/component-sdk'; + +export const DEFAULT_API_BASE_URL = + process.env.STUDIO_API_BASE_URL ?? + process.env.SHIPSEC_API_BASE_URL ?? + process.env.API_BASE_URL ?? + 'http://localhost:3211'; + +export const DEFAULT_GATEWAY_URL = `${DEFAULT_API_BASE_URL}/mcp/gateway`; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export async function getGatewaySessionToken( + runId: string, + organizationId: string | null, + connectedToolNodeIds?: string[], +): Promise { + const internalToken = process.env.INTERNAL_SERVICE_TOKEN; + + if (!internalToken) { + throw new ConfigurationError( + 'INTERNAL_SERVICE_TOKEN env var must be set for agent tool discovery', + { configKey: 'INTERNAL_SERVICE_TOKEN' }, + ); + } + + const url = `${DEFAULT_API_BASE_URL}/internal/mcp/generate-token`; + // If connectedToolNodeIds is empty or undefined, we might still want to generate a token + // without specific allowedNodeIds if the API supports it, or handle it otherwise. + // The original code passed `allowedNodeIds: connectedToolNodeIds`. + const body = { runId, organizationId, allowedNodeIds: connectedToolNodeIds }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Internal-Token': internalToken, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to generate gateway session token: ${errorText}`); + } + + const payload = await response.json(); + const token = isRecord(payload) && typeof payload.token === 'string' ? payload.token : null; + if (!token) { + throw new Error('Failed to generate gateway session token: invalid response shape'); + } + return token; +} diff --git a/worker/src/components/core/logic-script.ts b/worker/src/components/core/logic-script.ts index 94a6f498..f9c1aac9 100644 --- a/worker/src/components/core/logic-script.ts +++ b/worker/src/components/core/logic-script.ts @@ -172,13 +172,22 @@ async function run() { console.log('[Script] Execution completed, writing output...'); const OUTPUT_PATH = process.env.SHIPSEC_OUTPUT_PATH || '/shipsec-output/result.json'; + const OUTPUT_DIR = OUTPUT_PATH.substring(0, OUTPUT_PATH.lastIndexOf('/')); - // Write output to mounted file instead of stdout - await Bun.write(OUTPUT_PATH, JSON.stringify(result || {})); - - console.log('[Script] Output written to', OUTPUT_PATH); + try { + const { mkdirSync, writeFileSync, existsSync } = await import("node:fs"); + if (!existsSync(OUTPUT_DIR)) { + mkdirSync(OUTPUT_DIR, { recursive: true }); + } + writeFileSync(OUTPUT_PATH, JSON.stringify(result || {})); + console.log('[Script] Output written to', OUTPUT_PATH); + } catch (writeErr) { + console.error('[Script] Failed to write output:', writeErr.message); + throw writeErr; + } } catch (err) { - console.error('Runtime Error:', err.stack || err.message); + console.error('Runtime Error:', err.message); + if (err.stack) console.error(err.stack); process.exit(1); } } @@ -198,7 +207,7 @@ const baseRunner: DockerRunnerConfig = { command: ['-c', ''], // Will be set dynamically in execute() env: {}, network: 'bridge', // Need network access for fetch() and HTTP imports - timeoutSeconds: 30, + timeoutSeconds: 60, stdinJson: false, // Inputs are passed via mounted file now }; @@ -284,7 +293,7 @@ const definition = defineComponent({ context.emitProgress({ message: 'Starting script execution in Docker...', level: 'info', - data: { inputCount: Object.keys(params).length }, + data: { inputCount: Object.keys(params).length, returnsCount: variables.length }, }); // 5. Execute using the Docker runner diff --git a/worker/src/components/core/mcp-runtime.ts b/worker/src/components/core/mcp-runtime.ts new file mode 100644 index 00000000..7c1c7e87 --- /dev/null +++ b/worker/src/components/core/mcp-runtime.ts @@ -0,0 +1,82 @@ +import { createServer } from 'node:net'; +import { runComponentWithRunner, ValidationError } from '@shipsec/component-sdk'; + +interface StartMcpServerInput { + image: string; + command?: string[]; + args?: string[]; + env?: Record; + port?: number; + volumes?: { + source: string; + target: string; + readOnly?: boolean; + }[]; + params: Record; + context: any; +} + +interface StartMcpServerOutput { + endpoint: string; + containerId?: string; +} + +async function getAvailablePort(): Promise { + return await new Promise((resolve, reject) => { + const server = createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, '0.0.0.0', () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + server.close((closeErr) => { + if (closeErr) { + reject(closeErr); + } else { + resolve(port); + } + }); + }); + }); +} + +export async function startMcpDockerServer( + input: StartMcpServerInput, +): Promise { + const port = input.port ?? (await getAvailablePort()); + + if (!input.image || input.image.trim().length === 0) { + throw new ValidationError('Docker image is required for MCP server', { + fieldErrors: { image: ['Docker image is required'] }, + }); + } + + const endpoint = `http://127.0.0.1:${port}/mcp`; + const runnerConfig = { + kind: 'docker' as const, + image: input.image, + command: [...(input.command ?? []), ...(input.args ?? [])], + env: { ...input.env, PORT: String(port), ENDPOINT: endpoint }, + network: 'bridge' as const, + detached: true, + ports: { [`127.0.0.1:${port}`]: port } as unknown as Record, + volumes: input.volumes, + }; + + const result = await runComponentWithRunner( + runnerConfig, + async () => ({}), + input.params, + input.context, + ); + + let containerId: string | undefined; + if (result && typeof result === 'object' && 'containerId' in result) { + containerId = (result as { containerId?: string }).containerId; + } + + return { + endpoint, + containerId, + }; +} diff --git a/worker/src/components/core/mcp-server.ts b/worker/src/components/core/mcp-server.ts new file mode 100644 index 00000000..8854107d --- /dev/null +++ b/worker/src/components/core/mcp-server.ts @@ -0,0 +1,144 @@ +import { z } from 'zod'; +import { + componentRegistry, + defineComponent, + inputs, + outputs, + parameters, + param, + port, + ValidationError, +} from '@shipsec/component-sdk'; +import { startMcpDockerServer } from './mcp-runtime'; + +const inputSchema = inputs({}); + +const outputSchema = outputs({ + endpoint: port(z.string().describe('The URL of the MCP server'), { label: 'Endpoint' }), + containerId: port(z.string().optional().describe('The Docker container ID'), { + label: 'Container ID', + hidden: true, + }), +}); + +const parameterSchema = parameters({ + mode: param(z.enum(['http', 'stdio']).default('http').describe('How to launch the MCP server.'), { + label: 'Mode', + editor: 'select', + options: [ + { label: 'HTTP Server', value: 'http' }, + { label: 'Stdio Server (Proxy)', value: 'stdio' }, + ], + description: 'HTTP starts a native MCP HTTP server. Stdio starts a proxy container.', + }), + image: param(z.string().describe('Docker image for the MCP server'), { + label: 'Docker Image', + editor: 'text', + placeholder: 'shipsec/mcp-stdio-proxy:latest', + }), + stdioCommand: param( + z.string().optional().describe('Stdio MCP command to run inside the proxy container.'), + { + label: 'Stdio Command', + editor: 'text', + placeholder: 'uvx', + }, + ), + stdioArgs: param( + z.array(z.string()).default([]).describe('Arguments for the stdio MCP command.'), + { + label: 'Stdio Args', + editor: 'variable-list', + }, + ), + command: param(z.array(z.string()).default([]).describe('Entrypoint command'), { + label: 'Command', + editor: 'variable-list', + }), + args: param(z.array(z.string()).default([]).describe('Arguments for the command'), { + label: 'Arguments', + editor: 'variable-list', + }), + env: param(z.record(z.string(), z.string()).default({}).describe('Environment variables'), { + label: 'Environment Variables', + editor: 'json', + }), + port: param(z.number().default(8080).describe('Internal port the server listens on'), { + label: 'Port', + editor: 'number', + }), +}); + +const definition = defineComponent({ + id: 'core.mcp.server', + label: 'MCP Server', + category: 'mcp', + // The runner configuration here is a placeholder. + // The actual runner config is constructed dynamically in the execute method + // because `this.runner` is not interpolated when used directly in `execute`. + runner: { + kind: 'docker', + image: 'placeholder', + command: [], + detached: true, + }, + inputs: inputSchema, + outputs: outputSchema, + parameters: parameterSchema, + docs: 'Starts an MCP server in a Docker container and registers it as a tool source. Use stdio mode with the MCP stdio proxy image to wrap CLI-based MCP servers.', + ui: { + slug: 'mcp-server', + version: '1.0.0', + type: 'process', + category: 'mcp', + description: 'Run an external Model Context Protocol (MCP) server.', + icon: 'Server', + author: { + name: 'ShipSecAI', + type: 'shipsecai', + }, + agentTool: { + enabled: true, + toolName: 'mcp_server', + toolDescription: 'Expose an MCP server as a tool source.', + }, + isLatest: true, + }, + async execute({ params }, context) { + const serverPort = params.port || 8080; + const isStdioMode = params.mode === 'stdio'; + + if (isStdioMode && !params.stdioCommand) { + throw new ValidationError('Stdio command is required for stdio MCP servers', { + fieldErrors: { stdioCommand: ['Stdio command is required'] }, + }); + } + + const command = isStdioMode ? [] : [...(params.command || []), ...(params.args || [])]; + const env = { + ...params.env, + ...(isStdioMode + ? { + MCP_COMMAND: params.stdioCommand ?? '', + MCP_ARGS: JSON.stringify(params.stdioArgs ?? []), + MCP_PORT: String(serverPort), + } + : {}), + }; + + return startMcpDockerServer({ + image: params.image, + command, + env, + port: serverPort, + params, + context, + }); + }, +}); + +componentRegistry.register(definition); + +export type McpServerInput = typeof inputSchema; +export type McpServerParams = typeof parameterSchema; +export type McpServerOutput = typeof outputSchema; diff --git a/worker/src/components/index.ts b/worker/src/components/index.ts index 00139859..2dea721e 100644 --- a/worker/src/components/index.ts +++ b/worker/src/components/index.ts @@ -27,6 +27,7 @@ import './core/destination-artifact'; import './core/destination-s3'; import './core/text-block'; import './core/workflow-call'; +import './core/mcp-server'; // Manual Action components import './manual-action/manual-approval'; import './manual-action/manual-selection'; @@ -36,6 +37,7 @@ import './ai/gemini-provider'; import './ai/openrouter-provider'; import './ai/ai-agent'; import './ai/llm-generate-text'; +import './ai/opencode'; import './ai/mcp-http-provider'; import './ai/mcp-tool-merge'; @@ -55,6 +57,8 @@ import './security/trufflehog'; import './security/terminal-demo'; import './security/virustotal'; import './security/abuseipdb'; +import './security/aws-cloudtrail-mcp'; +import './security/aws-cloudwatch-mcp'; // GitHub components import './github/connection-provider'; diff --git a/worker/src/components/security/abuseipdb.ts b/worker/src/components/security/abuseipdb.ts index 81802044..c9a5a044 100644 --- a/worker/src/components/security/abuseipdb.ts +++ b/worker/src/components/security/abuseipdb.ts @@ -132,6 +132,10 @@ const definition = defineComponent({ author: { name: 'ShipSecAI', type: 'shipsecai' }, isLatest: true, deprecated: false, + agentTool: { + enabled: true, + toolDescription: 'IP reputation and abuse report lookup (AbuseIPDB).', + }, }, async execute({ inputs, params }, context) { const { ipAddress, apiKey } = inputs; diff --git a/worker/src/components/security/amass.ts b/worker/src/components/security/amass.ts index c3d1a32a..f075c454 100644 --- a/worker/src/components/security/amass.ts +++ b/worker/src/components/security/amass.ts @@ -473,6 +473,10 @@ printf '{"subdomains":%s,"rawOutput":"%s","domainCount":%d,"subdomainCount":%d," }, isLatest: true, deprecated: false, + agentTool: { + enabled: true, + toolDescription: 'Deep subdomain enumeration and attack surface mapping tool (Amass).', + }, example: '`amass enum -d example.com -brute -alts` - Aggressively enumerates subdomains with brute force and alteration engines enabled.', examples: [ diff --git a/worker/src/components/security/aws-cloudtrail-mcp.ts b/worker/src/components/security/aws-cloudtrail-mcp.ts new file mode 100644 index 00000000..8cd76ce6 --- /dev/null +++ b/worker/src/components/security/aws-cloudtrail-mcp.ts @@ -0,0 +1,162 @@ +import { z } from 'zod'; +import { + componentRegistry, + defineComponent, + inputs, + outputs, + parameters, + param, + port, + ValidationError, +} from '@shipsec/component-sdk'; +import { awsCredentialSchema } from '@shipsec/contracts'; +import { startMcpDockerServer } from '../core/mcp-runtime'; +import { IsolatedContainerVolume } from '../../utils/isolated-volume'; + +const inputSchema = inputs({ + credentials: port(awsCredentialSchema(), { + label: 'AWS Credentials', + description: 'AWS credential bundle (access key, secret key, optional session token).', + connectionType: { kind: 'contract', name: 'core.credential.aws', credential: true }, + }), +}); + +const outputSchema = outputs({ + endpoint: port(z.string().describe('The URL of the MCP server'), { label: 'Endpoint' }), + containerId: port(z.string().optional().describe('The Docker container ID'), { + label: 'Container ID', + hidden: true, + }), +}); + +const parameterSchema = parameters({ + image: param(z.string().default('shipsec/mcp-aws-cloudtrail:latest'), { + label: 'Docker Image', + editor: 'text', + }), + region: param(z.string().optional().describe('AWS region for CloudTrail queries.'), { + label: 'Region', + editor: 'text', + placeholder: 'us-east-1', + }), + port: param(z.number().optional().describe('Internal port the MCP proxy listens on'), { + label: 'Port', + editor: 'number', + }), + extraArgs: param(z.array(z.string()).default([]).describe('Extra args for the MCP server.'), { + label: 'Extra Args', + editor: 'variable-list', + }), +}); + +const definition = defineComponent({ + id: 'security.aws-cloudtrail-mcp', + label: 'AWS CloudTrail MCP Server', + category: 'mcp', + runner: { + kind: 'docker', + image: 'placeholder', + command: [], + detached: true, + }, + inputs: inputSchema, + outputs: outputSchema, + parameters: parameterSchema, + docs: 'Runs the AWS CloudTrail MCP server in a container and exposes it via the MCP gateway (tool-mode only).', + ui: { + slug: 'aws-cloudtrail-mcp', + version: '1.0.0', + type: 'process', + category: 'mcp', + description: 'Expose AWS CloudTrail via MCP for tool-mode agents.', + icon: 'Plug', + author: { + name: 'ShipSecAI', + type: 'shipsecai', + }, + agentTool: { + enabled: true, + toolName: 'aws_cloudtrail_mcp', + toolDescription: 'Expose AWS CloudTrail MCP tools to agents.', + }, + isLatest: true, + }, + async execute({ inputs, params }, context) { + const credentials = inputs.credentials; + if (!credentials?.accessKeyId || !credentials?.secretAccessKey) { + throw new ValidationError('AWS credentials are required for CloudTrail MCP', { + fieldErrors: { credentials: ['AWS credentials are required'] }, + }); + } + + const region = params.region || credentials.region || 'us-east-1'; + const port = params.port; + + const env: Record = { + AWS_ACCESS_KEY_ID: credentials.accessKeyId, + AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey, + AWS_REGION: region, + AWS_DEFAULT_REGION: region, + }; + + if (params.extraArgs && params.extraArgs.length > 0) { + env.MCP_ARGS = JSON.stringify(params.extraArgs); + } + + if (port) { + env.MCP_PORT = String(port); + } + + if (credentials.sessionToken) { + env.AWS_SESSION_TOKEN = credentials.sessionToken; + } + + env.AWS_SHARED_CREDENTIALS_FILE = '/root/.aws/credentials'; + env.AWS_CONFIG_FILE = '/root/.aws/config'; + env.AWS_PROFILE = 'default'; + + const tenantId = (context as any).tenantId ?? 'default-tenant'; + const volume = new IsolatedContainerVolume(tenantId, context.runId); + let volumeInitialized = false; + + try { + const credsLines = [ + '[default]', + `aws_access_key_id = ${credentials.accessKeyId}`, + `aws_secret_access_key = ${credentials.secretAccessKey}`, + ]; + if (credentials.sessionToken) { + credsLines.push(`aws_session_token = ${credentials.sessionToken}`); + } + + const configLines = ['[default]', `region = ${region}`, 'output = json']; + + await volume.initialize({ + credentials: credsLines.join('\n'), + config: configLines.join('\n'), + }); + volumeInitialized = true; + + return await startMcpDockerServer({ + image: params.image, + command: [], + env, + port, + params, + context, + volumes: [volume.getVolumeConfig('/root/.aws', true)], + }); + } catch (error) { + if (volumeInitialized) { + await volume.cleanup().catch(() => {}); + } + throw error; + } + }, +}); + +componentRegistry.register(definition); + +export type AwsCloudtrailMcpInput = typeof inputSchema; +export type AwsCloudtrailMcpParams = typeof parameterSchema; +export type AwsCloudtrailMcpOutput = typeof outputSchema; diff --git a/worker/src/components/security/aws-cloudwatch-mcp.ts b/worker/src/components/security/aws-cloudwatch-mcp.ts new file mode 100644 index 00000000..8950c748 --- /dev/null +++ b/worker/src/components/security/aws-cloudwatch-mcp.ts @@ -0,0 +1,162 @@ +import { z } from 'zod'; +import { + componentRegistry, + defineComponent, + inputs, + outputs, + parameters, + param, + port, + ValidationError, +} from '@shipsec/component-sdk'; +import { awsCredentialSchema } from '@shipsec/contracts'; +import { startMcpDockerServer } from '../core/mcp-runtime'; +import { IsolatedContainerVolume } from '../../utils/isolated-volume'; + +const inputSchema = inputs({ + credentials: port(awsCredentialSchema(), { + label: 'AWS Credentials', + description: 'AWS credential bundle (access key, secret key, optional session token).', + connectionType: { kind: 'contract', name: 'core.credential.aws', credential: true }, + }), +}); + +const outputSchema = outputs({ + endpoint: port(z.string().describe('The URL of the MCP server'), { label: 'Endpoint' }), + containerId: port(z.string().optional().describe('The Docker container ID'), { + label: 'Container ID', + hidden: true, + }), +}); + +const parameterSchema = parameters({ + image: param(z.string().default('shipsec/mcp-aws-cloudwatch:latest'), { + label: 'Docker Image', + editor: 'text', + }), + region: param(z.string().optional().describe('AWS region for CloudWatch queries.'), { + label: 'Region', + editor: 'text', + placeholder: 'us-east-1', + }), + port: param(z.number().optional().describe('Internal port the MCP proxy listens on'), { + label: 'Port', + editor: 'number', + }), + extraArgs: param(z.array(z.string()).default([]).describe('Extra args for the MCP server.'), { + label: 'Extra Args', + editor: 'variable-list', + }), +}); + +const definition = defineComponent({ + id: 'security.aws-cloudwatch-mcp', + label: 'AWS CloudWatch MCP Server', + category: 'mcp', + runner: { + kind: 'docker', + image: 'placeholder', + command: [], + detached: true, + }, + inputs: inputSchema, + outputs: outputSchema, + parameters: parameterSchema, + docs: 'Runs the AWS CloudWatch MCP server in a container and exposes it via the MCP gateway (tool-mode only).', + ui: { + slug: 'aws-cloudwatch-mcp', + version: '1.0.0', + type: 'process', + category: 'mcp', + description: 'Expose AWS CloudWatch via MCP for tool-mode agents.', + icon: 'Plug', + author: { + name: 'ShipSecAI', + type: 'shipsecai', + }, + agentTool: { + enabled: true, + toolName: 'aws_cloudwatch_mcp', + toolDescription: 'Expose AWS CloudWatch MCP tools to agents.', + }, + isLatest: true, + }, + async execute({ inputs, params }, context) { + const credentials = inputs.credentials; + if (!credentials?.accessKeyId || !credentials?.secretAccessKey) { + throw new ValidationError('AWS credentials are required for CloudWatch MCP', { + fieldErrors: { credentials: ['AWS credentials are required'] }, + }); + } + + const region = params.region || credentials.region || 'us-east-1'; + const port = params.port; + + const env: Record = { + AWS_ACCESS_KEY_ID: credentials.accessKeyId, + AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey, + AWS_REGION: region, + AWS_DEFAULT_REGION: region, + }; + + if (params.extraArgs && params.extraArgs.length > 0) { + env.MCP_ARGS = JSON.stringify(params.extraArgs); + } + + if (port) { + env.MCP_PORT = String(port); + } + + if (credentials.sessionToken) { + env.AWS_SESSION_TOKEN = credentials.sessionToken; + } + + env.AWS_SHARED_CREDENTIALS_FILE = '/root/.aws/credentials'; + env.AWS_CONFIG_FILE = '/root/.aws/config'; + env.AWS_PROFILE = 'default'; + + const tenantId = (context as any).tenantId ?? 'default-tenant'; + const volume = new IsolatedContainerVolume(tenantId, context.runId); + let volumeInitialized = false; + + try { + const credsLines = [ + '[default]', + `aws_access_key_id = ${credentials.accessKeyId}`, + `aws_secret_access_key = ${credentials.secretAccessKey}`, + ]; + if (credentials.sessionToken) { + credsLines.push(`aws_session_token = ${credentials.sessionToken}`); + } + + const configLines = ['[default]', `region = ${region}`, 'output = json']; + + await volume.initialize({ + credentials: credsLines.join('\n'), + config: configLines.join('\n'), + }); + volumeInitialized = true; + + return await startMcpDockerServer({ + image: params.image, + command: [], + env, + port, + params, + context, + volumes: [volume.getVolumeConfig('/root/.aws', true)], + }); + } catch (error) { + if (volumeInitialized) { + await volume.cleanup().catch(() => {}); + } + throw error; + } + }, +}); + +componentRegistry.register(definition); + +export type AwsCloudwatchMcpInput = typeof inputSchema; +export type AwsCloudwatchMcpParams = typeof parameterSchema; +export type AwsCloudwatchMcpOutput = typeof outputSchema; diff --git a/worker/src/components/security/dnsx.ts b/worker/src/components/security/dnsx.ts index 83d82ce7..2dcf2f12 100644 --- a/worker/src/components/security/dnsx.ts +++ b/worker/src/components/security/dnsx.ts @@ -508,6 +508,10 @@ const definition = defineComponent({ }, isLatest: true, deprecated: false, + agentTool: { + enabled: true, + toolDescription: 'DNS resolution and record lookup tool (dnsx).', + }, }, async execute({ inputs, params }, context) { const parsedParams = parameterSchema.parse(params); diff --git a/worker/src/components/security/httpx.ts b/worker/src/components/security/httpx.ts index 200f8aa4..84198ea5 100644 --- a/worker/src/components/security/httpx.ts +++ b/worker/src/components/security/httpx.ts @@ -245,6 +245,10 @@ const definition = defineComponent({ 'Validate Subfinder or Amass discoveries by probing for live web services.', 'Filter Naabu results to identify hosts exposing HTTP/S services on uncommon ports.', ], + agentTool: { + enabled: true, + toolDescription: 'Live HTTP endpoint probe and metadata collector (httpx).', + }, }, async execute({ inputs, params }, context) { const parsedParams = parameterSchema.parse(params); diff --git a/worker/src/components/security/naabu.ts b/worker/src/components/security/naabu.ts index f7ac5a2a..052aa16a 100644 --- a/worker/src/components/security/naabu.ts +++ b/worker/src/components/security/naabu.ts @@ -301,6 +301,10 @@ eval "$CMD" 'Scan Amass or Subfinder discoveries to identify exposed services.', 'Target a custom list of IPs with tuned rate and retries for stealth scans.', ], + agentTool: { + enabled: true, + toolDescription: 'Fast TCP port scanner (Naabu).', + }, }, async execute({ inputs, params }, context) { const trimmedPorts = params.ports?.trim(); diff --git a/worker/src/components/security/nuclei.ts b/worker/src/components/security/nuclei.ts index 67f20a3e..8e4951a9 100644 --- a/worker/src/components/security/nuclei.ts +++ b/worker/src/components/security/nuclei.ts @@ -290,6 +290,11 @@ const definition = defineComponent({ 'Bulk custom scan: Upload zip archive via Entry Point → File Loader → Nuclei', 'Comprehensive scan: Combine custom archive + built-in templates for complete coverage', ], + agentTool: { + enabled: true, + toolDescription: + 'Fast vulnerability scanner for CVEs, misconfigurations, and exposures using YAML templates.', + }, }, async execute({ inputs, params }, context) { const parsedInputs = inputSchema.parse(inputs); diff --git a/worker/src/components/security/prowler-scan.ts b/worker/src/components/security/prowler-scan.ts index 69452a8b..bc299fd1 100644 --- a/worker/src/components/security/prowler-scan.ts +++ b/worker/src/components/security/prowler-scan.ts @@ -426,6 +426,10 @@ const definition = defineComponent({ 'Run nightly `prowler aws --quick --severity-filter high,critical` scans on production accounts and forward findings into ELK.', 'Use `prowler cloud` with custom flags to generate a multi-cloud compliance snapshot.', ], + agentTool: { + enabled: true, + toolDescription: 'AWS and multi-cloud security assessment tool (Prowler).', + }, }, async execute({ inputs, params }, context) { const parsedInputs = inputSchema.parse(inputs); diff --git a/worker/src/components/security/subfinder.ts b/worker/src/components/security/subfinder.ts index f0e18789..36d023e6 100644 --- a/worker/src/components/security/subfinder.ts +++ b/worker/src/components/security/subfinder.ts @@ -138,6 +138,10 @@ subfinder -silent -dL /inputs/domains.txt 2>/dev/null || true 'Enumerate subdomains for a single target domain prior to Amass or Naabu.', 'Quick passive discovery during scope triage workflows.', ], + agentTool: { + enabled: true, + toolDescription: 'Passive subdomain enumeration tool (Subfinder).', + }, }, async execute({ inputs, params }, context) { const baseRunner = definition.runner; diff --git a/worker/src/components/security/trufflehog.ts b/worker/src/components/security/trufflehog.ts index 731f0b93..3ed88eef 100644 --- a/worker/src/components/security/trufflehog.ts +++ b/worker/src/components/security/trufflehog.ts @@ -349,6 +349,10 @@ const definition = defineComponent({ 'Scan only changes in a Pull Request by setting branch to PR branch and sinceCommit to base branch.', 'Scan last 10 commits in CI/CD using sinceCommit=HEAD~10 to catch recent secrets.', ], + agentTool: { + enabled: true, + toolDescription: 'Secret and credential leakage scanner (TruffleHog).', + }, }, async execute({ inputs, params }, context) { const parsedParams = parameterSchema.parse(params); diff --git a/worker/src/components/security/virustotal.ts b/worker/src/components/security/virustotal.ts index ae83dae3..76bcad0d 100644 --- a/worker/src/components/security/virustotal.ts +++ b/worker/src/components/security/virustotal.ts @@ -95,6 +95,11 @@ const definition = defineComponent({ author: { name: 'ShipSecAI', type: 'shipsecai' }, isLatest: true, deprecated: false, + agentTool: { + enabled: true, + toolDescription: + 'Threat intelligence lookup for IPs, domains, hashes, and URLs (VirusTotal).', + }, }, async execute({ inputs, params }, context) { const { indicator, apiKey } = inputs; diff --git a/worker/src/temporal/activities/mcp.activity.ts b/worker/src/temporal/activities/mcp.activity.ts new file mode 100644 index 00000000..94c7dab3 --- /dev/null +++ b/worker/src/temporal/activities/mcp.activity.ts @@ -0,0 +1,182 @@ +import { + componentRegistry, + ConfigurationError, + getCredentialInputIds, + getToolMetadata, + ServiceError, +} from '@shipsec/component-sdk'; +import { + CleanupLocalMcpActivityInput, + RegisterComponentToolActivityInput, + RegisterLocalMcpActivityInput, + RegisterRemoteMcpActivityInput, +} from '../types'; + +const DEFAULT_API_BASE_URL = + process.env.STUDIO_API_BASE_URL ?? + process.env.SHIPSEC_API_BASE_URL ?? + process.env.API_BASE_URL ?? + 'http://localhost:3211'; + +function normalizeBaseUrl(url: string): string { + return url.endsWith('/') ? url.slice(0, -1) : url; +} + +async function callInternalApi(path: string, body: any) { + const internalToken = process.env.INTERNAL_SERVICE_TOKEN; + if (!internalToken) { + throw new ConfigurationError( + 'INTERNAL_SERVICE_TOKEN env var must be set to call internal MCP registry', + { + configKey: 'INTERNAL_SERVICE_TOKEN', + }, + ); + } + + const baseUrl = normalizeBaseUrl(DEFAULT_API_BASE_URL); + const response = await fetch(`${baseUrl}/internal/mcp/${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Internal-Token': internalToken, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const raw = await response.text().catch(() => ''); + throw new ServiceError(`Failed to call internal MCP registry (${path}): ${raw}`, { + statusCode: response.status, + details: { statusText: response.statusText }, + }); + } + + return response.json(); +} + +export async function registerComponentToolActivity( + input: RegisterComponentToolActivityInput, +): Promise { + await callInternalApi('register-component', input); +} + +export async function registerRemoteMcpActivity( + input: RegisterRemoteMcpActivityInput, +): Promise { + await callInternalApi('register-remote', input); +} + +export async function registerLocalMcpActivity( + input: RegisterLocalMcpActivityInput, +): Promise { + const port = input.port || 8080; + // Use provided endpoint/containerId or fall back to defaults + const endpoint = input.endpoint || `http://localhost:${port}`; + const containerId = input.containerId || `docker-${input.image.replace(/[^a-zA-Z0-9]/g, '-')}`; + + await callInternalApi('register-local', { + ...input, + endpoint, + containerId, + }); +} + +export async function cleanupLocalMcpActivity(input: CleanupLocalMcpActivityInput): Promise { + const response = (await callInternalApi('cleanup', { runId: input.runId })) as { + containerIds?: string[]; + }; + const containerIds = Array.isArray(response?.containerIds) ? response.containerIds : []; + + if (containerIds.length === 0) { + return; + } + + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + await Promise.all( + containerIds.map(async (containerId: string) => { + if (!containerId || typeof containerId !== 'string') return; + if (!/^[a-zA-Z0-9_.-]+$/.test(containerId)) { + console.warn(`[MCP Cleanup] Skipping container with unsafe id: ${containerId}`); + return; + } + try { + await execAsync(`docker rm -f ${containerId}`); + } catch (error) { + console.warn(`[MCP Cleanup] Failed to remove container ${containerId}:`, error); + } + }), + ); + + if (!/^[a-zA-Z0-9_.-]+$/.test(input.runId)) { + console.warn(`[MCP Cleanup] Skipping volume cleanup with unsafe runId: ${input.runId}`); + return; + } + + try { + const { stdout } = await execAsync( + `docker volume ls --filter "label=studio.managed=true" --filter "label=studio.run=${input.runId}" --format "{{.Name}}"`, + ); + const volumeNames = stdout + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + + if (volumeNames.length === 0) { + return; + } + + await Promise.all( + volumeNames.map(async (volumeName) => { + if (!/^[a-zA-Z0-9_.-]+$/.test(volumeName)) { + console.warn(`[MCP Cleanup] Skipping volume with unsafe name: ${volumeName}`); + return; + } + try { + await execAsync(`docker volume rm ${volumeName}`); + } catch (error) { + console.warn(`[MCP Cleanup] Failed to remove volume ${volumeName}:`, error); + } + }), + ); + } catch (error) { + console.warn(`[MCP Cleanup] Failed to list volumes for run ${input.runId}:`, error); + } +} + +export async function prepareAndRegisterToolActivity(input: { + runId: string; + nodeId: string; + componentId: string; + inputs: Record; + params: Record; +}): Promise { + const component = componentRegistry.get(input.componentId); + if (!component) { + throw new ServiceError(`Component ${input.componentId} not found`); + } + + const metadata = getToolMetadata(component); + const credentialIds = getCredentialInputIds(component); + + // Extract credentials from inputs/params + const allInputs = { ...input.inputs, ...input.params }; + const credentials: Record = {}; + for (const id of credentialIds) { + if (id in allInputs) { + credentials[id] = allInputs[id]; + } + } + + await callInternalApi('register-component', { + runId: input.runId, + nodeId: input.nodeId, + toolName: input.nodeId.replace(/[^a-zA-Z0-9]/g, '_'), + componentId: input.componentId, + description: metadata.description, + inputSchema: metadata.inputSchema, + credentials, + }); +} diff --git a/worker/src/temporal/activities/run-component.activity.ts b/worker/src/temporal/activities/run-component.activity.ts index d68ddc76..ed2c65b8 100644 --- a/worker/src/temporal/activities/run-component.activity.ts +++ b/worker/src/temporal/activities/run-component.activity.ts @@ -39,6 +39,41 @@ let globalLogs: WorkflowLogSink | undefined; let globalTerminal: RedisTerminalStreamAdapter | undefined; let globalAgentTracePublisher: AgentTracePublisher | undefined; +const ERROR_LOG_LIMIT = 600; + +function truncateText(value: string, maxLength: number): string { + if (value.length <= maxLength) { + return value; + } + const remaining = value.length - maxLength; + return `${value.slice(0, maxLength)}...(+${remaining} chars)`; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error && typeof error.message === 'string') { + return error.message; + } + return String(error); +} + +function truncateDetails( + details: Record | undefined, + maxLength: number, +): Record | undefined { + if (!details) { + return undefined; + } + try { + const raw = JSON.stringify(details); + if (raw.length <= maxLength) { + return details; + } + return { truncated: true, preview: truncateText(raw, maxLength) }; + } catch { + return { truncated: true, preview: truncateText(String(details), maxLength) }; + } +} + export function initializeComponentActivityServices(options: { storage: IFileStorageService; secrets?: ISecretsService; @@ -83,26 +118,6 @@ export async function runComponentActivity( ): Promise { const { action, inputs, params, warnings = [] } = input; const activityInfo = Context.current().info; - console.log(`🎯 ACTIVITY CALLED - runComponentActivity:`, { - activityId: activityInfo.activityId, - attempt: activityInfo.attempt, - workflowId: activityInfo.workflowExecution?.workflowId ?? 'unknown', - runId: activityInfo.workflowExecution?.runId ?? 'unknown', - componentId: action.componentId, - ref: action.ref, - timestamp: new Date().toISOString(), - }); - - console.log(`📋 Activity input details:`, { - componentId: action.componentId, - ref: action.ref, - hasParams: !!params, - paramKeys: params ? Object.keys(params) : [], - hasInputs: !!inputs, - inputKeys: inputs ? Object.keys(inputs) : [], - warningsCount: warnings.length, - }); - const component = componentRegistry.get(action.componentId); if (!component) { console.error(`[Activity] Component not found: ${action.componentId}`); @@ -118,6 +133,7 @@ export async function runComponentActivity( const joinStrategy = nodeMetadata.joinStrategy; const triggeredBy = nodeMetadata.triggeredBy; const failure = nodeMetadata.failure; + const connectedToolNodeIds = nodeMetadata.connectedToolNodeIds; const correlationId = `${input.runId}:${action.ref}:${activityInfo.activityId}`; const scopedArtifacts = globalArtifacts @@ -144,7 +160,9 @@ export async function runComponentActivity( joinStrategy, triggeredBy, failure, - }, + connectedToolNodeIds, + organizationId: input.organizationId ?? undefined, + } as any, storage: globalStorage, secrets: allowSecrets ? globalSecrets : undefined, artifacts: scopedArtifacts, @@ -372,7 +390,8 @@ export async function runComponentActivity( return { output, activeOutputPorts }; } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + const rawErrorMsg = getErrorMessage(error); + const errorMsg = truncateText(rawErrorMsg, ERROR_LOG_LIMIT); console.error(`[Activity] Failed ${action.ref}: ${errorMsg}`); // Extract error properties without using 'any' @@ -403,7 +422,10 @@ export async function runComponentActivity( typeof (error as { details: unknown }).details === 'object' && (error as { details: unknown }).details !== null ) { - errorDetails = (error as { details: Record }).details; + errorDetails = truncateDetails( + (error as { details: Record }).details, + ERROR_LOG_LIMIT, + ); } // Extract fieldErrors if it's a ValidationError @@ -421,7 +443,10 @@ export async function runComponentActivity( } = { message: errorMsg, type: errorType || 'UnknownError', - stack: error instanceof Error ? error.stack : undefined, + stack: + error instanceof Error && error.stack + ? truncateText(error.stack, ERROR_LOG_LIMIT) + : undefined, details: errorDetails, fieldErrors, }; diff --git a/worker/src/temporal/signals.ts b/worker/src/temporal/signals.ts index de9325fa..e985b81a 100644 --- a/worker/src/temporal/signals.ts +++ b/worker/src/temporal/signals.ts @@ -34,3 +34,63 @@ export interface PendingHumanInput { title: string; createdAt: string; } + +// ============================================================================= +// Tool Call Signals (for MCP Gateway) +// ============================================================================= + +/** + * Request to execute a tool within the workflow context + */ +export interface ToolCallRequest { + /** Unique identifier for this tool call */ + callId: string; + /** The node ID of the registered tool */ + nodeId: string; + /** The component ID to execute */ + componentId: string; + /** Arguments provided by the agent */ + arguments: Record; + /** Pre-bound credentials from tool registration */ + credentials?: Record; + /** Component parameters */ + parameters?: Record; + /** Timestamp when the call was initiated */ + requestedAt: string; +} + +/** + * Result of a tool call execution + */ +export interface ToolCallResult { + /** The call ID this result corresponds to */ + callId: string; + /** Whether the execution was successful */ + success: boolean; + /** The output from the component (if successful) */ + output?: unknown; + /** Error message (if failed) */ + error?: string; + /** Timestamp when the call completed */ + completedAt: string; +} + +/** + * Signal to request tool execution within the workflow + */ +export const executeToolCallSignal = defineSignal<[ToolCallRequest]>('executeToolCall'); + +/** + * Signal sent back when a tool call completes (for external listeners) + * Note: This is informational - results are also stored in workflow state + */ +export const toolCallCompletedSignal = defineSignal<[ToolCallResult]>('toolCallCompleted'); + +// ============================================================================= +// Queries (for polling tool call results) +// ============================================================================= + +/** + * Query to get of a specific tool call result + */ +export const getToolCallResultQuery = 'getToolCallResult'; diff --git a/worker/src/temporal/types.ts b/worker/src/temporal/types.ts index 3a17c016..84ed6ce4 100644 --- a/worker/src/temporal/types.ts +++ b/worker/src/temporal/types.ts @@ -40,6 +40,12 @@ export interface WorkflowNodeMetadata { maxConcurrency?: number; groupId?: string; streamId?: string; + mode?: 'normal' | 'tool'; + toolConfig?: { + boundInputIds: string[]; + exposedInputIds: string[]; + }; + connectedToolNodeIds?: string[]; } export interface WorkflowFailureMetadata { @@ -87,6 +93,7 @@ export interface RunComponentActivityInput { groupId?: string; triggeredBy?: string; failure?: WorkflowFailureMetadata; + connectedToolNodeIds?: string[]; }; } @@ -173,3 +180,52 @@ export interface PrepareRunPayloadActivityInput { parentRunId?: string; parentNodeRef?: string; } + +// MCP Activity types + +export interface RegisterComponentToolActivityInput { + runId: string; + nodeId: string; + toolName: string; + componentId: string; + description: string; + inputSchema: any; + credentials: Record; +} + +export interface RegisterRemoteMcpActivityInput { + runId: string; + nodeId: string; + toolName: string; + description: string; + inputSchema: any; + endpoint: string; + authToken?: string; +} + +export interface RegisterLocalMcpActivityInput { + runId: string; + nodeId: string; + toolName: string; + description: string; + inputSchema: any; + image: string; + command?: string; + args?: string; + env?: Record; + port: number; + endpoint: string; + containerId: string; +} + +export interface PrepareAndRegisterToolActivityInput { + runId: string; + nodeId: string; + componentId: string; + inputs: Record; + params: Record; +} + +export interface CleanupLocalMcpActivityInput { + runId: string; +} diff --git a/worker/src/temporal/workers/dev.worker.ts b/worker/src/temporal/workers/dev.worker.ts index 92f5f23a..88b0ff3e 100644 --- a/worker/src/temporal/workers/dev.worker.ts +++ b/worker/src/temporal/workers/dev.worker.ts @@ -25,6 +25,13 @@ import { } from '../activities/human-input.activity'; import { prepareRunPayloadActivity } from '../activities/run-dispatcher.activity'; import { recordTraceEventActivity, initializeTraceActivity } from '../activities/trace.activity'; +import { + registerComponentToolActivity, + registerLocalMcpActivity, + registerRemoteMcpActivity, + cleanupLocalMcpActivity, + prepareAndRegisterToolActivity, +} from '../activities/mcp.activity'; // ... (existing imports) @@ -40,6 +47,7 @@ import { } from '../../adapters'; import { ConfigurationError } from '@shipsec/component-sdk'; import * as schema from '../../adapters/schema'; +import { logHeartbeat } from '../../utils/debug-logger'; // Load environment variables from .env file config({ path: join(dirname(fileURLToPath(import.meta.url)), '../../..', '.env') }); @@ -211,6 +219,10 @@ async function main() { createHumanInputRequestActivity, cancelHumanInputRequestActivity, recordTraceEventActivity, + registerComponentToolActivity, + registerLocalMcpActivity, + registerRemoteMcpActivity, + cleanupLocalMcpActivity, }).join(', ')}`, ); @@ -243,6 +255,11 @@ async function main() { cancelHumanInputRequestActivity, expireHumanInputRequestActivity, recordTraceEventActivity, + registerComponentToolActivity, + registerLocalMcpActivity, + registerRemoteMcpActivity, + cleanupLocalMcpActivity, + prepareAndRegisterToolActivity, }, bundlerOptions: { ignoreModules: ['child_process'], @@ -323,20 +340,10 @@ async function main() { console.log(`⏳ Worker is now running and waiting for tasks...`); - // Set up periodic status logging + // Set up periodic heartbeat logging (file-based only) setInterval(() => { - console.log( - `💓 Worker heartbeat - Still polling on queue: ${taskQueue} (${new Date().toISOString()})`, - ); - - // Log worker stats to see if we're receiving any tasks - console.log(`📊 Worker stats check:`, { - taskQueue, - namespace, - timestamp: new Date().toISOString(), - workerBuildId: worker.options.buildId || 'default', - }); - }, 15000); // Log every 15 seconds for better debugging + logHeartbeat(taskQueue); + }, 15000); console.log(`🚀 Starting worker.run() - this will block and listen for tasks...`); await worker.run(); diff --git a/worker/src/temporal/workflows/index.ts b/worker/src/temporal/workflows/index.ts index bca583bf..59bff3d2 100644 --- a/worker/src/temporal/workflows/index.ts +++ b/worker/src/temporal/workflows/index.ts @@ -1,6 +1,7 @@ import { ApplicationFailure, condition, + defineQuery, getExternalWorkflowHandle, proxyActivities, setHandler, @@ -11,7 +12,13 @@ import { import type { ComponentRetryPolicy } from '@shipsec/component-sdk'; import { runWorkflowWithScheduler } from '../workflow-scheduler'; import { buildActionPayload } from '../input-resolver'; -import { resolveHumanInputSignal, type HumanInputResolution } from '../signals'; +import { + resolveHumanInputSignal, + executeToolCallSignal, + type HumanInputResolution, + type ToolCallRequest, + type ToolCallResult, +} from '../signals'; import type { ExecutionTriggerMetadata, PreparedRunPayload } from '@shipsec/shared'; import type { RunComponentActivityInput, @@ -20,6 +27,10 @@ import type { RunWorkflowActivityOutput, WorkflowAction, PrepareRunPayloadActivityInput, + RegisterComponentToolActivityInput, + CleanupLocalMcpActivityInput, + RegisterLocalMcpActivityInput, + PrepareAndRegisterToolActivityInput, } from '../types'; const { @@ -28,6 +39,9 @@ const { finalizeRunActivity, createHumanInputRequestActivity, expireHumanInputRequestActivity, + registerLocalMcpActivity, + cleanupLocalMcpActivity, + prepareAndRegisterToolActivity, } = proxyActivities<{ runComponentActivity(input: RunComponentActivityInput): Promise; setRunMetadataActivity(input: { @@ -53,6 +67,10 @@ const { resolveUrl: string; }>; expireHumanInputRequestActivity(requestId: string): Promise; + registerComponentToolActivity(input: RegisterComponentToolActivityInput): Promise; + registerLocalMcpActivity(input: RegisterLocalMcpActivityInput): Promise; + cleanupLocalMcpActivity(input: CleanupLocalMcpActivityInput): Promise; + prepareAndRegisterToolActivity(input: PrepareAndRegisterToolActivityInput): Promise; }>({ startToCloseTimeout: '10 minutes', }); @@ -69,6 +87,31 @@ const { recordTraceEventActivity } = proxyActivities<{ startToCloseTimeout: '1 minute', }); +const MCP_SERVER_COMPONENTS: Record< + string, + { toolName: (params: Record) => string; description: string } +> = { + 'core.mcp.server': { + toolName: (params) => { + const image = typeof params.image === 'string' ? params.image : ''; + return image.split('/').pop()?.split(':')[0] || 'mcp_server'; + }, + description: 'Local MCP Server', + }, + 'security.aws-cloudtrail-mcp': { + toolName: () => 'aws_cloudtrail_mcp', + description: 'AWS CloudTrail MCP Server', + }, + 'security.aws-cloudwatch-mcp': { + toolName: () => 'aws_cloudwatch_mcp', + description: 'AWS CloudWatch MCP Server', + }, +}; + +function isMcpServerComponent(componentId: string): boolean { + return componentId in MCP_SERVER_COMPONENTS; +} + /** * Check if an output indicates a pending approval gate */ @@ -126,6 +169,83 @@ export async function shipsecWorkflowRun( } }); + // Track pending tool calls and their results (for MCP gateway) + const pendingToolCalls = new Map< + string, + { request: ToolCallRequest; resolve: (result: ToolCallResult) => void } + >(); + const toolCallResults = new Map(); + + // Set up signal handler for tool call execution requests + setHandler(executeToolCallSignal, async (request: ToolCallRequest) => { + console.log( + `[Workflow] Received tool call signal: callId=${request.callId}, componentId=${request.componentId}`, + ); + + // Execute the component via runComponentActivity + try { + const activityOutput = await _runComponentActivity({ + runId: input.runId, + workflowId: input.workflowId, + workflowVersionId: input.workflowVersionId, + organizationId: input.organizationId, + action: { + ref: `tool-call:${request.callId}`, + componentId: request.componentId, + }, + // Merge credentials (pre-bound) with agent-provided arguments + inputs: { + ...(request.credentials ?? {}), + ...request.arguments, + }, + params: request.parameters ?? {}, + metadata: { + streamId: request.callId, + }, + }); + + const result: ToolCallResult = { + callId: request.callId, + success: true, + output: activityOutput.output, + completedAt: new Date().toISOString(), + }; + + toolCallResults.set(request.callId, result); + console.log(`[Workflow] Tool call completed: callId=${request.callId}, success=true`); + + // Resolve any pending waiters + const pending = pendingToolCalls.get(request.callId); + if (pending) { + pending.resolve(result); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const result: ToolCallResult = { + callId: request.callId, + success: false, + error: errorMessage, + completedAt: new Date().toISOString(), + }; + + toolCallResults.set(request.callId, result); + console.log(`[Workflow] Tool call failed: callId=${request.callId}, error=${errorMessage}`); + + const pending = pendingToolCalls.get(request.callId); + if (pending) { + pending.resolve(result); + } + } + }); + + // Set up query handler for tool call results + setHandler( + defineQuery('getToolCallResult'), + (callId: string) => { + return toolCallResults.get(callId) ?? null; + }, + ); + console.log(`[Workflow] Starting shipsec workflow run: ${input.runId}`); console.log( `[Workflow] Definition actions:`, @@ -514,11 +634,87 @@ export async function shipsecWorkflowRun( groupId: nodeMetadata?.groupId, triggeredBy, failure, + connectedToolNodeIds: nodeMetadata?.connectedToolNodeIds, }, }; const retryOptions = mapRetryPolicy(action.retryPolicy); + const isToolMode = nodeMetadata?.mode === 'tool'; + + if (isToolMode) { + console.log(`[Workflow] Node ${action.ref} is in tool mode, registering...`); + if (isMcpServerComponent(action.componentId)) { + const { runComponentActivity: runMcp } = proxyActivities<{ + runComponentActivity( + input: RunComponentActivityInput, + ): Promise; + }>({ + startToCloseTimeout: '10 minutes', + retry: retryOptions, + }); + + const mcpOutput = await runMcp(activityInput); + const output = mcpOutput.output as any; + const endpoint = output.endpoint; + const containerId = output.containerId; + + if (!endpoint) { + throw new Error('MCP server output missing endpoint'); + } + + if (!containerId) { + throw new Error('MCP server output missing containerId'); + } + + const mcpMeta = MCP_SERVER_COMPONENTS[action.componentId]; + const toolName = mcpMeta.toolName(mergedParams); + const description = mcpMeta.description; + + await registerLocalMcpActivity({ + runId: input.runId, + nodeId: action.ref, + toolName, + description, + inputSchema: {}, + image: (mergedParams.image as string) || 'unknown', + port: (mergedParams.port as number) || 8080, + endpoint, + containerId, + }); + } else { + await prepareAndRegisterToolActivity({ + runId: input.runId, + nodeId: action.ref, + componentId: action.componentId, + inputs: mergedInputs, + params: mergedParams, + }); + } + + console.log(`[Workflow] Node ${action.ref} registered as tool, setting results.`); + const toolResult = { mode: 'tool', status: 'ready', tools: [] }; + results.set(action.ref, toolResult); + + await recordTraceEventActivity({ + type: 'NODE_COMPLETED', + runId: input.runId, + nodeRef: action.ref, + timestamp: new Date().toISOString(), + outputSummary: toolResult, + level: 'info', + }); + + return { activePorts: ['default', 'tools'] }; + } + + if (isMcpServerComponent(action.componentId)) { + throw ApplicationFailure.nonRetryable( + `Component ${action.componentId} is tool-mode only`, + 'ToolModeOnly', + ); + } + const { runComponentActivity: runComponentWithRetry } = proxyActivities<{ runComponentActivity( input: RunComponentActivityInput, @@ -721,6 +917,9 @@ export async function shipsecWorkflowRun( [{ outputs, error: normalizedError.message }], ); } finally { + await cleanupLocalMcpActivity({ runId: input.runId }).catch((err) => { + console.error(`[Workflow] Failed to cleanup MCP containers for run ${input.runId}`, err); + }); await finalizeRunActivity({ runId: input.runId }).catch((err) => { console.error(`[Workflow] Failed to finalize run ${input.runId}`, err); }); diff --git a/worker/src/utils/debug-logger.ts b/worker/src/utils/debug-logger.ts new file mode 100644 index 00000000..68909aff --- /dev/null +++ b/worker/src/utils/debug-logger.ts @@ -0,0 +1,145 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const DEBUG_LOG_DIR = '/tmp/shipsec-debug'; +const DEBUG_LOG_FILE = path.join(DEBUG_LOG_DIR, 'worker.log'); +const HEARTBEAT_LOG_FILE = path.join(DEBUG_LOG_DIR, 'heartbeat.log'); + +// Ensure debug directory exists +if (!fs.existsSync(DEBUG_LOG_DIR)) { + fs.mkdirSync(DEBUG_LOG_DIR, { recursive: true }); +} + +interface LogEntry { + timestamp: string; + level: 'debug' | 'info' | 'warn' | 'error'; + context: string; + message: string; + data?: unknown; +} + +/** + * Structured logging for debugging + */ +export class DebugLogger { + private context: string; + private enableConsole: boolean; + + constructor(context: string, enableConsole = false) { + this.context = context; + // Only log to console if explicitly enabled or DEBUG_LOGS_CONSOLE env var set + this.enableConsole = enableConsole || process.env.DEBUG_LOGS_CONSOLE === 'true'; + } + + private writeLog(level: string, message: string, data?: unknown) { + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level: level as 'debug' | 'info' | 'warn' | 'error', + context: this.context, + message, + data, + }; + + // Write to file + try { + const logLine = JSON.stringify(entry); + fs.appendFileSync(DEBUG_LOG_FILE, logLine + '\n'); + } catch (_err) { + // Silently fail if we can't write + } + + // Also log to console if enabled + if (this.enableConsole) { + console.log(`[${entry.timestamp}] [${level}] [${this.context}] ${message}`, data ? data : ''); + } + } + + debug(message: string, data?: unknown) { + this.writeLog('debug', message, data); + } + + info(message: string, data?: unknown) { + this.writeLog('info', message, data); + } + + warn(message: string, data?: unknown) { + this.writeLog('warn', message, data); + } + + error(message: string, data?: unknown) { + this.writeLog('error', message, data); + } +} + +/** + * Log heartbeat only to dedicated file, not to console + */ +export function logHeartbeat(taskQueue: string) { + const entry = { + timestamp: new Date().toISOString(), + taskQueue, + }; + + try { + fs.appendFileSync(HEARTBEAT_LOG_FILE, JSON.stringify(entry) + '\n'); + } catch (_err) { + // Silently fail + } +} + +/** + * Get recent logs from debug file + */ +export function getRecentLogs(lines = 100): LogEntry[] { + try { + const content = fs.readFileSync(DEBUG_LOG_FILE, 'utf-8'); + return content + .split('\n') + .filter((line) => line.trim()) + .slice(-lines) + .map((line) => { + try { + return JSON.parse(line); + } catch { + return null; + } + }) + .filter(Boolean) as LogEntry[]; + } catch { + return []; + } +} + +/** + * Get logs filtered by context + */ +export function getLogsByContext(context: string, lines = 100): LogEntry[] { + return getRecentLogs(lines * 2).filter((log) => log.context.includes(context)); +} + +/** + * Get logs filtered by level + */ +export function getLogsByLevel(level: string, lines = 100): LogEntry[] { + return getRecentLogs(lines * 2).filter((log) => log.level === level); +} + +/** + * Clear debug logs + */ +export function clearDebugLogs() { + try { + fs.writeFileSync(DEBUG_LOG_FILE, ''); + fs.writeFileSync(HEARTBEAT_LOG_FILE, ''); + } catch { + // Silently fail + } +} + +export function getDebugLogPath() { + return DEBUG_LOG_FILE; +} + +export function getHeartbeatLogPath() { + return HEARTBEAT_LOG_FILE; +} diff --git a/worker/tsconfig.json b/worker/tsconfig.json index 3abe3fff..9eb353ec 100644 --- a/worker/tsconfig.json +++ b/worker/tsconfig.json @@ -18,7 +18,6 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "emitDeclarationOnly": true, "types": [ "bun-types" ],