From 9ae7e4f4214e12942b4ac719a970e90d4b530c4b Mon Sep 17 00:00:00 2001 From: Nicholas Bucher Date: Tue, 25 Aug 2026 21:14:33 -0400 Subject: [PATCH 01/25] feat(ui): rewrite the web interface on Vite, and give conversations a first-class API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Next.js app is replaced by a Vite + React 19 single-page app. Routing is React Router's, reads go through SWR, components come from antd 6 and styling from Emotion. Settings reach the app at runtime from `window.environmentVariables` rather than being frozen into a build, so one image serves every deployment. The pages follow the v1alpha3 model rather than the one the old app was written for. An agent is not a resource: it is what exists once a Harness admits an AgentTemplate, read out of `AgentTemplate.status.harnesses[]`. The agents landing page says so, in an overview that maps the four concepts — AgentTemplate, Harness, Agent, AgentInstance — onto the Agent Substrate words for the same things, and its three tabs are the way to each. An agent's own page lists its conversations, and a conversation is the chat. Chat runs over A2A on gRPC-Web, which needed a server-side half: - `AgentInstance` gains a name, end to end — migration, sqlc, store, proto, service, gRPC handler and policy — so a conversation can be called something. Additive: an empty name renders by id. - `ListAgentInstances` takes a query rather than six positional arguments, and can narrow to one agent by template and harness. The pair is resolved through `prepared_revision`, so no new column is needed and rows written before the filter existed still match. - The A2A gateway serves an instance in any state for reads, so a suspended conversation can still be opened and its transcript read. - A share resolves to the share *and* the instance's owner, because that is what a share grants: the reader stays themselves, and the token widens what they may read. - The gRPC server can hand its gRPC-Web handler to the HTTP server, so a browser reaches the services over the origin it was served from. The split happens outside the router's middleware chain: that chain is for REST-shaped handlers, and a gRPC-Web frame neither survives it nor needs it. Test coverage is 376 unit tests and 88 browser tests, all against the real pages. What could not be covered honestly is written down in `ui/playwright/DEFERRED.md` with the surface each spec is waiting on, rather than committed as a skipped test — a skipped spec reads as coverage and that list does not. `ui/dev-scripts/` builds a Kind cluster with kagent on it in one command, for anyone who wants to try the app against a real backend. Without a cluster, `ENABLE_MOCK_UI=true` serves every page from in-browser fixtures, and each such page says on itself that the data is not real. Signed-off-by: Nicholas Bucher --- .github/workflows/ci.yaml | 36 +- .github/workflows/ui-chromatic.yaml | 55 - .github/workflows/ui-playwright.yaml | 119 - CLAUDE.md | 79 + go/api/database/client.go | 37 +- go/api/database/models.go | 25 + .../kagent/api/v1alpha1/agent_instances.pb.go | 391 +- .../api/v1alpha1/agent_instances_grpc.pb.go | 38 + .../kagent/api/v1alpha1/agent_templates.pb.go | 679 + .../api/v1alpha1/agent_templates_grpc.pb.go | 283 + .../gen/kagent/api/v1alpha1/agents_grpc.pb.go | 8 + .../gen/kagent/api/v1alpha1/harnesses.pb.go | 677 + .../kagent/api/v1alpha1/harnesses_grpc.pb.go | 299 + go/api/gen/kagent/api/v1alpha1/system.pb.go | 981 +- .../gen/kagent/api/v1alpha1/system_grpc.pb.go | 178 +- go/api/v1alpha3/harness_types.go | 5 + go/core/cmd/controller-v2/main.go | 35 +- .../database/client_agent_instance_test.go | 380 +- go/core/internal/database/client_postgres.go | 165 +- .../database/gen/agent_instance_tasks.sql.go | 45 +- .../database/gen/agent_instances.sql.go | 155 +- go/core/internal/database/gen/models.go | 1 + go/core/internal/database/gen/querier.go | 33 +- .../database/queries/agent_instance_tasks.sql | 13 + .../database/queries/agent_instances.sql | 49 +- go/core/internal/grpcserver/agenttemplate.go | 151 + .../grpcserver/agenttemplate_harness_test.go | 290 + go/core/internal/grpcserver/grpcweb.go | 78 + go/core/internal/grpcserver/grpcweb_test.go | 28 + go/core/internal/grpcserver/harness.go | 171 + go/core/internal/grpcserver/interceptors.go | 61 +- .../internal/grpcserver/interceptors_test.go | 129 + go/core/internal/grpcserver/policy.go | 16 + go/core/internal/grpcserver/policy_test.go | 88 + go/core/internal/grpcserver/server.go | 19 + go/core/internal/grpcserver/system.go | 243 +- go/core/internal/httpserver/server.go | 26 +- .../httpserver/server_grpcweb_test.go | 76 + .../internal/service/agenttemplate/service.go | 180 + .../service/agenttemplate/service_test.go | 252 + go/core/internal/service/harness/service.go | 186 + .../internal/service/harness/service_test.go | 259 + go/core/internal/service/system/service.go | 39 +- .../internal/service/system/service_test.go | 19 + go/core/internal/service/system/substrate.go | 720 + .../internal/service/system/substrate_test.go | 491 + .../internal/service/system/substratecache.go | 149 + .../service/system/substratecache_test.go | 133 + go/core/pkg/app/app.go | 69 +- go/core/pkg/auth/share.go | 19 +- .../core/000017_agent_instance_name.down.sql | 2 + .../core/000017_agent_instance_name.up.sql | 6 + .../substrate/lifecycle_shared.go | 24 +- .../substrate/lifecycle_shared_secret_test.go | 66 + go/core/pkg/sandboxbackend/substrate/list.go | 46 + go/core/v2/a2agateway/gateway.go | 156 +- go/core/v2/a2agateway/gateway_test.go | 501 +- go/core/v2/agentinstance/grpc.go | 11 +- go/core/v2/agentinstance/service.go | 165 +- go/core/v2/agentinstance/service_test.go | 365 +- go/core/v2/translator/compiler_test.go | 6 +- .../v2/translator/kagent/agentcard_test.go | 45 + go/core/v2/translator/kagent/compiler.go | 29 +- go/go.mod | 11 +- go/go.sum | 454 + helm/kagent/files/nginx.conf | 115 +- helm/kagent/files/supervisord.conf | 41 - helm/kagent/templates/_helpers.tpl | 15 - helm/kagent/templates/ui-deployment.yaml | 27 +- helm/kagent/templates/ui-nginx-configmap.yaml | 2 - helm/kagent/tests/security-context_test.yaml | 19 - helm/kagent/tests/ui-deployment_test.yaml | 98 +- .../kagent/tests/ui-nginx-configmap_test.yaml | 145 +- helm/kagent/values.yaml | 36 +- helm/tools/grafana-mcp/templates/_helpers.tpl | 33 + .../grafana-mcp/templates/deployment.yaml | 2 + .../grafana-mcp/tests/deployment_test.yaml | 39 +- helm/tools/grafana-mcp/values.yaml | 5 + .../kagent/api/v1alpha1/agent_instances.proto | 26 + .../kagent/api/v1alpha1/agent_templates.proto | 81 + proto/kagent/api/v1alpha1/agents.proto | 4 + proto/kagent/api/v1alpha1/harnesses.proto | 87 + proto/kagent/api/v1alpha1/system.proto | 206 + .../kagent-adk/src/kagent/adk/_a2a.py | 6 +- ui/.dockerignore | 7 +- ui/.env.example | 92 + ui/.gitignore | 29 +- ui/.prettierignore | 2 + ui/.storybook/main.ts | 53 - ui/.storybook/mocks/agents.ts | 40 - ui/.storybook/mocks/grpc-client.ts | 18 - ui/.storybook/mocks/mcp-apps.ts | 33 - ui/.storybook/mocks/namespaces.ts | 18 - ui/.storybook/mocks/session-shares.ts | 22 - ui/.storybook/mocks/sessions.ts | 38 - ui/.storybook/preview.tsx | 63 - ui/.storybook/vitest.setup.ts | 7 - ui/.yarnrc.yml | 1 + ui/Dockerfile | 82 +- ui/Makefile | 21 +- ui/README.md | 103 +- ui/bunfig.toml | 19 - ui/components.json | 21 - ui/dev-scripts/README.md | 113 + ui/dev-scripts/setup-cluster.sh | 178 + ui/docs/vendor-extensions.md | 515 + ui/eslint.config.mjs | 64 +- ui/index.html | 47 + ui/jest.config.ts | 33 - ui/jest.setup.ts | 84 - ui/next.config.ts | 30 - ui/package-lock.json | 19241 ---------------- ui/package.json | 153 +- ui/playwright.config.ts | 274 +- ui/playwright/DEFERRED.md | 363 + ui/playwright/README.md | 197 +- ui/playwright/backend.ts | 19 - ui/playwright/fixtures/test.ts | 106 +- ui/playwright/globalSetup.ts | 124 + ui/playwright/helpers/a2a.ts | 18 - ui/playwright/helpers/app.ts | 160 + ui/playwright/helpers/extensions.ts | 93 + ui/playwright/helpers/grpc.ts | 131 - ui/playwright/helpers/mockCalls.ts | 108 + ui/playwright/helpers/nav.ts | 86 +- ui/playwright/helpers/page.ts | 59 - ui/playwright/helpers/resources.ts | 22 - ui/playwright/helpers/select.ts | 37 - ui/playwright/live/agent-lifecycle.spec.ts | 108 + ui/playwright/live/helpers/live.ts | 70 + ui/playwright/live/pages.spec.ts | 95 + ui/playwright/mocks/server.mjs | 184 - ui/playwright/scripts/setup.sh | 56 - ui/playwright/setup.ts | 118 - ui/playwright/teardown.ts | 29 - .../agent-templates/agent-templates.spec.ts | 382 + .../tests/agents/agent-chat-entry.spec.ts | 107 + .../tests/agents/agent-conversations.spec.ts | 510 + .../tests/agents/agents-errors.spec.ts | 140 +- ui/playwright/tests/agents/agents.spec.ts | 438 +- ui/playwright/tests/agents/harnesses.spec.ts | 140 + ui/playwright/tests/app-shell.spec.ts | 191 +- ui/playwright/tests/auth/auth-modes.spec.ts | 110 + ui/playwright/tests/chat/agent-rail.spec.ts | 509 + .../tests/chat/agent-sharing.spec.ts | 152 + ui/playwright/tests/chat/chat-errors.spec.ts | 285 +- ui/playwright/tests/chat/chat.spec.ts | 419 +- .../tests/chat/shared-conversation.spec.ts | 72 + ui/playwright/tests/cleanup.spec.ts | 51 - .../extension-points-absent.spec.ts | 61 + .../extension-points.vendor.spec.ts | 179 + .../tests/lists/list-filters.spec.ts | 324 + .../mcp-servers/mcp-servers-errors.spec.ts | 59 +- .../tests/mcp-servers/mcp-servers.spec.ts | 151 +- .../tests/mcp-servers/row-interaction.spec.ts | 67 + .../tests/models/models-errors.spec.ts | 59 +- ui/playwright/tests/models/models.spec.ts | 133 +- .../tests/onboarding/onboarding.spec.ts | 83 - .../prompt-libraries-errors.spec.ts | 16 - .../prompt-libraries/prompt-libraries.spec.ts | 79 - .../prompts/prompt-libraries-errors.spec.ts | 91 + .../tests/prompts/prompt-libraries.spec.ts | 73 + ui/playwright/tests/refresh-toast.spec.ts | 84 + ui/playwright/tests/routing.spec.ts | 95 + ui/playwright/tests/shell-chrome.spec.ts | 123 + .../tests/substrate/substrate-polling.spec.ts | 190 + .../tests/substrate/substrate.spec.ts | 357 + ui/playwright/tests/theme-contrast.spec.ts | 143 + ui/playwright/tsconfig.json | 7 - ui/postcss.config.mjs | 9 - ui/public/env-config.js | 8 + ui/public/login-bg.webp | Bin 269334 -> 0 bytes ui/public/mockServiceWorker.js | 361 + ui/public/sandbox_proxy.html | 92 - ui/scripts/init.sh | 61 +- ui/src/App.tsx | 93 + ui/src/api/ApiError.ts | 159 + ui/src/api/chat/a2aGrpcChatClient.test.ts | 664 + ui/src/api/chat/a2aGrpcChatClient.ts | 772 + ui/src/api/chat/hitl.test.ts | 196 + ui/src/api/chat/hitl.ts | 211 + ui/src/api/chat/index.ts | 64 + ui/src/api/chat/mockChatClient.ts | 600 + ui/src/api/chat/turnMachine.test.ts | 174 + ui/src/api/chat/turnMachine.ts | 229 + ui/src/api/chat/types.ts | 175 + ui/src/api/client.ts | 524 + ui/src/api/config.ts | 82 + ui/src/api/domain/agentInstances.ts | 250 + ui/src/api/domain/agentPairs.test.ts | 188 + ui/src/api/domain/agentPairs.ts | 252 + ui/src/api/domain/agentTemplates.ts | 222 + ui/src/api/domain/agents.test.ts | 74 + ui/src/api/domain/agents.ts | 334 + ui/src/api/domain/common.ts | 86 + ui/src/api/domain/harnesses.ts | 144 + ui/src/api/domain/mcpServers.ts | 94 + ui/src/api/domain/models.ts | 149 + ui/src/api/domain/namespaces.ts | 13 + ui/src/api/domain/prompts.ts | 32 + ui/src/api/domain/sessions.ts | 54 + ui/src/api/domain/substrate.ts | 172 + ui/src/api/endpoints.ts | 85 + ui/src/api/extensionPoints.ts | 215 + ui/src/api/grpc/operations.ts | 1685 ++ ui/src/api/grpc/wire.ts | 145 + ui/src/api/hooks/useAgentBuildingBlocks.ts | 191 + ui/src/api/hooks/useAgentInstances.ts | 193 + ui/src/api/hooks/useAgents.ts | 19 + ui/src/api/hooks/useApiResource.ts | 75 + ui/src/api/hooks/useChat.timeout.test.ts | 71 + ui/src/api/hooks/useChat.transcript.test.ts | 542 + ui/src/api/hooks/useChat.ts | 661 + ui/src/api/hooks/useConversationTitles.ts | 86 + ui/src/api/hooks/useLiveTranscript.ts | 50 + ui/src/api/hooks/useMcpServers.ts | 13 + ui/src/api/hooks/useModels.ts | 38 + ui/src/api/hooks/useNamespaces.ts | 14 + ui/src/api/hooks/usePrompts.ts | 58 + ui/src/api/hooks/useSessionTranscript.ts | 91 + ui/src/api/hooks/useSessions.ts | 21 + ui/src/api/hooks/useSubstrate.ts | 108 + ui/src/api/index.ts | 140 + ui/src/api/operations.test.ts | 1276 + ui/src/api/operations.ts | 470 + ui/src/api/order.test.ts | 86 + ui/src/api/order.ts | 85 + ui/src/api/runtimeConfig.ts | 54 + ui/src/api/shareToken.test.ts | 87 + ui/src/api/shareToken.ts | 217 + ui/src/api/transport.ts | 444 + .../[namespace]/[agentName]/route.ts | 151 - ui/src/app/actions/__tests__/auth.test.ts | 76 - ui/src/app/actions/__tests__/mcp-apps.test.ts | 108 - .../actions/__tests__/promptTemplates.test.ts | 107 - .../actions/__tests__/systemFeedback.test.ts | 116 - ui/src/app/actions/agentHarnessSession.ts | 71 - ui/src/app/actions/agents.ts | 216 - ui/src/app/actions/auth.ts | 42 - ui/src/app/actions/config.ts | 19 - ui/src/app/actions/feedback.ts | 52 - ui/src/app/actions/mcp-apps.ts | 61 - ui/src/app/actions/memories.ts | 28 - ui/src/app/actions/modelConfigs.ts | 111 - ui/src/app/actions/models.ts | 20 - ui/src/app/actions/namespaces.ts | 29 - ui/src/app/actions/promptTemplates.ts | 71 - ui/src/app/actions/providers.ts | 58 - ui/src/app/actions/servers.ts | 90 - ui/src/app/actions/sessionShares.ts | 45 - ui/src/app/actions/sessions.ts | 165 - ui/src/app/actions/substrate.ts | 20 - ui/src/app/actions/tools.ts | 17 - ui/src/app/actions/utils.ts | 11 - .../[namespace]/[name]/chat/[chatId]/page.tsx | 76 - .../agents/[namespace]/[name]/chat/layout.tsx | 98 - .../agents/[namespace]/[name]/chat/page.tsx | 159 - ui/src/app/agents/new-harness/page.tsx | 379 - ui/src/app/agents/new/page.tsx | 743 - ui/src/app/agents/page.tsx | 12 - ui/src/app/apps/[appName]/page.tsx | 53 - ui/src/app/globals.css | 90 - ui/src/app/icon.tsx | 31 - ui/src/app/layout.tsx | 46 - ui/src/app/loading.tsx | 5 - ui/src/app/login/page.tsx | 70 - ui/src/app/mcp/new/page.tsx | 101 - ui/src/app/mcp/page.tsx | 12 - .../new/__tests__/providerSelection.test.tsx | 170 - ui/src/app/models/new/page.tsx | 807 - ui/src/app/models/page.tsx | 7 - ui/src/app/page.tsx | 7 - .../app/prompts/[namespace]/[name]/page.tsx | 105 - ui/src/app/prompts/new/page.tsx | 91 - ui/src/app/prompts/page.tsx | 21 - ui/src/app/substrate/SubstrateStatusPage.tsx | 179 - ui/src/app/substrate/page.tsx | 19 - ui/src/auth/AuthProvider.tsx | 90 + ui/src/auth/auth.test.ts | 222 + ui/src/auth/authContext.ts | 37 + ui/src/auth/index.ts | 20 + ui/src/auth/oauth2ProxyAuthSource.ts | 142 + ui/src/auth/reauthenticate.test.ts | 99 + ui/src/auth/reauthenticate.ts | 97 + ui/src/auth/types.ts | 57 + ui/src/components/AgentCard.stories.tsx | 109 - ui/src/components/AgentCard.tsx | 212 - ui/src/components/AgentGrid.tsx | 23 - ui/src/components/AgentList.tsx | 250 - ui/src/components/AgentListView.tsx | 468 - ui/src/components/AgentsProvider.tsx | 228 - ui/src/components/AppInitializer.tsx | 51 - ui/src/components/ConfirmDialog.tsx | 48 - ui/src/components/DeleteAgentButton.tsx | 110 - ui/src/components/ErrorState.stories.tsx | 35 - ui/src/components/ErrorState.tsx | 38 - ui/src/components/Footer.stories.tsx | 15 - ui/src/components/Footer.tsx | 12 - ui/src/components/Header.stories.tsx | 34 - ui/src/components/Header.tsx | 278 - ui/src/components/LoadingState.stories.tsx | 15 - ui/src/components/LoadingState.tsx | 19 - ui/src/components/MemoriesDialog.tsx | 197 - ui/src/components/ModelCombobox.tsx | 92 - ui/src/components/ModelProviderCombobox.tsx | 207 - ui/src/components/NamespaceCombobox.tsx | 212 - ui/src/components/ProviderCombobox.tsx | 162 - ui/src/components/SandboxBadge.tsx | 15 - ui/src/components/SettingsModal.tsx | 51 - ui/src/components/Structure/AppHeader.tsx | 142 + ui/src/components/Structure/AppLayout.tsx | 74 + ui/src/components/Structure/AppSidebar.tsx | 225 + ui/src/components/Structure/PageFrame.tsx | 74 + ui/src/components/Structure/SidebarFooter.tsx | 171 + ui/src/components/Structure/navItems.ts | 58 + ui/src/components/SubstrateFeatureGate.tsx | 32 - ui/src/components/ThemeProvider.tsx | 11 - ui/src/components/ThemeToggle.stories.tsx | 58 - ui/src/components/ThemeToggle.tsx | 71 - ui/src/components/ToolDisplay.stories.tsx | 142 - ui/src/components/ToolDisplay.tsx | 315 - ui/src/components/UserMenu.tsx | 81 - .../__tests__/DeleteAgentButton.test.tsx | 71 - .../components/__tests__/ThemeToggle.test.tsx | 56 - .../agent-form/AgentHarnessFields.tsx | 680 - .../agent-form/AgentSkillsFormSection.tsx | 374 - .../agent-form/ByoDeploymentFields.tsx | 211 - .../components/agent-form/agent-form-types.ts | 1 - .../components/agent-form/agentUpdate.test.ts | 244 + ui/src/components/agent-form/agentUpdate.ts | 162 + .../agent-form/focusFirstFormError.ts | 79 - .../components/agent-form/form-primitives.tsx | 61 - .../agent-instances/InstanceTags.tsx | 121 + .../agent-instances/LifecycleButton.tsx | 138 + .../RenameConversationButton.tsx | 169 + .../agent-instances/instanceFields.tsx | 160 + .../agent-instances/instanceLabels.test.ts | 243 + .../agent-instances/instanceLabels.ts | 232 + .../agent-template-form/AgentTemplateForm.tsx | 679 + .../agentTemplateDraft.test.ts | 189 + .../agent-template-form/agentTemplateDraft.ts | 269 + .../agent-template-form/unshownFields.ts | 24 + ui/src/components/agent/AgentRail.tsx | 1547 ++ ui/src/components/agent/AgentSwitcher.tsx | 328 + .../agent/agentCapabilities.test.ts | 248 + ui/src/components/agent/agentCapabilities.ts | 152 + ui/src/components/agent/agentUrl.ts | 80 + ui/src/components/agent/controlStyles.ts | 102 + .../KagentLogo.tsx} | 118 +- ui/src/components/chat/AcpChatComposer.tsx | 70 - ui/src/components/chat/AcpChatEmptyState.tsx | 28 - ui/src/components/chat/AcpHarnessChat.tsx | 115 - .../chat/AgentCallDisplay.stories.tsx | 92 - ui/src/components/chat/AgentCallDisplay.tsx | 322 - ui/src/components/chat/AgentContextPanel.tsx | 205 + ui/src/components/chat/AskUserDisplay.tsx | 209 - ui/src/components/chat/AskUserPrompt.tsx | 186 + ui/src/components/chat/ChatAgentContext.tsx | 49 - ui/src/components/chat/ChatComposer.tsx | 146 + .../components/chat/ChatInterface.stories.tsx | 303 - ui/src/components/chat/ChatInterface.tsx | 1082 - ui/src/components/chat/ChatLayoutUI.tsx | 175 - ui/src/components/chat/ChatMcpAppsContext.tsx | 229 - .../components/chat/ChatMessage.stories.tsx | 238 - ui/src/components/chat/ChatMessage.tsx | 225 - ui/src/components/chat/ChatMessageItem.tsx | 121 + ui/src/components/chat/ChatMinimap.tsx | 169 - ui/src/components/chat/ChatTranscript.tsx | 410 + ui/src/components/chat/CodeBlock.stories.tsx | 199 - ui/src/components/chat/CodeBlock.tsx | 58 - .../chat/ConversationDetailsModal.tsx | 75 + ui/src/components/chat/FeedbackDialog.tsx | 104 - ui/src/components/chat/HTMLPreviewDialog.tsx | 79 - .../chat/HarnessActorStatusContext.tsx | 73 - ui/src/components/chat/LLMCallModal.tsx | 143 - ui/src/components/chat/MarkdownContent.tsx | 56 - ui/src/components/chat/MarkdownMessage.tsx | 187 + ui/src/components/chat/NoMessagesState.tsx | 21 - .../components/chat/ResizableAside.test.tsx | 100 + ui/src/components/chat/ResizableAside.tsx | 117 + ui/src/components/chat/ShareButton.tsx | 227 - ui/src/components/chat/ShareDialog.tsx | 316 + .../components/chat/StatusDisplay.stories.tsx | 90 - ui/src/components/chat/StatusDisplay.tsx | 39 - .../chat/StreamingMessage.stories.tsx | 87 - ui/src/components/chat/StreamingMessage.tsx | 20 - ui/src/components/chat/TokenStats.stories.tsx | 87 - ui/src/components/chat/TokenStats.tsx | 25 - ui/src/components/chat/TokenStatsTooltip.tsx | 26 - ui/src/components/chat/ToolCallCard.tsx | 104 + ui/src/components/chat/ToolCallDisplay.tsx | 225 - .../components/chat/ToolCallGroup.stories.tsx | 118 - ui/src/components/chat/ToolCallGroup.tsx | 332 - .../chat/TruncatableText.stories.tsx | 205 - ui/src/components/chat/TruncatableText.tsx | 59 - .../ChatInterface.sendGuard.test.tsx | 307 - .../chat/__tests__/ChatLayoutUI.test.tsx | 82 - .../__tests__/ChatMcpAppsContext.test.tsx | 111 - .../HarnessActorStatusContext.test.tsx | 132 - .../chat/__tests__/ToolCallDisplay.test.tsx | 31 - .../chat/__tests__/ToolCallGroup.test.tsx | 317 - .../chat/__tests__/TruncatableText.test.tsx | 20 - .../components/chat/conversationName.test.ts | 47 + ui/src/components/chat/conversationName.ts | 26 + .../components/chat/lifecycleReading.test.ts | 156 + ui/src/components/chat/lifecycleReading.ts | 206 + ui/src/components/chat/messageText.ts | 14 + ui/src/components/common/SubmitError.tsx | 74 + ui/src/components/common/resourceName.ts | 34 + ui/src/components/create/ContextSection.tsx | 147 - ui/src/components/create/MemorySection.tsx | 145 - .../create/ModelSelectionSection.tsx | 101 - .../PromptInstructionsTextarea.stories.tsx | 91 - .../create/PromptInstructionsTextarea.tsx | 465 - ui/src/components/create/ProviderFilter.tsx | 40 - .../create/SelectToolsDialog.stories.tsx | 165 - .../components/create/SelectToolsDialog.tsx | 1037 - .../create/SystemPromptSection.stories.tsx | 73 - .../components/create/SystemPromptSection.tsx | 86 - ui/src/components/create/ToolsSection.tsx | 370 - .../PromptInstructionsTextarea.test.tsx | 68 - .../create/__tests__/ToolsSection.test.tsx | 224 - .../components/dashboard/ReadinessMeter.tsx | 77 + ui/src/components/dashboard/StatTile.tsx | 78 + .../dashboard/ToolsPerServerChart.tsx | 105 + .../components/dashboard/chartTheme.test.ts | 36 + ui/src/components/dashboard/chartTheme.ts | 134 + ui/src/components/hermes-logo.tsx | 19 - ui/src/components/icons/Anthropic.tsx | 8 - ui/src/components/icons/Azure.tsx | 9 - ui/src/components/icons/Bedrock.tsx | 5 - ui/src/components/icons/Gemini.tsx | 5 - ui/src/components/icons/McpIcon.tsx | 21 - ui/src/components/icons/Ollama.tsx | 17 - ui/src/components/icons/OpenAI.tsx | 12 - ui/src/components/icons/SAPAICore.tsx | 10 - ui/src/components/icons/Twitter.tsx | 7 - ui/src/components/kagent-logo.tsx | 49 - ui/src/components/layout/AppPageFrame.tsx | 56 - ui/src/components/layout/PageHeader.tsx | 77 - ui/src/components/mcp-apps/McpAppRenderer.tsx | 237 - .../components/mcp-apps/McpAppsInspector.tsx | 198 - ui/src/components/mcp/McpPageClient.tsx | 48 - ui/src/components/mcp/McpServerForm.tsx | 652 - ui/src/components/mcp/McpServersView.tsx | 502 - ui/src/components/mcp/ToolServerTools.tsx | 134 + .../components/mcp/mcpServerRequest.test.ts | 74 + ui/src/components/mcp/mcpServerRequest.ts | 328 + ui/src/components/model-form/ModelForm.tsx | 861 + ui/src/components/model-form/ProviderIcon.tsx | 179 + .../components/model-form/modelDraft.test.ts | 218 + ui/src/components/model-form/modelDraft.ts | 366 + ui/src/components/model-form/providerInfo.ts | 97 + .../components/models/ModelsListSection.tsx | 220 - ui/src/components/models/ModelsPageClient.tsx | 97 - ui/src/components/models/new/AuthSection.tsx | 119 - .../models/new/BasicInfoSection.tsx | 244 - .../components/models/new/ParamsSection.tsx | 106 - .../onboarding/OnboardingWizard.tsx | 218 - .../onboarding/steps/AgentSetupStep.tsx | 136 - .../onboarding/steps/FinishStep.tsx | 64 - .../onboarding/steps/ModelConfigStep.tsx | 540 - .../onboarding/steps/ReviewStep.tsx | 99 - .../onboarding/steps/ToolSelectionStep.tsx | 251 - .../onboarding/steps/WelcomeStep.tsx | 64 - ui/src/components/openclaw-logo.tsx | 47 - ui/src/components/prompts/FragmentEditor.tsx | 117 + .../prompts/FragmentEntriesEditor.tsx | 118 - ui/src/components/prompts/PromptFragment.tsx | 59 + .../prompts/PromptLibrariesPanel.tsx | 111 - .../prompts/PromptLibraryCreatePanel.tsx | 107 - .../prompts/PromptLibraryEditorPanel.tsx | 111 - .../components/prompts/PromptsPageClient.tsx | 40 - ui/src/components/prompts/fragmentRows.ts | 60 + .../sidebars/AgentDetailsSidebar.stories.tsx | 174 - .../sidebars/AgentDetailsSidebar.tsx | 458 - .../sidebars/AgentSwitcher.stories.tsx | 102 - ui/src/components/sidebars/AgentSwitcher.tsx | 83 - .../components/sidebars/ChatItem.stories.tsx | 116 - ui/src/components/sidebars/ChatItem.tsx | 143 - .../sidebars/EmptyState.stories.tsx | 22 - ui/src/components/sidebars/EmptyState.tsx | 48 - .../sidebars/GroupedChats.stories.tsx | 95 - ui/src/components/sidebars/GroupedChats.tsx | 205 - .../sidebars/HarnessActorControl.tsx | 95 - .../sidebars/SessionGroup.stories.tsx | 118 - ui/src/components/sidebars/SessionGroup.tsx | 45 - .../components/sidebars/SessionsSidebar.tsx | 60 - .../__tests__/AgentDetailsSidebar.test.tsx | 78 - .../substrate/SubstratePageGuard.tsx | 35 - .../substrate/SubstrateStatusView.tsx | 405 - .../components/table/DeleteResourceButton.tsx | 121 + ui/src/components/table/FilterBar.test.tsx | 178 + ui/src/components/table/FilterBar.tsx | 269 + ui/src/components/table/RefreshButton.tsx | 96 + ui/src/components/table/SearchInput.tsx | 47 + ui/src/components/table/listTable.ts | 106 + ui/src/components/table/useListView.ts | 168 + ui/src/components/tools/CategoryFilter.tsx | 48 - ui/src/components/ui/alert-dialog.tsx | 141 - ui/src/components/ui/alert.tsx | 59 - ui/src/components/ui/badge.tsx | 36 - ui/src/components/ui/button.tsx | 57 - ui/src/components/ui/card.tsx | 76 - ui/src/components/ui/checkbox.tsx | 30 - ui/src/components/ui/collapsible.tsx | 11 - ui/src/components/ui/command.tsx | 153 - ui/src/components/ui/dialog.tsx | 122 - ui/src/components/ui/dropdown-menu.tsx | 201 - ui/src/components/ui/form.tsx | 178 - ui/src/components/ui/input.tsx | 22 - ui/src/components/ui/label.tsx | 26 - ui/src/components/ui/popover.tsx | 33 - ui/src/components/ui/progress.tsx | 28 - ui/src/components/ui/radio-group.tsx | 44 - ui/src/components/ui/scroll-area.tsx | 48 - ui/src/components/ui/select.tsx | 159 - ui/src/components/ui/separator.tsx | 31 - ui/src/components/ui/sheet.tsx | 140 - ui/src/components/ui/sidebar.tsx | 771 - ui/src/components/ui/skeleton.tsx | 15 - ui/src/components/ui/sonner.tsx | 31 - ui/src/components/ui/switch.tsx | 29 - ui/src/components/ui/table.tsx | 120 - ui/src/components/ui/tabs.tsx | 55 - ui/src/components/ui/textarea.tsx | 22 - ui/src/components/ui/tooltip.tsx | 32 - ui/src/contexts/AuthContext.tsx | 95 - ui/src/contexts/SubstrateFeaturesContext.tsx | 100 - .../contexts/__tests__/AuthContext.test.tsx | 107 - ui/src/env.test.ts | 66 + ui/src/env.ts | 115 + .../kagent/api/v1alpha1/agent_instances_pb.ts | 115 +- .../kagent/api/v1alpha1/agent_templates_pb.ts | 294 + .../kagent/api/v1alpha1/agents_pb.ts | 5 + .../kagent/api/v1alpha1/harnesses_pb.ts | 301 + .../kagent/api/v1alpha1/system_pb.ts | 524 +- ui/src/hooks/use-mobile.tsx | 19 - ui/src/hooks/useAcpHarnessChat.ts | 742 - ui/src/hooks/useSpeechRecognition.ts | 198 - .../__tests__/AgentList.namespace.test.tsx | 177 - .../AgentsProvider.namespace.test.tsx | 112 - .../CreateAgentPage.namespace.test.tsx | 75 - ui/src/lib/__tests__/a2aClient.test.ts | 113 - ui/src/lib/__tests__/a2aErrors.test.ts | 13 - ui/src/lib/__tests__/acp.test.ts | 71 - ui/src/lib/__tests__/agentHarnessForm.test.ts | 139 - ui/src/lib/__tests__/agentSkillsForm.test.ts | 454 - ui/src/lib/__tests__/agentsActions.test.ts | 39 - ui/src/lib/__tests__/auth.test.ts | 183 - ui/src/lib/__tests__/countAgentTools.test.ts | 64 - ui/src/lib/__tests__/formatTimeAgo.test.ts | 39 - ui/src/lib/__tests__/hitl.test.ts | 134 - ui/src/lib/__tests__/jwt.test.ts | 48 - ui/src/lib/__tests__/mcpAppInitCompat.test.ts | 104 - ui/src/lib/__tests__/mcpAppToolResult.test.ts | 77 - ui/src/lib/__tests__/messageHandlers.test.ts | 986 - .../lib/__tests__/promptMentionUtils.test.ts | 122 - ui/src/lib/__tests__/providers.test.ts | 70 - ui/src/lib/__tests__/sandboxAgentForm.test.ts | 79 - .../lib/__tests__/sessionTimestamps.test.ts | 45 - ui/src/lib/__tests__/sessionTitle.test.ts | 34 - .../lib/__tests__/toolCallExtraction.test.ts | 137 - ui/src/lib/__tests__/toolUtils.test.ts | 1030 - ui/src/lib/__tests__/utils.test.ts | 123 - ui/src/lib/a2aClient.ts | 196 - ui/src/lib/a2aErrors.ts | 16 - ui/src/lib/acp.ts | 48 - ui/src/lib/agentFormDomain.ts | 622 - ui/src/lib/agentFormLayout.ts | 25 - ui/src/lib/agentHarness.ts | 74 - ui/src/lib/agentHarnessForm.ts | 383 - ui/src/lib/agentSkillsForm.ts | 361 - ui/src/lib/auth.ts | 118 - ui/src/lib/chatSessionGuard.ts | 95 - ui/src/lib/constants.ts | 7 - ui/src/lib/countAgentTools.ts | 37 - ui/src/lib/formatTimeAgo.ts | 50 - ui/src/lib/grpc/client.test.ts | 1069 - ui/src/lib/grpc/client.ts | 1415 -- ui/src/lib/hitl.ts | 243 - ui/src/lib/jwt.ts | 17 - ui/src/lib/k8sUtils.ts | 62 - ui/src/lib/mcpAppInitCompat.ts | 99 - ui/src/lib/mcpAppToolResult.ts | 121 - ui/src/lib/messageHandlers.ts | 1159 - ui/src/lib/promptMentionUtils.ts | 181 - ui/src/lib/promptSourceRow.ts | 11 - ui/src/lib/providers.ts | 111 - ui/src/lib/sandboxAgentForm.ts | 50 - ui/src/lib/sessionTimestamps.ts | 35 - ui/src/lib/sessionTitle.ts | 17 - ui/src/lib/skipToContent.ts | 3 - ui/src/lib/statusUtils.ts | 93 - ui/src/lib/textareaCaret.ts | 86 - ui/src/lib/toolCallExtraction.ts | 143 - ui/src/lib/toolUtils.ts | 278 - ui/src/lib/userStore.ts | 25 - ui/src/lib/utils.ts | 179 - ui/src/main.tsx | 42 + ui/src/mocks/browser.ts | 10 + ui/src/mocks/factories.ts | 187 - ui/src/mocks/fixtures.ts | 1147 + ui/src/mocks/handlers.ts | 47 + ui/src/mocks/mockBackend.test.ts | 506 + ui/src/mocks/scenario.ts | 170 + ui/src/mocks/startMockBackend.ts | 54 + ui/src/mocks/state.ts | 663 + ui/src/mocks/transport.ts | 2100 ++ ui/src/pages/AgentChatPage.tsx | 655 + ui/src/pages/AgentDetailsPage.tsx | 386 + ui/src/pages/AgentNewChatPage.tsx | 313 + ui/src/pages/AgentPage.tsx | 909 + ui/src/pages/AgentTemplateDetailsPage.tsx | 698 + ui/src/pages/AgentTemplateNewPage.tsx | 158 + ui/src/pages/AgentTemplatesPage.tsx | 352 + ui/src/pages/AgentsPage.tsx | 595 + ui/src/pages/AppDetailPage.tsx | 214 + ui/src/pages/DashboardPage.tsx | 283 + ui/src/pages/LoginPage.tsx | 91 + ui/src/pages/McpServerNewPage.tsx | 593 + ui/src/pages/McpServersPage.tsx | 402 + ui/src/pages/ModelEditPage.tsx | 83 + ui/src/pages/ModelNewPage.tsx | 37 + ui/src/pages/ModelsPage.tsx | 272 + ui/src/pages/NotFoundPage.tsx | 116 + ui/src/pages/PromptDetailPage.tsx | 120 + ui/src/pages/PromptNewPage.tsx | 196 + ui/src/pages/PromptsPage.tsx | 279 + ui/src/pages/SharedAgentPage.tsx | 162 + ui/src/pages/SharedSessionPage.tsx | 112 + ui/src/pages/SubstratePage.tsx | 1535 ++ ui/src/pages/UnmappedConversationsPage.tsx | 223 + ui/src/pages/agents/AgentConcepts.tsx | 331 + ui/src/pages/agents/AgentsLandingPage.tsx | 120 + ui/src/pages/agents/HarnessNewPage.tsx | 248 + ui/src/pages/agents/HarnessesTab.tsx | 279 + ui/src/router/router.tsx | 152 + ui/src/router/routes.ts | 151 + ui/src/router/useUrlState.test.tsx | 280 + ui/src/router/useUrlState.ts | 214 + .../stories/pages/CreateAgentPage.stories.tsx | 52 - .../stories/pages/CreateMcpPage.stories.tsx | 48 - .../stories/pages/CreateModelPage.stories.tsx | 37 - .../pages/CreatePromptPage.stories.tsx | 47 - .../stories/pages/ViewAgentsPage.stories.tsx | 39 - ui/src/stories/pages/ViewHomePage.stories.tsx | 29 - ui/src/stories/pages/ViewMcpPage.stories.tsx | 104 - .../stories/pages/ViewModelsPage.stories.tsx | 115 - .../ViewPromptLibraryDetailPage.stories.tsx | 48 - .../stories/pages/ViewPromptsPage.stories.tsx | 55 - ui/src/stories/pages/fixtures.ts | 104 - ui/src/testSetup.ts | 29 + ui/src/theme/GlobalStyles.tsx | 129 + ui/src/theme/theme.ts | 326 + ui/src/theme/themeMode.tsx | 147 + ui/src/types/acp.ts | 42 - ui/src/types/index.ts | 725 - .../VendorExtensionProvider.tsx | 36 + ui/src/vendorExtensions/VendorProviders.tsx | 25 + ui/src/vendorExtensions/VendorSlot.test.tsx | 133 + ui/src/vendorExtensions/VendorSlot.tsx | 56 + ui/src/vendorExtensions/activeConfig.ts | 20 + ui/src/vendorExtensions/api/apiExtension.ts | 101 + .../api/installVendorApiExtension.ts | 110 + ui/src/vendorExtensions/branding.ts | 37 + ui/src/vendorExtensions/composition.ts | 58 + ui/src/vendorExtensions/context.ts | 13 + .../example/ExampleComplianceTierField.tsx | 41 + .../example/ExampleInsightsPage.tsx | 109 + .../example/ExampleNavItem.tsx | 40 + .../vendorExtensions/example/ExampleSlots.tsx | 150 + .../example/ExampleTenantProvider.tsx | 18 + .../example/exampleComplianceTier.ts | 30 + .../example/exampleExtension.tsx | 104 + .../example/exampleFormFields.ts | 54 + .../example/exampleTableColumns.ts | 20 + .../vendorExtensions/example/exampleTenant.ts | 16 + ui/src/vendorExtensions/example/paths.ts | 2 + ui/src/vendorExtensions/extensionPoints.ts | 113 + ui/src/vendorExtensions/formFields.ts | 153 + ui/src/vendorExtensions/hooks.ts | 77 + ui/src/vendorExtensions/index.ts | 114 + .../installActiveExtension.ts | 11 + ui/src/vendorExtensions/navOverrides.ts | 69 + ui/src/vendorExtensions/shell.ts | 53 + ui/src/vendorExtensions/tableColumns.ts | 124 + ui/src/vendorExtensions/theme.test.ts | 118 + ui/src/vendorExtensions/theme.ts | 189 + ui/src/vendorExtensions/types.ts | 235 + ui/src/vendorExtensions/validateConfig.ts | 107 + .../vendorExtensions/vendorExtensions.test.ts | 352 + ui/src/vite-env.d.ts | 12 + ui/tailwind.config.ts | 96 - ui/tsconfig.json | 50 +- ui/vite.config.ts | 137 + ui/vitest.config.ts | 37 - ui/vitest.shims.d.ts | 1 - ui/yarn.lock | 5699 +++++ 699 files changed, 71771 insertions(+), 66491 deletions(-) delete mode 100644 .github/workflows/ui-chromatic.yaml delete mode 100644 .github/workflows/ui-playwright.yaml create mode 100644 go/api/gen/kagent/api/v1alpha1/agent_templates.pb.go create mode 100644 go/api/gen/kagent/api/v1alpha1/agent_templates_grpc.pb.go create mode 100644 go/api/gen/kagent/api/v1alpha1/harnesses.pb.go create mode 100644 go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go create mode 100644 go/core/internal/grpcserver/agenttemplate.go create mode 100644 go/core/internal/grpcserver/agenttemplate_harness_test.go create mode 100644 go/core/internal/grpcserver/grpcweb.go create mode 100644 go/core/internal/grpcserver/grpcweb_test.go create mode 100644 go/core/internal/grpcserver/harness.go create mode 100644 go/core/internal/grpcserver/policy_test.go create mode 100644 go/core/internal/httpserver/server_grpcweb_test.go create mode 100644 go/core/internal/service/agenttemplate/service.go create mode 100644 go/core/internal/service/agenttemplate/service_test.go create mode 100644 go/core/internal/service/harness/service.go create mode 100644 go/core/internal/service/harness/service_test.go create mode 100644 go/core/internal/service/system/substrate.go create mode 100644 go/core/internal/service/system/substrate_test.go create mode 100644 go/core/internal/service/system/substratecache.go create mode 100644 go/core/internal/service/system/substratecache_test.go create mode 100644 go/core/pkg/migrations/core/000017_agent_instance_name.down.sql create mode 100644 go/core/pkg/migrations/core/000017_agent_instance_name.up.sql create mode 100644 go/core/pkg/sandboxbackend/substrate/lifecycle_shared_secret_test.go create mode 100644 go/core/v2/translator/kagent/agentcard_test.go delete mode 100644 helm/kagent/files/supervisord.conf create mode 100644 proto/kagent/api/v1alpha1/agent_templates.proto create mode 100644 proto/kagent/api/v1alpha1/harnesses.proto create mode 100644 ui/.env.example create mode 100644 ui/.prettierignore delete mode 100644 ui/.storybook/main.ts delete mode 100644 ui/.storybook/mocks/agents.ts delete mode 100644 ui/.storybook/mocks/grpc-client.ts delete mode 100644 ui/.storybook/mocks/mcp-apps.ts delete mode 100644 ui/.storybook/mocks/namespaces.ts delete mode 100644 ui/.storybook/mocks/session-shares.ts delete mode 100644 ui/.storybook/mocks/sessions.ts delete mode 100644 ui/.storybook/preview.tsx delete mode 100644 ui/.storybook/vitest.setup.ts create mode 100644 ui/.yarnrc.yml delete mode 100644 ui/bunfig.toml delete mode 100644 ui/components.json create mode 100644 ui/dev-scripts/README.md create mode 100755 ui/dev-scripts/setup-cluster.sh create mode 100644 ui/docs/vendor-extensions.md create mode 100644 ui/index.html delete mode 100644 ui/jest.config.ts delete mode 100644 ui/jest.setup.ts delete mode 100644 ui/next.config.ts delete mode 100644 ui/package-lock.json create mode 100644 ui/playwright/DEFERRED.md delete mode 100644 ui/playwright/backend.ts create mode 100644 ui/playwright/globalSetup.ts delete mode 100644 ui/playwright/helpers/a2a.ts create mode 100644 ui/playwright/helpers/app.ts create mode 100644 ui/playwright/helpers/extensions.ts delete mode 100644 ui/playwright/helpers/grpc.ts create mode 100644 ui/playwright/helpers/mockCalls.ts delete mode 100644 ui/playwright/helpers/page.ts delete mode 100644 ui/playwright/helpers/resources.ts delete mode 100644 ui/playwright/helpers/select.ts create mode 100644 ui/playwright/live/agent-lifecycle.spec.ts create mode 100644 ui/playwright/live/helpers/live.ts create mode 100644 ui/playwright/live/pages.spec.ts delete mode 100644 ui/playwright/mocks/server.mjs delete mode 100755 ui/playwright/scripts/setup.sh delete mode 100644 ui/playwright/setup.ts delete mode 100644 ui/playwright/teardown.ts create mode 100644 ui/playwright/tests/agent-templates/agent-templates.spec.ts create mode 100644 ui/playwright/tests/agents/agent-chat-entry.spec.ts create mode 100644 ui/playwright/tests/agents/agent-conversations.spec.ts create mode 100644 ui/playwright/tests/agents/harnesses.spec.ts create mode 100644 ui/playwright/tests/auth/auth-modes.spec.ts create mode 100644 ui/playwright/tests/chat/agent-rail.spec.ts create mode 100644 ui/playwright/tests/chat/agent-sharing.spec.ts create mode 100644 ui/playwright/tests/chat/shared-conversation.spec.ts delete mode 100644 ui/playwright/tests/cleanup.spec.ts create mode 100644 ui/playwright/tests/extensions/extension-points-absent.spec.ts create mode 100644 ui/playwright/tests/extensions/extension-points.vendor.spec.ts create mode 100644 ui/playwright/tests/lists/list-filters.spec.ts create mode 100644 ui/playwright/tests/mcp-servers/row-interaction.spec.ts delete mode 100644 ui/playwright/tests/onboarding/onboarding.spec.ts delete mode 100644 ui/playwright/tests/prompt-libraries/prompt-libraries-errors.spec.ts delete mode 100644 ui/playwright/tests/prompt-libraries/prompt-libraries.spec.ts create mode 100644 ui/playwright/tests/prompts/prompt-libraries-errors.spec.ts create mode 100644 ui/playwright/tests/prompts/prompt-libraries.spec.ts create mode 100644 ui/playwright/tests/refresh-toast.spec.ts create mode 100644 ui/playwright/tests/routing.spec.ts create mode 100644 ui/playwright/tests/shell-chrome.spec.ts create mode 100644 ui/playwright/tests/substrate/substrate-polling.spec.ts create mode 100644 ui/playwright/tests/substrate/substrate.spec.ts create mode 100644 ui/playwright/tests/theme-contrast.spec.ts delete mode 100644 ui/playwright/tsconfig.json delete mode 100644 ui/postcss.config.mjs create mode 100644 ui/public/env-config.js delete mode 100644 ui/public/login-bg.webp create mode 100644 ui/public/mockServiceWorker.js delete mode 100644 ui/public/sandbox_proxy.html create mode 100644 ui/src/App.tsx create mode 100644 ui/src/api/ApiError.ts create mode 100644 ui/src/api/chat/a2aGrpcChatClient.test.ts create mode 100644 ui/src/api/chat/a2aGrpcChatClient.ts create mode 100644 ui/src/api/chat/hitl.test.ts create mode 100644 ui/src/api/chat/hitl.ts create mode 100644 ui/src/api/chat/index.ts create mode 100644 ui/src/api/chat/mockChatClient.ts create mode 100644 ui/src/api/chat/turnMachine.test.ts create mode 100644 ui/src/api/chat/turnMachine.ts create mode 100644 ui/src/api/chat/types.ts create mode 100644 ui/src/api/client.ts create mode 100644 ui/src/api/config.ts create mode 100644 ui/src/api/domain/agentInstances.ts create mode 100644 ui/src/api/domain/agentPairs.test.ts create mode 100644 ui/src/api/domain/agentPairs.ts create mode 100644 ui/src/api/domain/agentTemplates.ts create mode 100644 ui/src/api/domain/agents.test.ts create mode 100644 ui/src/api/domain/agents.ts create mode 100644 ui/src/api/domain/common.ts create mode 100644 ui/src/api/domain/harnesses.ts create mode 100644 ui/src/api/domain/mcpServers.ts create mode 100644 ui/src/api/domain/models.ts create mode 100644 ui/src/api/domain/namespaces.ts create mode 100644 ui/src/api/domain/prompts.ts create mode 100644 ui/src/api/domain/sessions.ts create mode 100644 ui/src/api/domain/substrate.ts create mode 100644 ui/src/api/endpoints.ts create mode 100644 ui/src/api/extensionPoints.ts create mode 100644 ui/src/api/grpc/operations.ts create mode 100644 ui/src/api/grpc/wire.ts create mode 100644 ui/src/api/hooks/useAgentBuildingBlocks.ts create mode 100644 ui/src/api/hooks/useAgentInstances.ts create mode 100644 ui/src/api/hooks/useAgents.ts create mode 100644 ui/src/api/hooks/useApiResource.ts create mode 100644 ui/src/api/hooks/useChat.timeout.test.ts create mode 100644 ui/src/api/hooks/useChat.transcript.test.ts create mode 100644 ui/src/api/hooks/useChat.ts create mode 100644 ui/src/api/hooks/useConversationTitles.ts create mode 100644 ui/src/api/hooks/useLiveTranscript.ts create mode 100644 ui/src/api/hooks/useMcpServers.ts create mode 100644 ui/src/api/hooks/useModels.ts create mode 100644 ui/src/api/hooks/useNamespaces.ts create mode 100644 ui/src/api/hooks/usePrompts.ts create mode 100644 ui/src/api/hooks/useSessionTranscript.ts create mode 100644 ui/src/api/hooks/useSessions.ts create mode 100644 ui/src/api/hooks/useSubstrate.ts create mode 100644 ui/src/api/index.ts create mode 100644 ui/src/api/operations.test.ts create mode 100644 ui/src/api/operations.ts create mode 100644 ui/src/api/order.test.ts create mode 100644 ui/src/api/order.ts create mode 100644 ui/src/api/runtimeConfig.ts create mode 100644 ui/src/api/shareToken.test.ts create mode 100644 ui/src/api/shareToken.ts create mode 100644 ui/src/api/transport.ts delete mode 100644 ui/src/app/a2a-sandboxes/[namespace]/[agentName]/route.ts delete mode 100644 ui/src/app/actions/__tests__/auth.test.ts delete mode 100644 ui/src/app/actions/__tests__/mcp-apps.test.ts delete mode 100644 ui/src/app/actions/__tests__/promptTemplates.test.ts delete mode 100644 ui/src/app/actions/__tests__/systemFeedback.test.ts delete mode 100644 ui/src/app/actions/agentHarnessSession.ts delete mode 100644 ui/src/app/actions/agents.ts delete mode 100644 ui/src/app/actions/auth.ts delete mode 100644 ui/src/app/actions/config.ts delete mode 100644 ui/src/app/actions/feedback.ts delete mode 100644 ui/src/app/actions/mcp-apps.ts delete mode 100644 ui/src/app/actions/memories.ts delete mode 100644 ui/src/app/actions/modelConfigs.ts delete mode 100644 ui/src/app/actions/models.ts delete mode 100644 ui/src/app/actions/namespaces.ts delete mode 100644 ui/src/app/actions/promptTemplates.ts delete mode 100644 ui/src/app/actions/providers.ts delete mode 100644 ui/src/app/actions/servers.ts delete mode 100644 ui/src/app/actions/sessionShares.ts delete mode 100644 ui/src/app/actions/sessions.ts delete mode 100644 ui/src/app/actions/substrate.ts delete mode 100644 ui/src/app/actions/tools.ts delete mode 100644 ui/src/app/actions/utils.ts delete mode 100644 ui/src/app/agents/[namespace]/[name]/chat/[chatId]/page.tsx delete mode 100644 ui/src/app/agents/[namespace]/[name]/chat/layout.tsx delete mode 100644 ui/src/app/agents/[namespace]/[name]/chat/page.tsx delete mode 100644 ui/src/app/agents/new-harness/page.tsx delete mode 100644 ui/src/app/agents/new/page.tsx delete mode 100644 ui/src/app/agents/page.tsx delete mode 100644 ui/src/app/apps/[appName]/page.tsx delete mode 100644 ui/src/app/globals.css delete mode 100644 ui/src/app/icon.tsx delete mode 100644 ui/src/app/layout.tsx delete mode 100644 ui/src/app/loading.tsx delete mode 100644 ui/src/app/login/page.tsx delete mode 100644 ui/src/app/mcp/new/page.tsx delete mode 100644 ui/src/app/mcp/page.tsx delete mode 100644 ui/src/app/models/new/__tests__/providerSelection.test.tsx delete mode 100644 ui/src/app/models/new/page.tsx delete mode 100644 ui/src/app/models/page.tsx delete mode 100644 ui/src/app/page.tsx delete mode 100644 ui/src/app/prompts/[namespace]/[name]/page.tsx delete mode 100644 ui/src/app/prompts/new/page.tsx delete mode 100644 ui/src/app/prompts/page.tsx delete mode 100644 ui/src/app/substrate/SubstrateStatusPage.tsx delete mode 100644 ui/src/app/substrate/page.tsx create mode 100644 ui/src/auth/AuthProvider.tsx create mode 100644 ui/src/auth/auth.test.ts create mode 100644 ui/src/auth/authContext.ts create mode 100644 ui/src/auth/index.ts create mode 100644 ui/src/auth/oauth2ProxyAuthSource.ts create mode 100644 ui/src/auth/reauthenticate.test.ts create mode 100644 ui/src/auth/reauthenticate.ts create mode 100644 ui/src/auth/types.ts delete mode 100644 ui/src/components/AgentCard.stories.tsx delete mode 100644 ui/src/components/AgentCard.tsx delete mode 100644 ui/src/components/AgentGrid.tsx delete mode 100644 ui/src/components/AgentList.tsx delete mode 100644 ui/src/components/AgentListView.tsx delete mode 100644 ui/src/components/AgentsProvider.tsx delete mode 100644 ui/src/components/AppInitializer.tsx delete mode 100644 ui/src/components/ConfirmDialog.tsx delete mode 100644 ui/src/components/DeleteAgentButton.tsx delete mode 100644 ui/src/components/ErrorState.stories.tsx delete mode 100644 ui/src/components/ErrorState.tsx delete mode 100644 ui/src/components/Footer.stories.tsx delete mode 100644 ui/src/components/Footer.tsx delete mode 100644 ui/src/components/Header.stories.tsx delete mode 100644 ui/src/components/Header.tsx delete mode 100644 ui/src/components/LoadingState.stories.tsx delete mode 100644 ui/src/components/LoadingState.tsx delete mode 100644 ui/src/components/MemoriesDialog.tsx delete mode 100644 ui/src/components/ModelCombobox.tsx delete mode 100644 ui/src/components/ModelProviderCombobox.tsx delete mode 100644 ui/src/components/NamespaceCombobox.tsx delete mode 100644 ui/src/components/ProviderCombobox.tsx delete mode 100644 ui/src/components/SandboxBadge.tsx delete mode 100644 ui/src/components/SettingsModal.tsx create mode 100644 ui/src/components/Structure/AppHeader.tsx create mode 100644 ui/src/components/Structure/AppLayout.tsx create mode 100644 ui/src/components/Structure/AppSidebar.tsx create mode 100644 ui/src/components/Structure/PageFrame.tsx create mode 100644 ui/src/components/Structure/SidebarFooter.tsx create mode 100644 ui/src/components/Structure/navItems.ts delete mode 100644 ui/src/components/SubstrateFeatureGate.tsx delete mode 100644 ui/src/components/ThemeProvider.tsx delete mode 100644 ui/src/components/ThemeToggle.stories.tsx delete mode 100644 ui/src/components/ThemeToggle.tsx delete mode 100644 ui/src/components/ToolDisplay.stories.tsx delete mode 100644 ui/src/components/ToolDisplay.tsx delete mode 100644 ui/src/components/UserMenu.tsx delete mode 100644 ui/src/components/__tests__/DeleteAgentButton.test.tsx delete mode 100644 ui/src/components/__tests__/ThemeToggle.test.tsx delete mode 100644 ui/src/components/agent-form/AgentHarnessFields.tsx delete mode 100644 ui/src/components/agent-form/AgentSkillsFormSection.tsx delete mode 100644 ui/src/components/agent-form/ByoDeploymentFields.tsx delete mode 100644 ui/src/components/agent-form/agent-form-types.ts create mode 100644 ui/src/components/agent-form/agentUpdate.test.ts create mode 100644 ui/src/components/agent-form/agentUpdate.ts delete mode 100644 ui/src/components/agent-form/focusFirstFormError.ts delete mode 100644 ui/src/components/agent-form/form-primitives.tsx create mode 100644 ui/src/components/agent-instances/InstanceTags.tsx create mode 100644 ui/src/components/agent-instances/LifecycleButton.tsx create mode 100644 ui/src/components/agent-instances/RenameConversationButton.tsx create mode 100644 ui/src/components/agent-instances/instanceFields.tsx create mode 100644 ui/src/components/agent-instances/instanceLabels.test.ts create mode 100644 ui/src/components/agent-instances/instanceLabels.ts create mode 100644 ui/src/components/agent-template-form/AgentTemplateForm.tsx create mode 100644 ui/src/components/agent-template-form/agentTemplateDraft.test.ts create mode 100644 ui/src/components/agent-template-form/agentTemplateDraft.ts create mode 100644 ui/src/components/agent-template-form/unshownFields.ts create mode 100644 ui/src/components/agent/AgentRail.tsx create mode 100644 ui/src/components/agent/AgentSwitcher.tsx create mode 100644 ui/src/components/agent/agentCapabilities.test.ts create mode 100644 ui/src/components/agent/agentCapabilities.ts create mode 100644 ui/src/components/agent/agentUrl.ts create mode 100644 ui/src/components/agent/controlStyles.ts rename ui/src/components/{kagent-logo-text.tsx => branding/KagentLogo.tsx} (76%) delete mode 100644 ui/src/components/chat/AcpChatComposer.tsx delete mode 100644 ui/src/components/chat/AcpChatEmptyState.tsx delete mode 100644 ui/src/components/chat/AcpHarnessChat.tsx delete mode 100644 ui/src/components/chat/AgentCallDisplay.stories.tsx delete mode 100644 ui/src/components/chat/AgentCallDisplay.tsx create mode 100644 ui/src/components/chat/AgentContextPanel.tsx delete mode 100644 ui/src/components/chat/AskUserDisplay.tsx create mode 100644 ui/src/components/chat/AskUserPrompt.tsx delete mode 100644 ui/src/components/chat/ChatAgentContext.tsx create mode 100644 ui/src/components/chat/ChatComposer.tsx delete mode 100644 ui/src/components/chat/ChatInterface.stories.tsx delete mode 100644 ui/src/components/chat/ChatInterface.tsx delete mode 100644 ui/src/components/chat/ChatLayoutUI.tsx delete mode 100644 ui/src/components/chat/ChatMcpAppsContext.tsx delete mode 100644 ui/src/components/chat/ChatMessage.stories.tsx delete mode 100644 ui/src/components/chat/ChatMessage.tsx create mode 100644 ui/src/components/chat/ChatMessageItem.tsx delete mode 100644 ui/src/components/chat/ChatMinimap.tsx create mode 100644 ui/src/components/chat/ChatTranscript.tsx delete mode 100644 ui/src/components/chat/CodeBlock.stories.tsx delete mode 100644 ui/src/components/chat/CodeBlock.tsx create mode 100644 ui/src/components/chat/ConversationDetailsModal.tsx delete mode 100644 ui/src/components/chat/FeedbackDialog.tsx delete mode 100644 ui/src/components/chat/HTMLPreviewDialog.tsx delete mode 100644 ui/src/components/chat/HarnessActorStatusContext.tsx delete mode 100644 ui/src/components/chat/LLMCallModal.tsx delete mode 100644 ui/src/components/chat/MarkdownContent.tsx create mode 100644 ui/src/components/chat/MarkdownMessage.tsx delete mode 100644 ui/src/components/chat/NoMessagesState.tsx create mode 100644 ui/src/components/chat/ResizableAside.test.tsx create mode 100644 ui/src/components/chat/ResizableAside.tsx delete mode 100644 ui/src/components/chat/ShareButton.tsx create mode 100644 ui/src/components/chat/ShareDialog.tsx delete mode 100644 ui/src/components/chat/StatusDisplay.stories.tsx delete mode 100644 ui/src/components/chat/StatusDisplay.tsx delete mode 100644 ui/src/components/chat/StreamingMessage.stories.tsx delete mode 100644 ui/src/components/chat/StreamingMessage.tsx delete mode 100644 ui/src/components/chat/TokenStats.stories.tsx delete mode 100644 ui/src/components/chat/TokenStats.tsx delete mode 100644 ui/src/components/chat/TokenStatsTooltip.tsx create mode 100644 ui/src/components/chat/ToolCallCard.tsx delete mode 100644 ui/src/components/chat/ToolCallDisplay.tsx delete mode 100644 ui/src/components/chat/ToolCallGroup.stories.tsx delete mode 100644 ui/src/components/chat/ToolCallGroup.tsx delete mode 100644 ui/src/components/chat/TruncatableText.stories.tsx delete mode 100644 ui/src/components/chat/TruncatableText.tsx delete mode 100644 ui/src/components/chat/__tests__/ChatInterface.sendGuard.test.tsx delete mode 100644 ui/src/components/chat/__tests__/ChatLayoutUI.test.tsx delete mode 100644 ui/src/components/chat/__tests__/ChatMcpAppsContext.test.tsx delete mode 100644 ui/src/components/chat/__tests__/HarnessActorStatusContext.test.tsx delete mode 100644 ui/src/components/chat/__tests__/ToolCallDisplay.test.tsx delete mode 100644 ui/src/components/chat/__tests__/ToolCallGroup.test.tsx delete mode 100644 ui/src/components/chat/__tests__/TruncatableText.test.tsx create mode 100644 ui/src/components/chat/conversationName.test.ts create mode 100644 ui/src/components/chat/conversationName.ts create mode 100644 ui/src/components/chat/lifecycleReading.test.ts create mode 100644 ui/src/components/chat/lifecycleReading.ts create mode 100644 ui/src/components/chat/messageText.ts create mode 100644 ui/src/components/common/SubmitError.tsx create mode 100644 ui/src/components/common/resourceName.ts delete mode 100644 ui/src/components/create/ContextSection.tsx delete mode 100644 ui/src/components/create/MemorySection.tsx delete mode 100644 ui/src/components/create/ModelSelectionSection.tsx delete mode 100644 ui/src/components/create/PromptInstructionsTextarea.stories.tsx delete mode 100644 ui/src/components/create/PromptInstructionsTextarea.tsx delete mode 100644 ui/src/components/create/ProviderFilter.tsx delete mode 100644 ui/src/components/create/SelectToolsDialog.stories.tsx delete mode 100644 ui/src/components/create/SelectToolsDialog.tsx delete mode 100644 ui/src/components/create/SystemPromptSection.stories.tsx delete mode 100644 ui/src/components/create/SystemPromptSection.tsx delete mode 100644 ui/src/components/create/ToolsSection.tsx delete mode 100644 ui/src/components/create/__tests__/PromptInstructionsTextarea.test.tsx delete mode 100644 ui/src/components/create/__tests__/ToolsSection.test.tsx create mode 100644 ui/src/components/dashboard/ReadinessMeter.tsx create mode 100644 ui/src/components/dashboard/StatTile.tsx create mode 100644 ui/src/components/dashboard/ToolsPerServerChart.tsx create mode 100644 ui/src/components/dashboard/chartTheme.test.ts create mode 100644 ui/src/components/dashboard/chartTheme.ts delete mode 100644 ui/src/components/hermes-logo.tsx delete mode 100644 ui/src/components/icons/Anthropic.tsx delete mode 100644 ui/src/components/icons/Azure.tsx delete mode 100644 ui/src/components/icons/Bedrock.tsx delete mode 100644 ui/src/components/icons/Gemini.tsx delete mode 100644 ui/src/components/icons/McpIcon.tsx delete mode 100644 ui/src/components/icons/Ollama.tsx delete mode 100644 ui/src/components/icons/OpenAI.tsx delete mode 100644 ui/src/components/icons/SAPAICore.tsx delete mode 100644 ui/src/components/icons/Twitter.tsx delete mode 100644 ui/src/components/kagent-logo.tsx delete mode 100644 ui/src/components/layout/AppPageFrame.tsx delete mode 100644 ui/src/components/layout/PageHeader.tsx delete mode 100644 ui/src/components/mcp-apps/McpAppRenderer.tsx delete mode 100644 ui/src/components/mcp-apps/McpAppsInspector.tsx delete mode 100644 ui/src/components/mcp/McpPageClient.tsx delete mode 100644 ui/src/components/mcp/McpServerForm.tsx delete mode 100644 ui/src/components/mcp/McpServersView.tsx create mode 100644 ui/src/components/mcp/ToolServerTools.tsx create mode 100644 ui/src/components/mcp/mcpServerRequest.test.ts create mode 100644 ui/src/components/mcp/mcpServerRequest.ts create mode 100644 ui/src/components/model-form/ModelForm.tsx create mode 100644 ui/src/components/model-form/ProviderIcon.tsx create mode 100644 ui/src/components/model-form/modelDraft.test.ts create mode 100644 ui/src/components/model-form/modelDraft.ts create mode 100644 ui/src/components/model-form/providerInfo.ts delete mode 100644 ui/src/components/models/ModelsListSection.tsx delete mode 100644 ui/src/components/models/ModelsPageClient.tsx delete mode 100644 ui/src/components/models/new/AuthSection.tsx delete mode 100644 ui/src/components/models/new/BasicInfoSection.tsx delete mode 100644 ui/src/components/models/new/ParamsSection.tsx delete mode 100644 ui/src/components/onboarding/OnboardingWizard.tsx delete mode 100644 ui/src/components/onboarding/steps/AgentSetupStep.tsx delete mode 100644 ui/src/components/onboarding/steps/FinishStep.tsx delete mode 100644 ui/src/components/onboarding/steps/ModelConfigStep.tsx delete mode 100644 ui/src/components/onboarding/steps/ReviewStep.tsx delete mode 100644 ui/src/components/onboarding/steps/ToolSelectionStep.tsx delete mode 100644 ui/src/components/onboarding/steps/WelcomeStep.tsx delete mode 100644 ui/src/components/openclaw-logo.tsx create mode 100644 ui/src/components/prompts/FragmentEditor.tsx delete mode 100644 ui/src/components/prompts/FragmentEntriesEditor.tsx create mode 100644 ui/src/components/prompts/PromptFragment.tsx delete mode 100644 ui/src/components/prompts/PromptLibrariesPanel.tsx delete mode 100644 ui/src/components/prompts/PromptLibraryCreatePanel.tsx delete mode 100644 ui/src/components/prompts/PromptLibraryEditorPanel.tsx delete mode 100644 ui/src/components/prompts/PromptsPageClient.tsx create mode 100644 ui/src/components/prompts/fragmentRows.ts delete mode 100644 ui/src/components/sidebars/AgentDetailsSidebar.stories.tsx delete mode 100644 ui/src/components/sidebars/AgentDetailsSidebar.tsx delete mode 100644 ui/src/components/sidebars/AgentSwitcher.stories.tsx delete mode 100644 ui/src/components/sidebars/AgentSwitcher.tsx delete mode 100644 ui/src/components/sidebars/ChatItem.stories.tsx delete mode 100644 ui/src/components/sidebars/ChatItem.tsx delete mode 100644 ui/src/components/sidebars/EmptyState.stories.tsx delete mode 100644 ui/src/components/sidebars/EmptyState.tsx delete mode 100644 ui/src/components/sidebars/GroupedChats.stories.tsx delete mode 100644 ui/src/components/sidebars/GroupedChats.tsx delete mode 100644 ui/src/components/sidebars/HarnessActorControl.tsx delete mode 100644 ui/src/components/sidebars/SessionGroup.stories.tsx delete mode 100644 ui/src/components/sidebars/SessionGroup.tsx delete mode 100644 ui/src/components/sidebars/SessionsSidebar.tsx delete mode 100644 ui/src/components/sidebars/__tests__/AgentDetailsSidebar.test.tsx delete mode 100644 ui/src/components/substrate/SubstratePageGuard.tsx delete mode 100644 ui/src/components/substrate/SubstrateStatusView.tsx create mode 100644 ui/src/components/table/DeleteResourceButton.tsx create mode 100644 ui/src/components/table/FilterBar.test.tsx create mode 100644 ui/src/components/table/FilterBar.tsx create mode 100644 ui/src/components/table/RefreshButton.tsx create mode 100644 ui/src/components/table/SearchInput.tsx create mode 100644 ui/src/components/table/listTable.ts create mode 100644 ui/src/components/table/useListView.ts delete mode 100644 ui/src/components/tools/CategoryFilter.tsx delete mode 100644 ui/src/components/ui/alert-dialog.tsx delete mode 100644 ui/src/components/ui/alert.tsx delete mode 100644 ui/src/components/ui/badge.tsx delete mode 100644 ui/src/components/ui/button.tsx delete mode 100644 ui/src/components/ui/card.tsx delete mode 100644 ui/src/components/ui/checkbox.tsx delete mode 100644 ui/src/components/ui/collapsible.tsx delete mode 100644 ui/src/components/ui/command.tsx delete mode 100644 ui/src/components/ui/dialog.tsx delete mode 100644 ui/src/components/ui/dropdown-menu.tsx delete mode 100644 ui/src/components/ui/form.tsx delete mode 100644 ui/src/components/ui/input.tsx delete mode 100644 ui/src/components/ui/label.tsx delete mode 100644 ui/src/components/ui/popover.tsx delete mode 100644 ui/src/components/ui/progress.tsx delete mode 100644 ui/src/components/ui/radio-group.tsx delete mode 100644 ui/src/components/ui/scroll-area.tsx delete mode 100644 ui/src/components/ui/select.tsx delete mode 100644 ui/src/components/ui/separator.tsx delete mode 100644 ui/src/components/ui/sheet.tsx delete mode 100644 ui/src/components/ui/sidebar.tsx delete mode 100644 ui/src/components/ui/skeleton.tsx delete mode 100644 ui/src/components/ui/sonner.tsx delete mode 100644 ui/src/components/ui/switch.tsx delete mode 100644 ui/src/components/ui/table.tsx delete mode 100644 ui/src/components/ui/tabs.tsx delete mode 100644 ui/src/components/ui/textarea.tsx delete mode 100644 ui/src/components/ui/tooltip.tsx delete mode 100644 ui/src/contexts/AuthContext.tsx delete mode 100644 ui/src/contexts/SubstrateFeaturesContext.tsx delete mode 100644 ui/src/contexts/__tests__/AuthContext.test.tsx create mode 100644 ui/src/env.test.ts create mode 100644 ui/src/env.ts create mode 100644 ui/src/generated/kagent/api/v1alpha1/agent_templates_pb.ts create mode 100644 ui/src/generated/kagent/api/v1alpha1/harnesses_pb.ts delete mode 100644 ui/src/hooks/use-mobile.tsx delete mode 100644 ui/src/hooks/useAcpHarnessChat.ts delete mode 100644 ui/src/hooks/useSpeechRecognition.ts delete mode 100644 ui/src/lib/__tests__/AgentList.namespace.test.tsx delete mode 100644 ui/src/lib/__tests__/AgentsProvider.namespace.test.tsx delete mode 100644 ui/src/lib/__tests__/CreateAgentPage.namespace.test.tsx delete mode 100644 ui/src/lib/__tests__/a2aClient.test.ts delete mode 100644 ui/src/lib/__tests__/a2aErrors.test.ts delete mode 100644 ui/src/lib/__tests__/acp.test.ts delete mode 100644 ui/src/lib/__tests__/agentHarnessForm.test.ts delete mode 100644 ui/src/lib/__tests__/agentSkillsForm.test.ts delete mode 100644 ui/src/lib/__tests__/agentsActions.test.ts delete mode 100644 ui/src/lib/__tests__/auth.test.ts delete mode 100644 ui/src/lib/__tests__/countAgentTools.test.ts delete mode 100644 ui/src/lib/__tests__/formatTimeAgo.test.ts delete mode 100644 ui/src/lib/__tests__/hitl.test.ts delete mode 100644 ui/src/lib/__tests__/jwt.test.ts delete mode 100644 ui/src/lib/__tests__/mcpAppInitCompat.test.ts delete mode 100644 ui/src/lib/__tests__/mcpAppToolResult.test.ts delete mode 100644 ui/src/lib/__tests__/messageHandlers.test.ts delete mode 100644 ui/src/lib/__tests__/promptMentionUtils.test.ts delete mode 100644 ui/src/lib/__tests__/providers.test.ts delete mode 100644 ui/src/lib/__tests__/sandboxAgentForm.test.ts delete mode 100644 ui/src/lib/__tests__/sessionTimestamps.test.ts delete mode 100644 ui/src/lib/__tests__/sessionTitle.test.ts delete mode 100644 ui/src/lib/__tests__/toolCallExtraction.test.ts delete mode 100644 ui/src/lib/__tests__/toolUtils.test.ts delete mode 100644 ui/src/lib/__tests__/utils.test.ts delete mode 100644 ui/src/lib/a2aClient.ts delete mode 100644 ui/src/lib/a2aErrors.ts delete mode 100644 ui/src/lib/acp.ts delete mode 100644 ui/src/lib/agentFormDomain.ts delete mode 100644 ui/src/lib/agentFormLayout.ts delete mode 100644 ui/src/lib/agentHarness.ts delete mode 100644 ui/src/lib/agentHarnessForm.ts delete mode 100644 ui/src/lib/agentSkillsForm.ts delete mode 100644 ui/src/lib/auth.ts delete mode 100644 ui/src/lib/chatSessionGuard.ts delete mode 100644 ui/src/lib/constants.ts delete mode 100644 ui/src/lib/countAgentTools.ts delete mode 100644 ui/src/lib/formatTimeAgo.ts delete mode 100644 ui/src/lib/grpc/client.test.ts delete mode 100644 ui/src/lib/grpc/client.ts delete mode 100644 ui/src/lib/hitl.ts delete mode 100644 ui/src/lib/jwt.ts delete mode 100644 ui/src/lib/k8sUtils.ts delete mode 100644 ui/src/lib/mcpAppInitCompat.ts delete mode 100644 ui/src/lib/mcpAppToolResult.ts delete mode 100644 ui/src/lib/messageHandlers.ts delete mode 100644 ui/src/lib/promptMentionUtils.ts delete mode 100644 ui/src/lib/promptSourceRow.ts delete mode 100644 ui/src/lib/providers.ts delete mode 100644 ui/src/lib/sandboxAgentForm.ts delete mode 100644 ui/src/lib/sessionTimestamps.ts delete mode 100644 ui/src/lib/sessionTitle.ts delete mode 100644 ui/src/lib/skipToContent.ts delete mode 100644 ui/src/lib/statusUtils.ts delete mode 100644 ui/src/lib/textareaCaret.ts delete mode 100644 ui/src/lib/toolCallExtraction.ts delete mode 100644 ui/src/lib/toolUtils.ts delete mode 100644 ui/src/lib/userStore.ts delete mode 100644 ui/src/lib/utils.ts create mode 100644 ui/src/main.tsx create mode 100644 ui/src/mocks/browser.ts delete mode 100644 ui/src/mocks/factories.ts create mode 100644 ui/src/mocks/fixtures.ts create mode 100644 ui/src/mocks/handlers.ts create mode 100644 ui/src/mocks/mockBackend.test.ts create mode 100644 ui/src/mocks/scenario.ts create mode 100644 ui/src/mocks/startMockBackend.ts create mode 100644 ui/src/mocks/state.ts create mode 100644 ui/src/mocks/transport.ts create mode 100644 ui/src/pages/AgentChatPage.tsx create mode 100644 ui/src/pages/AgentDetailsPage.tsx create mode 100644 ui/src/pages/AgentNewChatPage.tsx create mode 100644 ui/src/pages/AgentPage.tsx create mode 100644 ui/src/pages/AgentTemplateDetailsPage.tsx create mode 100644 ui/src/pages/AgentTemplateNewPage.tsx create mode 100644 ui/src/pages/AgentTemplatesPage.tsx create mode 100644 ui/src/pages/AgentsPage.tsx create mode 100644 ui/src/pages/AppDetailPage.tsx create mode 100644 ui/src/pages/DashboardPage.tsx create mode 100644 ui/src/pages/LoginPage.tsx create mode 100644 ui/src/pages/McpServerNewPage.tsx create mode 100644 ui/src/pages/McpServersPage.tsx create mode 100644 ui/src/pages/ModelEditPage.tsx create mode 100644 ui/src/pages/ModelNewPage.tsx create mode 100644 ui/src/pages/ModelsPage.tsx create mode 100644 ui/src/pages/NotFoundPage.tsx create mode 100644 ui/src/pages/PromptDetailPage.tsx create mode 100644 ui/src/pages/PromptNewPage.tsx create mode 100644 ui/src/pages/PromptsPage.tsx create mode 100644 ui/src/pages/SharedAgentPage.tsx create mode 100644 ui/src/pages/SharedSessionPage.tsx create mode 100644 ui/src/pages/SubstratePage.tsx create mode 100644 ui/src/pages/UnmappedConversationsPage.tsx create mode 100644 ui/src/pages/agents/AgentConcepts.tsx create mode 100644 ui/src/pages/agents/AgentsLandingPage.tsx create mode 100644 ui/src/pages/agents/HarnessNewPage.tsx create mode 100644 ui/src/pages/agents/HarnessesTab.tsx create mode 100644 ui/src/router/router.tsx create mode 100644 ui/src/router/routes.ts create mode 100644 ui/src/router/useUrlState.test.tsx create mode 100644 ui/src/router/useUrlState.ts delete mode 100644 ui/src/stories/pages/CreateAgentPage.stories.tsx delete mode 100644 ui/src/stories/pages/CreateMcpPage.stories.tsx delete mode 100644 ui/src/stories/pages/CreateModelPage.stories.tsx delete mode 100644 ui/src/stories/pages/CreatePromptPage.stories.tsx delete mode 100644 ui/src/stories/pages/ViewAgentsPage.stories.tsx delete mode 100644 ui/src/stories/pages/ViewHomePage.stories.tsx delete mode 100644 ui/src/stories/pages/ViewMcpPage.stories.tsx delete mode 100644 ui/src/stories/pages/ViewModelsPage.stories.tsx delete mode 100644 ui/src/stories/pages/ViewPromptLibraryDetailPage.stories.tsx delete mode 100644 ui/src/stories/pages/ViewPromptsPage.stories.tsx delete mode 100644 ui/src/stories/pages/fixtures.ts create mode 100644 ui/src/testSetup.ts create mode 100644 ui/src/theme/GlobalStyles.tsx create mode 100644 ui/src/theme/theme.ts create mode 100644 ui/src/theme/themeMode.tsx delete mode 100644 ui/src/types/acp.ts delete mode 100644 ui/src/types/index.ts create mode 100644 ui/src/vendorExtensions/VendorExtensionProvider.tsx create mode 100644 ui/src/vendorExtensions/VendorProviders.tsx create mode 100644 ui/src/vendorExtensions/VendorSlot.test.tsx create mode 100644 ui/src/vendorExtensions/VendorSlot.tsx create mode 100644 ui/src/vendorExtensions/activeConfig.ts create mode 100644 ui/src/vendorExtensions/api/apiExtension.ts create mode 100644 ui/src/vendorExtensions/api/installVendorApiExtension.ts create mode 100644 ui/src/vendorExtensions/branding.ts create mode 100644 ui/src/vendorExtensions/composition.ts create mode 100644 ui/src/vendorExtensions/context.ts create mode 100644 ui/src/vendorExtensions/example/ExampleComplianceTierField.tsx create mode 100644 ui/src/vendorExtensions/example/ExampleInsightsPage.tsx create mode 100644 ui/src/vendorExtensions/example/ExampleNavItem.tsx create mode 100644 ui/src/vendorExtensions/example/ExampleSlots.tsx create mode 100644 ui/src/vendorExtensions/example/ExampleTenantProvider.tsx create mode 100644 ui/src/vendorExtensions/example/exampleComplianceTier.ts create mode 100644 ui/src/vendorExtensions/example/exampleExtension.tsx create mode 100644 ui/src/vendorExtensions/example/exampleFormFields.ts create mode 100644 ui/src/vendorExtensions/example/exampleTableColumns.ts create mode 100644 ui/src/vendorExtensions/example/exampleTenant.ts create mode 100644 ui/src/vendorExtensions/example/paths.ts create mode 100644 ui/src/vendorExtensions/extensionPoints.ts create mode 100644 ui/src/vendorExtensions/formFields.ts create mode 100644 ui/src/vendorExtensions/hooks.ts create mode 100644 ui/src/vendorExtensions/index.ts create mode 100644 ui/src/vendorExtensions/installActiveExtension.ts create mode 100644 ui/src/vendorExtensions/navOverrides.ts create mode 100644 ui/src/vendorExtensions/shell.ts create mode 100644 ui/src/vendorExtensions/tableColumns.ts create mode 100644 ui/src/vendorExtensions/theme.test.ts create mode 100644 ui/src/vendorExtensions/theme.ts create mode 100644 ui/src/vendorExtensions/types.ts create mode 100644 ui/src/vendorExtensions/validateConfig.ts create mode 100644 ui/src/vendorExtensions/vendorExtensions.test.ts create mode 100644 ui/src/vite-env.d.ts delete mode 100644 ui/tailwind.config.ts create mode 100644 ui/vite.config.ts delete mode 100644 ui/vitest.config.ts delete mode 100644 ui/vitest.shims.d.ts create mode 100644 ui/yarn.lock diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4b84afdfb..075b5eff1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -358,33 +358,45 @@ jobs: uses: actions/setup-node@v7 with: node-version-file: ui/.nvmrc - cache: "npm" - cache-dependency-path: ui/package-lock.json - # Honor the pinned npm from ui/package.json "packageManager" so npm ci - # resolves the lock file with the same npm version it was generated with. + # Before any cache step that shells out to yarn: the pinned version in + # ui/package.json "packageManager" is Yarn 4, and without corepack the shim + # on the runner is a different one that cannot read this lock file. - name: Enable Corepack run: corepack enable + - name: Cache Yarn downloads + uses: actions/cache@v4 + with: + path: ui/.yarn/cache + key: yarn-${{ runner.os }}-${{ hashFiles('ui/yarn.lock') }} + restore-keys: yarn-${{ runner.os }}- + - name: Install dependencies working-directory: ./ui - run: npm ci + run: yarn install --immutable + + - name: Typecheck + working-directory: ./ui + run: yarn typecheck - name: Run lint working-directory: ./ui - run: npm run lint + run: yarn lint - - name: Run unit tests (Jest) + - name: Run unit tests working-directory: ./ui - run: npm run test + run: yarn test - - name: Install Playwright browser (Chromium) + # Both engines the suite declares. Installing only one leaves that project + # failing to launch, which reads as a broken app rather than a missing browser. + - name: Install Playwright browsers working-directory: ./ui - run: npx playwright install --with-deps chromium + run: yarn playwright install --with-deps chromium firefox - - name: Run Storybook tests (Vitest + Playwright) + - name: Run browser tests working-directory: ./ui - run: npm run test:vitest + run: yarn test:pw # This job builds the Docker images for the controller, UI, ADKs, and CLI on arm64. build: diff --git a/.github/workflows/ui-chromatic.yaml b/.github/workflows/ui-chromatic.yaml deleted file mode 100644 index 7aa13e865..000000000 --- a/.github/workflows/ui-chromatic.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: UI Storybook & Chromatic - -# Storybook static build + Chromatic only when the UI package changes. -# Kept separate from CI so the main workflow does not need paths-filter or conditional steps. - -on: - push: - branches: [main, "release/**"] - paths: - - "ui/**" - pull_request: - branches: [main, "release/**"] - paths: - - "ui/**" - workflow_dispatch: - -concurrency: - group: ui-chromatic-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - chromatic: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: ui/.nvmrc - cache: "npm" - cache-dependency-path: ui/package-lock.json - - # Honor the pinned npm from ui/package.json "packageManager" so npm ci - # resolves the lock file with the same npm version it was generated with. - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - working-directory: ./ui - run: npm ci - - - name: Build Storybook - working-directory: ./ui - run: npm run build-storybook - - - name: Publish to Chromatic - working-directory: ./ui - run: npm run chromatic diff --git a/.github/workflows/ui-playwright.yaml b/.github/workflows/ui-playwright.yaml deleted file mode 100644 index 738851c81..000000000 --- a/.github/workflows/ui-playwright.yaml +++ /dev/null @@ -1,119 +0,0 @@ -name: UI Playwright E2E - -# Page-level browser E2E for the UI against a real kagent backend in kind. -# -# A lightweight proxy (ui/playwright/mocks/server.mjs) forwards every /api call to -# the in-cluster controller and mocks only the chat A2A stream. The cluster is -# built by ui/playwright/scripts/setup.sh (same make targets as the Go e2e job); -# Playwright then port-forwards the controller and runs the suite. - -on: - push: - branches: [main, "release/**"] - pull_request: - branches: [main, "release/**"] - workflow_dispatch: - -env: - # Deterministic image tags for the build (matches ci.yaml's e2e job). - VERSION: v0.0.1-test - # Shared GHA build-cache scope prefix + consistent buildx builder (matches ci.yaml). - CACHE_KEY_PREFIX: kagent-v2 - BUILDX_BUILDER_NAME: kagent-builder-v0.23.0 - BUILDX_VERSION: v0.23.0 -concurrency: - group: ui-playwright-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - playwright: - # Disabled until the Kind setup provisions Agent Substrate for SandboxAgent. - if: false - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - # Free disk space + cancel superseded runs (image builds fill the runner). - - name: Initialize Environment - uses: ./.github/actions/initialize-environment - - - name: Allow unprivileged user namespaces - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true - - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - with: - platforms: linux/amd64,linux/arm64 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - with: - name: ${{ env.BUILDX_BUILDER_NAME }} - version: ${{ env.BUILDX_VERSION }} - use: "true" - driver-opts: network=host - - - name: Set up Helm - uses: azure/setup-helm@v5.0.1 - with: - version: v3.18.0 - - - name: Install Kind - uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc - with: - install_only: true - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version-file: "ui/.nvmrc" - cache: "npm" - cache-dependency-path: ui/package-lock.json - - # Honor the pinned npm from ui/package.json "packageManager" (matches ui-chromatic). - - name: Enable Corepack - run: corepack enable - - - name: Install dependencies - working-directory: ./ui - run: npm ci - - - name: Install Playwright browser - working-directory: ./ui - run: npx playwright install --with-deps chromium - - - name: Set up cluster + real kagent - working-directory: ./ui - # Chat is mocked, so a dummy provider key satisfies `make helm-install`. - # DOCKER_BUILD_ARGS mirrors ci.yaml's e2e build: single-arch, pushed to the - # kind registry, with a GHA layer cache warmed by the main e2e build. - env: - OPENAI_API_KEY: fake - BUILDX_BUILDER_NAME: ${{ env.BUILDX_BUILDER_NAME }} - KAGENT_HELM_EXTRA_ARGS: --cleanup-on-fail=false - DOCKER_BUILD_ARGS: >- - --cache-from=type=gha,scope=${{ env.CACHE_KEY_PREFIX }}-ui-e2e - --cache-from=type=gha,scope=${{ env.CACHE_KEY_PREFIX }}-main-e2e - --cache-to=type=gha,scope=${{ env.CACHE_KEY_PREFIX }}-ui-e2e,mode=max - --platform=linux/amd64 - --push - run: ./playwright/scripts/setup.sh - - - name: Run Playwright tests - working-directory: ./ui - env: - CI: "true" - run: npm run test:e2e - - - name: Upload Playwright report - if: always() - uses: actions/upload-artifact@v7 - with: - name: playwright-report - path: | - ui/playwright-report - ui/playwright/test-results - retention-days: 14 diff --git a/CLAUDE.md b/CLAUDE.md index 19b58c109..1267fff49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,6 +108,85 @@ Common commands: - Do not commit or push unless asked. - Keep PRs focused. Explain non-obvious invariants and operational tradeoffs, not line-by-line implementation details. +## The web interface (`ui/`) + +A Vite single-page app. It is a static bundle served by nginx: there is no server +process, so there are no server components, no server-side data fetching and no +file-system routing. + +**Stack:** Vite + React 19, TypeScript, antd 6 for components, Emotion for styling +(the `css` prop, via `jsxImportSource`), SWR for reads, Yarn 4. React Router owns +routing; there is no file-system routing and no server rendering. + +### Commands + +Run these from `ui/`: + +| Task | Command | +|------|---------| +| Dev server | `yarn dev` | +| Unit tests | `yarn test` | +| End-to-end, no cluster needed | `yarn test:pw` (Chromium and Firefox) | +| End-to-end against a real cluster | `yarn test:pw:live` | +| Type check | `yarn typecheck` | +| Lint | `yarn lint` | + +Only lint **errors** gate a change; a handful of warnings are pre-existing. + +`ui/dev-scripts/setup-cluster.sh` builds a Kind cluster with kagent on it in one +command, for work that needs a real backend. + +### Settings reach the app at runtime, not at build time + +Configuration is read from `window.environmentVariables`, which the container +rewrites from its own environment on every start. So one image serves every +deployment, and a setting is an operator's decision rather than something frozen +into a build. Locally the same values come from `ui/.env` (git-ignored; +`ui/.env.example` documents each one). + +Two consequences worth knowing before touching that code: + +- The script that supplies them is **synchronous** in `index.html`. Several modules + read settings at import time, so anything awaited would be read before it arrived. +- `import.meta.env` is for build-time flags only. A value that an operator should be + able to change belongs in `window.environmentVariables`. + +### Fixtures are opt-in + +`ENABLE_MOCK_UI=true` serves the whole API from an in-browser mock (MSW) with no +cluster at all, and `?mock=ok|empty|error|slow` picks which scenario the fixtures +play. **It is off unless asked for**, in a dev server exactly as in a built image: a +page that quietly serves fixtures when the backend is down looks healthy while +showing data that was never real. + +When mock mode is on it overrides every backend setting, and anything reporting who +is signed in correctly reports nobody — there is no backend to have signed in to. + +### Extension points + +One `VendorExtensionConfig` contributes navigation entries and overrides, routes and +route handles, slots, form fields, table columns, API overrides, providers, theme +tokens, shell regions, branding, provider icons and agent links. Components read +every colour, radius and font from those tokens, so overriding them restyles +components an extension never touches. When adding a feature, check whether it +belongs behind an extension point rather than as a branch inside a shared component. + +The full guide is [ui/docs/vendor-extensions.md](ui/docs/vendor-extensions.md). + +### Conventions specific to this codebase + +- **Say when data is not real.** A page showing fixtures says so on the page. Never + suppress an error because a mock flag is set — a broken backend must not render as + healthy mock data. +- **Normalise at the client boundary.** Go marshals a nil slice as JSON `null`, so + any collection the controller has nothing for arrives as null. Fix it once where the + response is parsed, not at each use. +- **Fixtures must match the controller, not each other.** A fixture, a type and a + test can agree perfectly and all three be wrong; that has happened here more than + once and each time only a real cluster objected. Check the CRD. +- **Prefer a smaller honest test suite** over a green one that proves nothing. + Coverage debt belongs in `playwright/DEFERRED.md`, not in skipped specs. + ## 9. References - [STYLE.md](STYLE.md) diff --git a/go/api/database/client.go b/go/api/database/client.go index 8732fb3c7..d388016b4 100644 --- a/go/api/database/client.go +++ b/go/api/database/client.go @@ -21,6 +21,22 @@ var ErrAgentInstanceConflict = errors.New("AgentInstance lifecycle operation con var ErrAgentInstanceTaskConflict = errors.New("AgentInstance already has an active task") +// TaskParkedAwaitingUser reports whether a task stopped to wait on a human +// rather than because it is being executed. Such a task is non-terminal, so it +// holds the instance's single active-task slot, but no execution is in flight: +// the runtime has asked a question (`ask_user`, a tool approval) and is waiting +// for the answer. +// +// The distinction has to live in one place because two callers act on it in +// opposite directions — a suspend must leave a parked turn alone, since the +// question is still valid and the reader may answer it after resuming, while a +// send has to report the parked turn as the reason it was refused. Getting +// either backwards destroys a pending question or hides why a conversation +// stopped answering. +func TaskParkedAwaitingUser(state a2a.TaskState) bool { + return state == a2a.TaskStateInputRequired || state == a2a.TaskStateAuthRequired +} + var ErrAgentInstanceNotQuiescent = errors.New("AgentInstance has no quiescent turn boundary") type QueryOptions struct { @@ -124,12 +140,19 @@ type Client interface { CreateAgentInstance(context.Context, *apiv1alpha1.AgentInstance, string) (*apiv1alpha1.AgentInstance, bool, error) ForkAgentInstance(context.Context, string, string, string, string, string) (*apiv1alpha1.AgentInstance, bool, error) GetAgentInstance(context.Context, string, string, string) (*apiv1alpha1.AgentInstance, error) - ListAgentInstances(context.Context, string, string, bool, map[string]string, string, int) ([]*apiv1alpha1.AgentInstance, error) + ListAgentInstances(context.Context, AgentInstanceQuery) ([]*apiv1alpha1.AgentInstance, error) + // RenameAgentInstance sets the instance's display name, scoped to its owner. + // Takes namespace, id, owner and the new name. + RenameAgentInstance(context.Context, string, string, string, string) (*apiv1alpha1.AgentInstance, error) MarkAgentInstanceReady(context.Context, string, string) (*apiv1alpha1.AgentInstance, error) TransitionAgentInstance(context.Context, *apiv1alpha1.AgentInstance, apiv1alpha1.AgentInstanceState, apiv1alpha1.AgentInstanceOperation) (*apiv1alpha1.AgentInstance, error) DeleteAgentInstance(context.Context, string) error CreateAgentInstanceShare(context.Context, AgentInstanceShare) (*AgentInstanceShare, error) ListAgentInstanceShares(context.Context, string, string, string, string, int) ([]AgentInstanceShare, error) + // GetAgentInstanceShareByTokenHash resolves a share token to its share and the + // owner of the instance it grants access to. Takes the digest, because only the + // digest is stored. + GetAgentInstanceShareByTokenHash(context.Context, []byte) (*AgentInstanceShare, error) DeleteAgentInstanceShare(context.Context, string, string, string) error // CreateAgentInstanceTask reserves the instance's single active-task slot. CreateAgentInstanceTask(context.Context, string, []byte, *a2a.Task) (*a2a.Task, bool, error) @@ -137,6 +160,18 @@ type Client interface { // InterruptActiveAgentInstanceTask fails the expected task and records an // interruption. It returns false if that task is no longer active. InterruptActiveAgentInstanceTask(context.Context, string, string) (bool, error) + // AbandonActiveAgentInstanceTask cancels the expected task, releasing the + // instance's active-task slot for a turn that was parked awaiting the reader. + // It returns false if that task is no longer active. + AbandonActiveAgentInstanceTask(context.Context, string, string) (bool, error) + // ClaimParkedAgentInstanceTask moves a task waiting on the reader into a + // working state so a reply can be delivered, returning the task as it was + // parked and whether this call claimed it. A second caller is refused, which + // is what stops a duplicate reply being delivered twice. + ClaimParkedAgentInstanceTask(context.Context, string, string) (*a2a.Task, bool, error) + // RestoreParkedAgentInstanceTask puts a claimed task back as it was, for a + // reply that never reached the runtime. + RestoreParkedAgentInstanceTask(context.Context, string, *a2a.Task) error StoreAgentInstanceTaskEvent(context.Context, string, *a2a.Task, a2a.Event, *AgentInstanceTaskSnapshot) error GetAgentInstanceTask(context.Context, string, string) (*a2a.Task, error) ListAgentInstanceTasks(context.Context, string, string, a2a.TaskState, *time.Time, int) ([]*a2a.Task, int, error) diff --git a/go/api/database/models.go b/go/api/database/models.go index 66dd676a8..4170fcd34 100644 --- a/go/api/database/models.go +++ b/go/api/database/models.go @@ -270,6 +270,24 @@ type RuntimeRevision struct { GoldenSnapshot string } +// AgentInstanceQuery narrows a page of AgentInstances. Zero values mean "do not +// filter on this", so an empty query lists the caller's own instances in the +// namespace. +type AgentInstanceQuery struct { + Namespace string + UserID string + AllUsers bool + MatchLabels map[string]string + // AgentTemplate and Harness name the agent whose conversations are wanted. + // They are matched against the (AgentTemplate, Harness) pair the instance's + // prepared revision was built from, not against its labels, so they select + // instances stored before either field existed. + AgentTemplate string + Harness string + AfterID string + Limit int +} + type AgentInstanceShare struct { ID string Namespace string @@ -278,6 +296,13 @@ type AgentInstanceShare struct { Permission string TokenHash []byte CreatedAt time.Time + // OwnerUserID is the user the shared AgentInstance belongs to. + // + // Populated only by the token lookup, which joins it in — that is what the + // share grants. A visitor is authenticated as themselves and the token widens + // what their account may reach to what the *owner* can see, so the instance + // read has to run as the owner or it finds nothing. + OwnerUserID string } // AgentInstanceTaskSnapshot identifies the immutable Substrate snapshot at a diff --git a/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go b/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go index 4958353fb..7ea004c69 100644 --- a/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go @@ -257,8 +257,11 @@ type AgentInstance struct { CreatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` Labels map[string]string `protobuf:"bytes,13,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Reader-supplied display name for the conversation. Empty means unnamed, + // which is the state every instance created before this field existed is in. + Name string `protobuf:"bytes,14,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AgentInstance) Reset() { @@ -382,12 +385,23 @@ func (x *AgentInstance) GetLabels() map[string]string { return nil } +func (x *AgentInstance) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type CreateAgentInstanceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` Harness string `protobuf:"bytes,2,opt,name=harness,proto3" json:"harness,omitempty"` AgentTemplate string `protobuf:"bytes,3,opt,name=agent_template,json=agentTemplate,proto3" json:"agent_template,omitempty"` RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Optional display name. Omit it to create an unnamed conversation. Unvalidated + // because empty is the ordinary case: a conversation is usually named later, or + // never. + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -450,6 +464,13 @@ func (x *CreateAgentInstanceRequest) GetRequestId() string { return "" } +func (x *CreateAgentInstanceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type CreateAgentInstanceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` AgentInstance *AgentInstance `protobuf:"bytes,1,opt,name=agent_instance,json=agentInstance,proto3" json:"agent_instance,omitempty"` @@ -595,8 +616,14 @@ type ListAgentInstancesRequest struct { Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` MatchLabels map[string]string `protobuf:"bytes,2,rep,name=match_labels,json=matchLabels,proto3" json:"match_labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Includes instances created by other users when authorized. - AllCreators bool `protobuf:"varint,3,opt,name=all_creators,json=allCreators,proto3" json:"all_creators,omitempty"` - Page *PageRequest `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` + AllCreators bool `protobuf:"varint,3,opt,name=all_creators,json=allCreators,proto3" json:"all_creators,omitempty"` + Page *PageRequest `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` + // Narrows the list to the conversations of one agent, an agent being an + // (AgentTemplate, Harness) pair. Either may be given alone. Both are matched + // against the pair the instance's prepared revision was built from, so they + // also select instances created before these fields existed. + AgentTemplate string `protobuf:"bytes,5,opt,name=agent_template,json=agentTemplate,proto3" json:"agent_template,omitempty"` + Harness string `protobuf:"bytes,6,opt,name=harness,proto3" json:"harness,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -659,6 +686,20 @@ func (x *ListAgentInstancesRequest) GetPage() *PageRequest { return nil } +func (x *ListAgentInstancesRequest) GetAgentTemplate() string { + if x != nil { + return x.AgentTemplate + } + return "" +} + +func (x *ListAgentInstancesRequest) GetHarness() string { + if x != nil { + return x.Harness + } + return "" +} + type ListAgentInstancesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` AgentInstances []*AgentInstance `protobuf:"bytes,1,rep,name=agent_instances,json=agentInstances,proto3" json:"agent_instances,omitempty"` @@ -711,6 +752,112 @@ func (x *ListAgentInstancesResponse) GetPage() *PageResponse { return nil } +type RenameAgentInstanceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + AgentInstanceId string `protobuf:"bytes,2,opt,name=agent_instance_id,json=agentInstanceId,proto3" json:"agent_instance_id,omitempty"` + // The new display name. Empty clears the name, returning the conversation to + // being identified by its id. + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenameAgentInstanceRequest) Reset() { + *x = RenameAgentInstanceRequest{} + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenameAgentInstanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenameAgentInstanceRequest) ProtoMessage() {} + +func (x *RenameAgentInstanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenameAgentInstanceRequest.ProtoReflect.Descriptor instead. +func (*RenameAgentInstanceRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{8} +} + +func (x *RenameAgentInstanceRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RenameAgentInstanceRequest) GetAgentInstanceId() string { + if x != nil { + return x.AgentInstanceId + } + return "" +} + +func (x *RenameAgentInstanceRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type RenameAgentInstanceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AgentInstance *AgentInstance `protobuf:"bytes,1,opt,name=agent_instance,json=agentInstance,proto3" json:"agent_instance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenameAgentInstanceResponse) Reset() { + *x = RenameAgentInstanceResponse{} + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenameAgentInstanceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenameAgentInstanceResponse) ProtoMessage() {} + +func (x *RenameAgentInstanceResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenameAgentInstanceResponse.ProtoReflect.Descriptor instead. +func (*RenameAgentInstanceResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{9} +} + +func (x *RenameAgentInstanceResponse) GetAgentInstance() *AgentInstance { + if x != nil { + return x.AgentInstance + } + return nil +} + type SuspendAgentInstanceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -721,7 +868,7 @@ type SuspendAgentInstanceRequest struct { func (x *SuspendAgentInstanceRequest) Reset() { *x = SuspendAgentInstanceRequest{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[8] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -733,7 +880,7 @@ func (x *SuspendAgentInstanceRequest) String() string { func (*SuspendAgentInstanceRequest) ProtoMessage() {} func (x *SuspendAgentInstanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[8] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -746,7 +893,7 @@ func (x *SuspendAgentInstanceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendAgentInstanceRequest.ProtoReflect.Descriptor instead. func (*SuspendAgentInstanceRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{8} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{10} } func (x *SuspendAgentInstanceRequest) GetNamespace() string { @@ -772,7 +919,7 @@ type SuspendAgentInstanceResponse struct { func (x *SuspendAgentInstanceResponse) Reset() { *x = SuspendAgentInstanceResponse{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[9] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -784,7 +931,7 @@ func (x *SuspendAgentInstanceResponse) String() string { func (*SuspendAgentInstanceResponse) ProtoMessage() {} func (x *SuspendAgentInstanceResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[9] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -797,7 +944,7 @@ func (x *SuspendAgentInstanceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SuspendAgentInstanceResponse.ProtoReflect.Descriptor instead. func (*SuspendAgentInstanceResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{9} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{11} } func (x *SuspendAgentInstanceResponse) GetAgentInstance() *AgentInstance { @@ -817,7 +964,7 @@ type ResumeAgentInstanceRequest struct { func (x *ResumeAgentInstanceRequest) Reset() { *x = ResumeAgentInstanceRequest{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[10] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -829,7 +976,7 @@ func (x *ResumeAgentInstanceRequest) String() string { func (*ResumeAgentInstanceRequest) ProtoMessage() {} func (x *ResumeAgentInstanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[10] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -842,7 +989,7 @@ func (x *ResumeAgentInstanceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeAgentInstanceRequest.ProtoReflect.Descriptor instead. func (*ResumeAgentInstanceRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{10} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{12} } func (x *ResumeAgentInstanceRequest) GetNamespace() string { @@ -868,7 +1015,7 @@ type ResumeAgentInstanceResponse struct { func (x *ResumeAgentInstanceResponse) Reset() { *x = ResumeAgentInstanceResponse{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[11] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -880,7 +1027,7 @@ func (x *ResumeAgentInstanceResponse) String() string { func (*ResumeAgentInstanceResponse) ProtoMessage() {} func (x *ResumeAgentInstanceResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[11] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -893,7 +1040,7 @@ func (x *ResumeAgentInstanceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResumeAgentInstanceResponse.ProtoReflect.Descriptor instead. func (*ResumeAgentInstanceResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{11} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{13} } func (x *ResumeAgentInstanceResponse) GetAgentInstance() *AgentInstance { @@ -913,7 +1060,7 @@ type DeleteAgentInstanceRequest struct { func (x *DeleteAgentInstanceRequest) Reset() { *x = DeleteAgentInstanceRequest{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[12] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -925,7 +1072,7 @@ func (x *DeleteAgentInstanceRequest) String() string { func (*DeleteAgentInstanceRequest) ProtoMessage() {} func (x *DeleteAgentInstanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[12] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -938,7 +1085,7 @@ func (x *DeleteAgentInstanceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAgentInstanceRequest.ProtoReflect.Descriptor instead. func (*DeleteAgentInstanceRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{12} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{14} } func (x *DeleteAgentInstanceRequest) GetNamespace() string { @@ -964,7 +1111,7 @@ type DeleteAgentInstanceResponse struct { func (x *DeleteAgentInstanceResponse) Reset() { *x = DeleteAgentInstanceResponse{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[13] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -976,7 +1123,7 @@ func (x *DeleteAgentInstanceResponse) String() string { func (*DeleteAgentInstanceResponse) ProtoMessage() {} func (x *DeleteAgentInstanceResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[13] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -989,7 +1136,7 @@ func (x *DeleteAgentInstanceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAgentInstanceResponse.ProtoReflect.Descriptor instead. func (*DeleteAgentInstanceResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{13} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{15} } func (x *DeleteAgentInstanceResponse) GetAgentInstance() *AgentInstance { @@ -1013,7 +1160,7 @@ type AgentInstanceShare struct { func (x *AgentInstanceShare) Reset() { *x = AgentInstanceShare{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[14] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1025,7 +1172,7 @@ func (x *AgentInstanceShare) String() string { func (*AgentInstanceShare) ProtoMessage() {} func (x *AgentInstanceShare) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[14] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1038,7 +1185,7 @@ func (x *AgentInstanceShare) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentInstanceShare.ProtoReflect.Descriptor instead. func (*AgentInstanceShare) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{14} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{16} } func (x *AgentInstanceShare) GetId() string { @@ -1094,7 +1241,7 @@ type CreateAgentInstanceShareRequest struct { func (x *CreateAgentInstanceShareRequest) Reset() { *x = CreateAgentInstanceShareRequest{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[15] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1106,7 +1253,7 @@ func (x *CreateAgentInstanceShareRequest) String() string { func (*CreateAgentInstanceShareRequest) ProtoMessage() {} func (x *CreateAgentInstanceShareRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[15] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1119,7 +1266,7 @@ func (x *CreateAgentInstanceShareRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAgentInstanceShareRequest.ProtoReflect.Descriptor instead. func (*CreateAgentInstanceShareRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{15} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{17} } func (x *CreateAgentInstanceShareRequest) GetNamespace() string { @@ -1154,7 +1301,7 @@ type CreateAgentInstanceShareResponse struct { func (x *CreateAgentInstanceShareResponse) Reset() { *x = CreateAgentInstanceShareResponse{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[16] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1166,7 +1313,7 @@ func (x *CreateAgentInstanceShareResponse) String() string { func (*CreateAgentInstanceShareResponse) ProtoMessage() {} func (x *CreateAgentInstanceShareResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[16] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1179,7 +1326,7 @@ func (x *CreateAgentInstanceShareResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAgentInstanceShareResponse.ProtoReflect.Descriptor instead. func (*CreateAgentInstanceShareResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{16} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{18} } func (x *CreateAgentInstanceShareResponse) GetShare() *AgentInstanceShare { @@ -1207,7 +1354,7 @@ type ListAgentInstanceSharesRequest struct { func (x *ListAgentInstanceSharesRequest) Reset() { *x = ListAgentInstanceSharesRequest{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[17] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1219,7 +1366,7 @@ func (x *ListAgentInstanceSharesRequest) String() string { func (*ListAgentInstanceSharesRequest) ProtoMessage() {} func (x *ListAgentInstanceSharesRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[17] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1232,7 +1379,7 @@ func (x *ListAgentInstanceSharesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAgentInstanceSharesRequest.ProtoReflect.Descriptor instead. func (*ListAgentInstanceSharesRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{17} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{19} } func (x *ListAgentInstanceSharesRequest) GetNamespace() string { @@ -1266,7 +1413,7 @@ type ListAgentInstanceSharesResponse struct { func (x *ListAgentInstanceSharesResponse) Reset() { *x = ListAgentInstanceSharesResponse{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[18] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1278,7 +1425,7 @@ func (x *ListAgentInstanceSharesResponse) String() string { func (*ListAgentInstanceSharesResponse) ProtoMessage() {} func (x *ListAgentInstanceSharesResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[18] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1291,7 +1438,7 @@ func (x *ListAgentInstanceSharesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAgentInstanceSharesResponse.ProtoReflect.Descriptor instead. func (*ListAgentInstanceSharesResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{18} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{20} } func (x *ListAgentInstanceSharesResponse) GetShares() []*AgentInstanceShare { @@ -1318,7 +1465,7 @@ type RevokeAgentInstanceShareRequest struct { func (x *RevokeAgentInstanceShareRequest) Reset() { *x = RevokeAgentInstanceShareRequest{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[19] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1330,7 +1477,7 @@ func (x *RevokeAgentInstanceShareRequest) String() string { func (*RevokeAgentInstanceShareRequest) ProtoMessage() {} func (x *RevokeAgentInstanceShareRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[19] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1343,7 +1490,7 @@ func (x *RevokeAgentInstanceShareRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeAgentInstanceShareRequest.ProtoReflect.Descriptor instead. func (*RevokeAgentInstanceShareRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{19} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{21} } func (x *RevokeAgentInstanceShareRequest) GetNamespace() string { @@ -1368,7 +1515,7 @@ type RevokeAgentInstanceShareResponse struct { func (x *RevokeAgentInstanceShareResponse) Reset() { *x = RevokeAgentInstanceShareResponse{} - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[20] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1380,7 +1527,7 @@ func (x *RevokeAgentInstanceShareResponse) String() string { func (*RevokeAgentInstanceShareResponse) ProtoMessage() {} func (x *RevokeAgentInstanceShareResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[20] + mi := &file_kagent_api_v1alpha1_agent_instances_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1393,7 +1540,7 @@ func (x *RevokeAgentInstanceShareResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeAgentInstanceShareResponse.ProtoReflect.Descriptor instead. func (*RevokeAgentInstanceShareResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{20} + return file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP(), []int{22} } var File_kagent_api_v1alpha1_agent_instances_proto protoreflect.FileDescriptor @@ -1403,7 +1550,7 @@ const file_kagent_api_v1alpha1_agent_instances_proto_rawDesc = "" + ")kagent/api/v1alpha1/agent_instances.proto\x12\x13kagent.api.v1alpha1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a kagent/api/v1alpha1/common.proto\";\n" + "\aFailure\x12\x16\n" + "\x06reason\x18\x01 \x01(\tR\x06reason\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\"\xf5\x05\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\x89\x06\n" + "\rAgentInstance\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1c\n" + "\tnamespace\x18\x02 \x01(\tR\tnamespace\x12\x18\n" + @@ -1420,35 +1567,45 @@ const file_kagent_api_v1alpha1_agent_instances_proto_rawDesc = "" + "created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "\n" + "updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12F\n" + - "\x06labels\x18\r \x03(\v2..kagent.api.v1alpha1.AgentInstance.LabelsEntryR\x06labels\x1a9\n" + + "\x06labels\x18\r \x03(\v2..kagent.api.v1alpha1.AgentInstance.LabelsEntryR\x06labels\x12\x12\n" + + "\x04name\x18\x0e \x01(\tR\x04name\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc1\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd5\x01\n" + "\x1aCreateAgentInstanceRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x12!\n" + "\aharness\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\aharness\x12.\n" + "\x0eagent_template\x18\x03 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\ragentTemplate\x12)\n" + "\n" + "request_id\x18\x04 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x01R\trequestId\"h\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\trequestId\x12\x12\n" + + "\x04name\x18\x05 \x01(\tR\x04name\"h\n" + "\x1bCreateAgentInstanceResponse\x12I\n" + "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"u\n" + "\x17GetAgentInstanceRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x123\n" + "\x11agent_instance_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x0fagentInstanceId\"e\n" + "\x18GetAgentInstanceResponse\x12I\n" + - "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"\xbf\x02\n" + + "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"\x80\x03\n" + "\x19ListAgentInstancesRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x12b\n" + "\fmatch_labels\x18\x02 \x03(\v2?.kagent.api.v1alpha1.ListAgentInstancesRequest.MatchLabelsEntryR\vmatchLabels\x12!\n" + "\fall_creators\x18\x03 \x01(\bR\vallCreators\x124\n" + - "\x04page\x18\x04 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x1a>\n" + + "\x04page\x18\x04 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12%\n" + + "\x0eagent_template\x18\x05 \x01(\tR\ragentTemplate\x12\x18\n" + + "\aharness\x18\x06 \x01(\tR\aharness\x1a>\n" + "\x10MatchLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa0\x01\n" + "\x1aListAgentInstancesResponse\x12K\n" + "\x0fagent_instances\x18\x01 \x03(\v2\".kagent.api.v1alpha1.AgentInstanceR\x0eagentInstances\x125\n" + - "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\"y\n" + + "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\"z\n" + + "\x1aRenameAgentInstanceRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12*\n" + + "\x11agent_instance_id\x18\x02 \x01(\tR\x0fagentInstanceId\x12\x12\n" + + "\x04name\x18\x03 \x01(\tR\x04name\"h\n" + + "\x1bRenameAgentInstanceResponse\x12I\n" + + "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"y\n" + "\x1bSuspendAgentInstanceRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x123\n" + "\x11agent_instance_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x0fagentInstanceId\"i\n" + @@ -1512,11 +1669,12 @@ const file_kagent_api_v1alpha1_agent_instances_proto_rawDesc = "" + "\x1cAgentInstanceSharePermission\x12/\n" + "+AGENT_INSTANCE_SHARE_PERMISSION_UNSPECIFIED\x10\x00\x12-\n" + ")AGENT_INSTANCE_SHARE_PERMISSION_READ_ONLY\x10\x01\x12.\n" + - "*AGENT_INSTANCE_SHARE_PERMISSION_READ_WRITE\x10\x022\x84\t\n" + + "*AGENT_INSTANCE_SHARE_PERMISSION_READ_WRITE\x10\x022\xfe\t\n" + "\x14AgentInstanceService\x12x\n" + "\x13CreateAgentInstance\x12/.kagent.api.v1alpha1.CreateAgentInstanceRequest\x1a0.kagent.api.v1alpha1.CreateAgentInstanceResponse\x12o\n" + "\x10GetAgentInstance\x12,.kagent.api.v1alpha1.GetAgentInstanceRequest\x1a-.kagent.api.v1alpha1.GetAgentInstanceResponse\x12u\n" + - "\x12ListAgentInstances\x12..kagent.api.v1alpha1.ListAgentInstancesRequest\x1a/.kagent.api.v1alpha1.ListAgentInstancesResponse\x12{\n" + + "\x12ListAgentInstances\x12..kagent.api.v1alpha1.ListAgentInstancesRequest\x1a/.kagent.api.v1alpha1.ListAgentInstancesResponse\x12x\n" + + "\x13RenameAgentInstance\x12/.kagent.api.v1alpha1.RenameAgentInstanceRequest\x1a0.kagent.api.v1alpha1.RenameAgentInstanceResponse\x12{\n" + "\x14SuspendAgentInstance\x120.kagent.api.v1alpha1.SuspendAgentInstanceRequest\x1a1.kagent.api.v1alpha1.SuspendAgentInstanceResponse\x12x\n" + "\x13ResumeAgentInstance\x12/.kagent.api.v1alpha1.ResumeAgentInstanceRequest\x1a0.kagent.api.v1alpha1.ResumeAgentInstanceResponse\x12x\n" + "\x13DeleteAgentInstance\x12/.kagent.api.v1alpha1.DeleteAgentInstanceRequest\x1a0.kagent.api.v1alpha1.DeleteAgentInstanceResponse\x12\x87\x01\n" + @@ -1537,7 +1695,7 @@ func file_kagent_api_v1alpha1_agent_instances_proto_rawDescGZIP() []byte { } var file_kagent_api_v1alpha1_agent_instances_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_kagent_api_v1alpha1_agent_instances_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_kagent_api_v1alpha1_agent_instances_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_kagent_api_v1alpha1_agent_instances_proto_goTypes = []any{ (AgentInstanceState)(0), // 0: kagent.api.v1alpha1.AgentInstanceState (AgentInstanceOperation)(0), // 1: kagent.api.v1alpha1.AgentInstanceOperation @@ -1550,74 +1708,79 @@ var file_kagent_api_v1alpha1_agent_instances_proto_goTypes = []any{ (*GetAgentInstanceResponse)(nil), // 8: kagent.api.v1alpha1.GetAgentInstanceResponse (*ListAgentInstancesRequest)(nil), // 9: kagent.api.v1alpha1.ListAgentInstancesRequest (*ListAgentInstancesResponse)(nil), // 10: kagent.api.v1alpha1.ListAgentInstancesResponse - (*SuspendAgentInstanceRequest)(nil), // 11: kagent.api.v1alpha1.SuspendAgentInstanceRequest - (*SuspendAgentInstanceResponse)(nil), // 12: kagent.api.v1alpha1.SuspendAgentInstanceResponse - (*ResumeAgentInstanceRequest)(nil), // 13: kagent.api.v1alpha1.ResumeAgentInstanceRequest - (*ResumeAgentInstanceResponse)(nil), // 14: kagent.api.v1alpha1.ResumeAgentInstanceResponse - (*DeleteAgentInstanceRequest)(nil), // 15: kagent.api.v1alpha1.DeleteAgentInstanceRequest - (*DeleteAgentInstanceResponse)(nil), // 16: kagent.api.v1alpha1.DeleteAgentInstanceResponse - (*AgentInstanceShare)(nil), // 17: kagent.api.v1alpha1.AgentInstanceShare - (*CreateAgentInstanceShareRequest)(nil), // 18: kagent.api.v1alpha1.CreateAgentInstanceShareRequest - (*CreateAgentInstanceShareResponse)(nil), // 19: kagent.api.v1alpha1.CreateAgentInstanceShareResponse - (*ListAgentInstanceSharesRequest)(nil), // 20: kagent.api.v1alpha1.ListAgentInstanceSharesRequest - (*ListAgentInstanceSharesResponse)(nil), // 21: kagent.api.v1alpha1.ListAgentInstanceSharesResponse - (*RevokeAgentInstanceShareRequest)(nil), // 22: kagent.api.v1alpha1.RevokeAgentInstanceShareRequest - (*RevokeAgentInstanceShareResponse)(nil), // 23: kagent.api.v1alpha1.RevokeAgentInstanceShareResponse - nil, // 24: kagent.api.v1alpha1.AgentInstance.LabelsEntry - nil, // 25: kagent.api.v1alpha1.ListAgentInstancesRequest.MatchLabelsEntry - (*ResourceReference)(nil), // 26: kagent.api.v1alpha1.ResourceReference - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp - (*PageRequest)(nil), // 28: kagent.api.v1alpha1.PageRequest - (*PageResponse)(nil), // 29: kagent.api.v1alpha1.PageResponse + (*RenameAgentInstanceRequest)(nil), // 11: kagent.api.v1alpha1.RenameAgentInstanceRequest + (*RenameAgentInstanceResponse)(nil), // 12: kagent.api.v1alpha1.RenameAgentInstanceResponse + (*SuspendAgentInstanceRequest)(nil), // 13: kagent.api.v1alpha1.SuspendAgentInstanceRequest + (*SuspendAgentInstanceResponse)(nil), // 14: kagent.api.v1alpha1.SuspendAgentInstanceResponse + (*ResumeAgentInstanceRequest)(nil), // 15: kagent.api.v1alpha1.ResumeAgentInstanceRequest + (*ResumeAgentInstanceResponse)(nil), // 16: kagent.api.v1alpha1.ResumeAgentInstanceResponse + (*DeleteAgentInstanceRequest)(nil), // 17: kagent.api.v1alpha1.DeleteAgentInstanceRequest + (*DeleteAgentInstanceResponse)(nil), // 18: kagent.api.v1alpha1.DeleteAgentInstanceResponse + (*AgentInstanceShare)(nil), // 19: kagent.api.v1alpha1.AgentInstanceShare + (*CreateAgentInstanceShareRequest)(nil), // 20: kagent.api.v1alpha1.CreateAgentInstanceShareRequest + (*CreateAgentInstanceShareResponse)(nil), // 21: kagent.api.v1alpha1.CreateAgentInstanceShareResponse + (*ListAgentInstanceSharesRequest)(nil), // 22: kagent.api.v1alpha1.ListAgentInstanceSharesRequest + (*ListAgentInstanceSharesResponse)(nil), // 23: kagent.api.v1alpha1.ListAgentInstanceSharesResponse + (*RevokeAgentInstanceShareRequest)(nil), // 24: kagent.api.v1alpha1.RevokeAgentInstanceShareRequest + (*RevokeAgentInstanceShareResponse)(nil), // 25: kagent.api.v1alpha1.RevokeAgentInstanceShareResponse + nil, // 26: kagent.api.v1alpha1.AgentInstance.LabelsEntry + nil, // 27: kagent.api.v1alpha1.ListAgentInstancesRequest.MatchLabelsEntry + (*ResourceReference)(nil), // 28: kagent.api.v1alpha1.ResourceReference + (*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp + (*PageRequest)(nil), // 30: kagent.api.v1alpha1.PageRequest + (*PageResponse)(nil), // 31: kagent.api.v1alpha1.PageResponse } var file_kagent_api_v1alpha1_agent_instances_proto_depIdxs = []int32{ - 26, // 0: kagent.api.v1alpha1.AgentInstance.harness:type_name -> kagent.api.v1alpha1.ResourceReference - 26, // 1: kagent.api.v1alpha1.AgentInstance.agent_template:type_name -> kagent.api.v1alpha1.ResourceReference + 28, // 0: kagent.api.v1alpha1.AgentInstance.harness:type_name -> kagent.api.v1alpha1.ResourceReference + 28, // 1: kagent.api.v1alpha1.AgentInstance.agent_template:type_name -> kagent.api.v1alpha1.ResourceReference 0, // 2: kagent.api.v1alpha1.AgentInstance.state:type_name -> kagent.api.v1alpha1.AgentInstanceState 1, // 3: kagent.api.v1alpha1.AgentInstance.operation:type_name -> kagent.api.v1alpha1.AgentInstanceOperation 3, // 4: kagent.api.v1alpha1.AgentInstance.failure:type_name -> kagent.api.v1alpha1.Failure - 27, // 5: kagent.api.v1alpha1.AgentInstance.created_at:type_name -> google.protobuf.Timestamp - 27, // 6: kagent.api.v1alpha1.AgentInstance.updated_at:type_name -> google.protobuf.Timestamp - 24, // 7: kagent.api.v1alpha1.AgentInstance.labels:type_name -> kagent.api.v1alpha1.AgentInstance.LabelsEntry + 29, // 5: kagent.api.v1alpha1.AgentInstance.created_at:type_name -> google.protobuf.Timestamp + 29, // 6: kagent.api.v1alpha1.AgentInstance.updated_at:type_name -> google.protobuf.Timestamp + 26, // 7: kagent.api.v1alpha1.AgentInstance.labels:type_name -> kagent.api.v1alpha1.AgentInstance.LabelsEntry 4, // 8: kagent.api.v1alpha1.CreateAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance 4, // 9: kagent.api.v1alpha1.GetAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance - 25, // 10: kagent.api.v1alpha1.ListAgentInstancesRequest.match_labels:type_name -> kagent.api.v1alpha1.ListAgentInstancesRequest.MatchLabelsEntry - 28, // 11: kagent.api.v1alpha1.ListAgentInstancesRequest.page:type_name -> kagent.api.v1alpha1.PageRequest + 27, // 10: kagent.api.v1alpha1.ListAgentInstancesRequest.match_labels:type_name -> kagent.api.v1alpha1.ListAgentInstancesRequest.MatchLabelsEntry + 30, // 11: kagent.api.v1alpha1.ListAgentInstancesRequest.page:type_name -> kagent.api.v1alpha1.PageRequest 4, // 12: kagent.api.v1alpha1.ListAgentInstancesResponse.agent_instances:type_name -> kagent.api.v1alpha1.AgentInstance - 29, // 13: kagent.api.v1alpha1.ListAgentInstancesResponse.page:type_name -> kagent.api.v1alpha1.PageResponse - 4, // 14: kagent.api.v1alpha1.SuspendAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance - 4, // 15: kagent.api.v1alpha1.ResumeAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance - 4, // 16: kagent.api.v1alpha1.DeleteAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance - 2, // 17: kagent.api.v1alpha1.AgentInstanceShare.permission:type_name -> kagent.api.v1alpha1.AgentInstanceSharePermission - 27, // 18: kagent.api.v1alpha1.AgentInstanceShare.created_at:type_name -> google.protobuf.Timestamp - 2, // 19: kagent.api.v1alpha1.CreateAgentInstanceShareRequest.permission:type_name -> kagent.api.v1alpha1.AgentInstanceSharePermission - 17, // 20: kagent.api.v1alpha1.CreateAgentInstanceShareResponse.share:type_name -> kagent.api.v1alpha1.AgentInstanceShare - 28, // 21: kagent.api.v1alpha1.ListAgentInstanceSharesRequest.page:type_name -> kagent.api.v1alpha1.PageRequest - 17, // 22: kagent.api.v1alpha1.ListAgentInstanceSharesResponse.shares:type_name -> kagent.api.v1alpha1.AgentInstanceShare - 29, // 23: kagent.api.v1alpha1.ListAgentInstanceSharesResponse.page:type_name -> kagent.api.v1alpha1.PageResponse - 5, // 24: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstance:input_type -> kagent.api.v1alpha1.CreateAgentInstanceRequest - 7, // 25: kagent.api.v1alpha1.AgentInstanceService.GetAgentInstance:input_type -> kagent.api.v1alpha1.GetAgentInstanceRequest - 9, // 26: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstances:input_type -> kagent.api.v1alpha1.ListAgentInstancesRequest - 11, // 27: kagent.api.v1alpha1.AgentInstanceService.SuspendAgentInstance:input_type -> kagent.api.v1alpha1.SuspendAgentInstanceRequest - 13, // 28: kagent.api.v1alpha1.AgentInstanceService.ResumeAgentInstance:input_type -> kagent.api.v1alpha1.ResumeAgentInstanceRequest - 15, // 29: kagent.api.v1alpha1.AgentInstanceService.DeleteAgentInstance:input_type -> kagent.api.v1alpha1.DeleteAgentInstanceRequest - 18, // 30: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstanceShare:input_type -> kagent.api.v1alpha1.CreateAgentInstanceShareRequest - 20, // 31: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstanceShares:input_type -> kagent.api.v1alpha1.ListAgentInstanceSharesRequest - 22, // 32: kagent.api.v1alpha1.AgentInstanceService.RevokeAgentInstanceShare:input_type -> kagent.api.v1alpha1.RevokeAgentInstanceShareRequest - 6, // 33: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstance:output_type -> kagent.api.v1alpha1.CreateAgentInstanceResponse - 8, // 34: kagent.api.v1alpha1.AgentInstanceService.GetAgentInstance:output_type -> kagent.api.v1alpha1.GetAgentInstanceResponse - 10, // 35: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstances:output_type -> kagent.api.v1alpha1.ListAgentInstancesResponse - 12, // 36: kagent.api.v1alpha1.AgentInstanceService.SuspendAgentInstance:output_type -> kagent.api.v1alpha1.SuspendAgentInstanceResponse - 14, // 37: kagent.api.v1alpha1.AgentInstanceService.ResumeAgentInstance:output_type -> kagent.api.v1alpha1.ResumeAgentInstanceResponse - 16, // 38: kagent.api.v1alpha1.AgentInstanceService.DeleteAgentInstance:output_type -> kagent.api.v1alpha1.DeleteAgentInstanceResponse - 19, // 39: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstanceShare:output_type -> kagent.api.v1alpha1.CreateAgentInstanceShareResponse - 21, // 40: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstanceShares:output_type -> kagent.api.v1alpha1.ListAgentInstanceSharesResponse - 23, // 41: kagent.api.v1alpha1.AgentInstanceService.RevokeAgentInstanceShare:output_type -> kagent.api.v1alpha1.RevokeAgentInstanceShareResponse - 33, // [33:42] is the sub-list for method output_type - 24, // [24:33] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 31, // 13: kagent.api.v1alpha1.ListAgentInstancesResponse.page:type_name -> kagent.api.v1alpha1.PageResponse + 4, // 14: kagent.api.v1alpha1.RenameAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance + 4, // 15: kagent.api.v1alpha1.SuspendAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance + 4, // 16: kagent.api.v1alpha1.ResumeAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance + 4, // 17: kagent.api.v1alpha1.DeleteAgentInstanceResponse.agent_instance:type_name -> kagent.api.v1alpha1.AgentInstance + 2, // 18: kagent.api.v1alpha1.AgentInstanceShare.permission:type_name -> kagent.api.v1alpha1.AgentInstanceSharePermission + 29, // 19: kagent.api.v1alpha1.AgentInstanceShare.created_at:type_name -> google.protobuf.Timestamp + 2, // 20: kagent.api.v1alpha1.CreateAgentInstanceShareRequest.permission:type_name -> kagent.api.v1alpha1.AgentInstanceSharePermission + 19, // 21: kagent.api.v1alpha1.CreateAgentInstanceShareResponse.share:type_name -> kagent.api.v1alpha1.AgentInstanceShare + 30, // 22: kagent.api.v1alpha1.ListAgentInstanceSharesRequest.page:type_name -> kagent.api.v1alpha1.PageRequest + 19, // 23: kagent.api.v1alpha1.ListAgentInstanceSharesResponse.shares:type_name -> kagent.api.v1alpha1.AgentInstanceShare + 31, // 24: kagent.api.v1alpha1.ListAgentInstanceSharesResponse.page:type_name -> kagent.api.v1alpha1.PageResponse + 5, // 25: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstance:input_type -> kagent.api.v1alpha1.CreateAgentInstanceRequest + 7, // 26: kagent.api.v1alpha1.AgentInstanceService.GetAgentInstance:input_type -> kagent.api.v1alpha1.GetAgentInstanceRequest + 9, // 27: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstances:input_type -> kagent.api.v1alpha1.ListAgentInstancesRequest + 11, // 28: kagent.api.v1alpha1.AgentInstanceService.RenameAgentInstance:input_type -> kagent.api.v1alpha1.RenameAgentInstanceRequest + 13, // 29: kagent.api.v1alpha1.AgentInstanceService.SuspendAgentInstance:input_type -> kagent.api.v1alpha1.SuspendAgentInstanceRequest + 15, // 30: kagent.api.v1alpha1.AgentInstanceService.ResumeAgentInstance:input_type -> kagent.api.v1alpha1.ResumeAgentInstanceRequest + 17, // 31: kagent.api.v1alpha1.AgentInstanceService.DeleteAgentInstance:input_type -> kagent.api.v1alpha1.DeleteAgentInstanceRequest + 20, // 32: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstanceShare:input_type -> kagent.api.v1alpha1.CreateAgentInstanceShareRequest + 22, // 33: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstanceShares:input_type -> kagent.api.v1alpha1.ListAgentInstanceSharesRequest + 24, // 34: kagent.api.v1alpha1.AgentInstanceService.RevokeAgentInstanceShare:input_type -> kagent.api.v1alpha1.RevokeAgentInstanceShareRequest + 6, // 35: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstance:output_type -> kagent.api.v1alpha1.CreateAgentInstanceResponse + 8, // 36: kagent.api.v1alpha1.AgentInstanceService.GetAgentInstance:output_type -> kagent.api.v1alpha1.GetAgentInstanceResponse + 10, // 37: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstances:output_type -> kagent.api.v1alpha1.ListAgentInstancesResponse + 12, // 38: kagent.api.v1alpha1.AgentInstanceService.RenameAgentInstance:output_type -> kagent.api.v1alpha1.RenameAgentInstanceResponse + 14, // 39: kagent.api.v1alpha1.AgentInstanceService.SuspendAgentInstance:output_type -> kagent.api.v1alpha1.SuspendAgentInstanceResponse + 16, // 40: kagent.api.v1alpha1.AgentInstanceService.ResumeAgentInstance:output_type -> kagent.api.v1alpha1.ResumeAgentInstanceResponse + 18, // 41: kagent.api.v1alpha1.AgentInstanceService.DeleteAgentInstance:output_type -> kagent.api.v1alpha1.DeleteAgentInstanceResponse + 21, // 42: kagent.api.v1alpha1.AgentInstanceService.CreateAgentInstanceShare:output_type -> kagent.api.v1alpha1.CreateAgentInstanceShareResponse + 23, // 43: kagent.api.v1alpha1.AgentInstanceService.ListAgentInstanceShares:output_type -> kagent.api.v1alpha1.ListAgentInstanceSharesResponse + 25, // 44: kagent.api.v1alpha1.AgentInstanceService.RevokeAgentInstanceShare:output_type -> kagent.api.v1alpha1.RevokeAgentInstanceShareResponse + 35, // [35:45] is the sub-list for method output_type + 25, // [25:35] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_kagent_api_v1alpha1_agent_instances_proto_init() } @@ -1632,7 +1795,7 @@ func file_kagent_api_v1alpha1_agent_instances_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_agent_instances_proto_rawDesc), len(file_kagent_api_v1alpha1_agent_instances_proto_rawDesc)), NumEnums: 3, - NumMessages: 23, + NumMessages: 25, NumExtensions: 0, NumServices: 1, }, diff --git a/go/api/gen/kagent/api/v1alpha1/agent_instances_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/agent_instances_grpc.pb.go index aea2c63c8..96fbd0cdc 100644 --- a/go/api/gen/kagent/api/v1alpha1/agent_instances_grpc.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/agent_instances_grpc.pb.go @@ -22,6 +22,7 @@ const ( AgentInstanceService_CreateAgentInstance_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/CreateAgentInstance" AgentInstanceService_GetAgentInstance_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/GetAgentInstance" AgentInstanceService_ListAgentInstances_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/ListAgentInstances" + AgentInstanceService_RenameAgentInstance_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/RenameAgentInstance" AgentInstanceService_SuspendAgentInstance_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/SuspendAgentInstance" AgentInstanceService_ResumeAgentInstance_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/ResumeAgentInstance" AgentInstanceService_DeleteAgentInstance_FullMethodName = "/kagent.api.v1alpha1.AgentInstanceService/DeleteAgentInstance" @@ -37,6 +38,7 @@ type AgentInstanceServiceClient interface { CreateAgentInstance(ctx context.Context, in *CreateAgentInstanceRequest, opts ...grpc.CallOption) (*CreateAgentInstanceResponse, error) GetAgentInstance(ctx context.Context, in *GetAgentInstanceRequest, opts ...grpc.CallOption) (*GetAgentInstanceResponse, error) ListAgentInstances(ctx context.Context, in *ListAgentInstancesRequest, opts ...grpc.CallOption) (*ListAgentInstancesResponse, error) + RenameAgentInstance(ctx context.Context, in *RenameAgentInstanceRequest, opts ...grpc.CallOption) (*RenameAgentInstanceResponse, error) SuspendAgentInstance(ctx context.Context, in *SuspendAgentInstanceRequest, opts ...grpc.CallOption) (*SuspendAgentInstanceResponse, error) ResumeAgentInstance(ctx context.Context, in *ResumeAgentInstanceRequest, opts ...grpc.CallOption) (*ResumeAgentInstanceResponse, error) DeleteAgentInstance(ctx context.Context, in *DeleteAgentInstanceRequest, opts ...grpc.CallOption) (*DeleteAgentInstanceResponse, error) @@ -83,6 +85,16 @@ func (c *agentInstanceServiceClient) ListAgentInstances(ctx context.Context, in return out, nil } +func (c *agentInstanceServiceClient) RenameAgentInstance(ctx context.Context, in *RenameAgentInstanceRequest, opts ...grpc.CallOption) (*RenameAgentInstanceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RenameAgentInstanceResponse) + err := c.cc.Invoke(ctx, AgentInstanceService_RenameAgentInstance_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *agentInstanceServiceClient) SuspendAgentInstance(ctx context.Context, in *SuspendAgentInstanceRequest, opts ...grpc.CallOption) (*SuspendAgentInstanceResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SuspendAgentInstanceResponse) @@ -150,6 +162,7 @@ type AgentInstanceServiceServer interface { CreateAgentInstance(context.Context, *CreateAgentInstanceRequest) (*CreateAgentInstanceResponse, error) GetAgentInstance(context.Context, *GetAgentInstanceRequest) (*GetAgentInstanceResponse, error) ListAgentInstances(context.Context, *ListAgentInstancesRequest) (*ListAgentInstancesResponse, error) + RenameAgentInstance(context.Context, *RenameAgentInstanceRequest) (*RenameAgentInstanceResponse, error) SuspendAgentInstance(context.Context, *SuspendAgentInstanceRequest) (*SuspendAgentInstanceResponse, error) ResumeAgentInstance(context.Context, *ResumeAgentInstanceRequest) (*ResumeAgentInstanceResponse, error) DeleteAgentInstance(context.Context, *DeleteAgentInstanceRequest) (*DeleteAgentInstanceResponse, error) @@ -175,6 +188,9 @@ func (UnimplementedAgentInstanceServiceServer) GetAgentInstance(context.Context, func (UnimplementedAgentInstanceServiceServer) ListAgentInstances(context.Context, *ListAgentInstancesRequest) (*ListAgentInstancesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListAgentInstances not implemented") } +func (UnimplementedAgentInstanceServiceServer) RenameAgentInstance(context.Context, *RenameAgentInstanceRequest) (*RenameAgentInstanceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RenameAgentInstance not implemented") +} func (UnimplementedAgentInstanceServiceServer) SuspendAgentInstance(context.Context, *SuspendAgentInstanceRequest) (*SuspendAgentInstanceResponse, error) { return nil, status.Error(codes.Unimplemented, "method SuspendAgentInstance not implemented") } @@ -268,6 +284,24 @@ func _AgentInstanceService_ListAgentInstances_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } +func _AgentInstanceService_RenameAgentInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RenameAgentInstanceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentInstanceServiceServer).RenameAgentInstance(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentInstanceService_RenameAgentInstance_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentInstanceServiceServer).RenameAgentInstance(ctx, req.(*RenameAgentInstanceRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _AgentInstanceService_SuspendAgentInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SuspendAgentInstanceRequest) if err := dec(in); err != nil { @@ -395,6 +429,10 @@ var AgentInstanceService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListAgentInstances", Handler: _AgentInstanceService_ListAgentInstances_Handler, }, + { + MethodName: "RenameAgentInstance", + Handler: _AgentInstanceService_RenameAgentInstance_Handler, + }, { MethodName: "SuspendAgentInstance", Handler: _AgentInstanceService_SuspendAgentInstance_Handler, diff --git a/go/api/gen/kagent/api/v1alpha1/agent_templates.pb.go b/go/api/gen/kagent/api/v1alpha1/agent_templates.pb.go new file mode 100644 index 000000000..dc557ee3b --- /dev/null +++ b/go/api/gen/kagent/api/v1alpha1/agent_templates.pb.go @@ -0,0 +1,679 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: kagent/api/v1alpha1/agent_templates.proto + +package apiv1alpha1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AgentTemplate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + // Resource is the whole AgentTemplate CR. The spec is rich — model config, + // system prompt, tools, skills, plugins — and is carried verbatim rather than + // re-modelled here so that a CRD change cannot silently drift from the API. + Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + // ModelConfigRef is spec.modelConfig resolved into the template's namespace. + // Denormalised because it is required on every template and is what a caller + // needs to render a list without parsing each spec. + ModelConfigRef *ResourceReference `protobuf:"bytes,3,opt,name=model_config_ref,json=modelConfigRef,proto3" json:"model_config_ref,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + // AdmittingHarnesses names the same-namespace Harnesses whose admission + // selector matches this template, as reported in status. It is the set a + // caller may legally pair with this template in CreateAgentInstance, and it + // is derivable only from the Harness side, so a caller cannot compute it. + AdmittingHarnesses []string `protobuf:"bytes,5,rep,name=admitting_harnesses,json=admittingHarnesses,proto3" json:"admitting_harnesses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AgentTemplate) Reset() { + *x = AgentTemplate{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AgentTemplate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AgentTemplate) ProtoMessage() {} + +func (x *AgentTemplate) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AgentTemplate.ProtoReflect.Descriptor instead. +func (*AgentTemplate) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{0} +} + +func (x *AgentTemplate) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *AgentTemplate) GetResource() *StructuredObject { + if x != nil { + return x.Resource + } + return nil +} + +func (x *AgentTemplate) GetModelConfigRef() *ResourceReference { + if x != nil { + return x.ModelConfigRef + } + return nil +} + +func (x *AgentTemplate) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AgentTemplate) GetAdmittingHarnesses() []string { + if x != nil { + return x.AdmittingHarnesses + } + return nil +} + +type ListAgentTemplatesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAgentTemplatesRequest) Reset() { + *x = ListAgentTemplatesRequest{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAgentTemplatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAgentTemplatesRequest) ProtoMessage() {} + +func (x *ListAgentTemplatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAgentTemplatesRequest.ProtoReflect.Descriptor instead. +func (*ListAgentTemplatesRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{1} +} + +func (x *ListAgentTemplatesRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +type ListAgentTemplatesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AgentTemplates []*AgentTemplate `protobuf:"bytes,1,rep,name=agent_templates,json=agentTemplates,proto3" json:"agent_templates,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAgentTemplatesResponse) Reset() { + *x = ListAgentTemplatesResponse{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAgentTemplatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAgentTemplatesResponse) ProtoMessage() {} + +func (x *ListAgentTemplatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAgentTemplatesResponse.ProtoReflect.Descriptor instead. +func (*ListAgentTemplatesResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{2} +} + +func (x *ListAgentTemplatesResponse) GetAgentTemplates() []*AgentTemplate { + if x != nil { + return x.AgentTemplates + } + return nil +} + +type GetAgentTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAgentTemplateRequest) Reset() { + *x = GetAgentTemplateRequest{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAgentTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAgentTemplateRequest) ProtoMessage() {} + +func (x *GetAgentTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAgentTemplateRequest.ProtoReflect.Descriptor instead. +func (*GetAgentTemplateRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{3} +} + +func (x *GetAgentTemplateRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +type GetAgentTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AgentTemplate *AgentTemplate `protobuf:"bytes,1,opt,name=agent_template,json=agentTemplate,proto3" json:"agent_template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAgentTemplateResponse) Reset() { + *x = GetAgentTemplateResponse{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAgentTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAgentTemplateResponse) ProtoMessage() {} + +func (x *GetAgentTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAgentTemplateResponse.ProtoReflect.Descriptor instead. +func (*GetAgentTemplateResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{4} +} + +func (x *GetAgentTemplateResponse) GetAgentTemplate() *AgentTemplate { + if x != nil { + return x.AgentTemplate + } + return nil +} + +type CreateAgentTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAgentTemplateRequest) Reset() { + *x = CreateAgentTemplateRequest{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAgentTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAgentTemplateRequest) ProtoMessage() {} + +func (x *CreateAgentTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateAgentTemplateRequest.ProtoReflect.Descriptor instead. +func (*CreateAgentTemplateRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateAgentTemplateRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *CreateAgentTemplateRequest) GetResource() *StructuredObject { + if x != nil { + return x.Resource + } + return nil +} + +type CreateAgentTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AgentTemplate *AgentTemplate `protobuf:"bytes,1,opt,name=agent_template,json=agentTemplate,proto3" json:"agent_template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAgentTemplateResponse) Reset() { + *x = CreateAgentTemplateResponse{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAgentTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAgentTemplateResponse) ProtoMessage() {} + +func (x *CreateAgentTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateAgentTemplateResponse.ProtoReflect.Descriptor instead. +func (*CreateAgentTemplateResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateAgentTemplateResponse) GetAgentTemplate() *AgentTemplate { + if x != nil { + return x.AgentTemplate + } + return nil +} + +type UpdateAgentTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAgentTemplateRequest) Reset() { + *x = UpdateAgentTemplateRequest{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAgentTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAgentTemplateRequest) ProtoMessage() {} + +func (x *UpdateAgentTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateAgentTemplateRequest.ProtoReflect.Descriptor instead. +func (*UpdateAgentTemplateRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateAgentTemplateRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *UpdateAgentTemplateRequest) GetResource() *StructuredObject { + if x != nil { + return x.Resource + } + return nil +} + +type UpdateAgentTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AgentTemplate *AgentTemplate `protobuf:"bytes,1,opt,name=agent_template,json=agentTemplate,proto3" json:"agent_template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAgentTemplateResponse) Reset() { + *x = UpdateAgentTemplateResponse{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAgentTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAgentTemplateResponse) ProtoMessage() {} + +func (x *UpdateAgentTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateAgentTemplateResponse.ProtoReflect.Descriptor instead. +func (*UpdateAgentTemplateResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdateAgentTemplateResponse) GetAgentTemplate() *AgentTemplate { + if x != nil { + return x.AgentTemplate + } + return nil +} + +type DeleteAgentTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAgentTemplateRequest) Reset() { + *x = DeleteAgentTemplateRequest{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAgentTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAgentTemplateRequest) ProtoMessage() {} + +func (x *DeleteAgentTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAgentTemplateRequest.ProtoReflect.Descriptor instead. +func (*DeleteAgentTemplateRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteAgentTemplateRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +type DeleteAgentTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAgentTemplateResponse) Reset() { + *x = DeleteAgentTemplateResponse{} + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAgentTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAgentTemplateResponse) ProtoMessage() {} + +func (x *DeleteAgentTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_agent_templates_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAgentTemplateResponse.ProtoReflect.Descriptor instead. +func (*DeleteAgentTemplateResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP(), []int{10} +} + +var File_kagent_api_v1alpha1_agent_templates_proto protoreflect.FileDescriptor + +const file_kagent_api_v1alpha1_agent_templates_proto_rawDesc = "" + + "\n" + + ")kagent/api/v1alpha1/agent_templates.proto\x12\x13kagent.api.v1alpha1\x1a kagent/api/v1alpha1/common.proto\"\xb1\x02\n" + + "\rAgentTemplate\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\x12P\n" + + "\x10model_config_ref\x18\x03 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x0emodelConfigRef\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12/\n" + + "\x13admitting_harnesses\x18\x05 \x03(\tR\x12admittingHarnesses\"9\n" + + "\x19ListAgentTemplatesRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\"i\n" + + "\x1aListAgentTemplatesResponse\x12K\n" + + "\x0fagent_templates\x18\x01 \x03(\v2\".kagent.api.v1alpha1.AgentTemplateR\x0eagentTemplates\"S\n" + + "\x17GetAgentTemplateRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\"e\n" + + "\x18GetAgentTemplateResponse\x12I\n" + + "\x0eagent_template\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentTemplateR\ragentTemplate\"\x99\x01\n" + + "\x1aCreateAgentTemplateRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\"h\n" + + "\x1bCreateAgentTemplateResponse\x12I\n" + + "\x0eagent_template\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentTemplateR\ragentTemplate\"\x99\x01\n" + + "\x1aUpdateAgentTemplateRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\"h\n" + + "\x1bUpdateAgentTemplateResponse\x12I\n" + + "\x0eagent_template\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentTemplateR\ragentTemplate\"V\n" + + "\x1aDeleteAgentTemplateRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\"\x1d\n" + + "\x1bDeleteAgentTemplateResponse2\xec\x04\n" + + "\x14AgentTemplateService\x12u\n" + + "\x12ListAgentTemplates\x12..kagent.api.v1alpha1.ListAgentTemplatesRequest\x1a/.kagent.api.v1alpha1.ListAgentTemplatesResponse\x12o\n" + + "\x10GetAgentTemplate\x12,.kagent.api.v1alpha1.GetAgentTemplateRequest\x1a-.kagent.api.v1alpha1.GetAgentTemplateResponse\x12x\n" + + "\x13CreateAgentTemplate\x12/.kagent.api.v1alpha1.CreateAgentTemplateRequest\x1a0.kagent.api.v1alpha1.CreateAgentTemplateResponse\x12x\n" + + "\x13UpdateAgentTemplate\x12/.kagent.api.v1alpha1.UpdateAgentTemplateRequest\x1a0.kagent.api.v1alpha1.UpdateAgentTemplateResponse\x12x\n" + + "\x13DeleteAgentTemplate\x12/.kagent.api.v1alpha1.DeleteAgentTemplateRequest\x1a0.kagent.api.v1alpha1.DeleteAgentTemplateResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" + +var ( + file_kagent_api_v1alpha1_agent_templates_proto_rawDescOnce sync.Once + file_kagent_api_v1alpha1_agent_templates_proto_rawDescData []byte +) + +func file_kagent_api_v1alpha1_agent_templates_proto_rawDescGZIP() []byte { + file_kagent_api_v1alpha1_agent_templates_proto_rawDescOnce.Do(func() { + file_kagent_api_v1alpha1_agent_templates_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_agent_templates_proto_rawDesc), len(file_kagent_api_v1alpha1_agent_templates_proto_rawDesc))) + }) + return file_kagent_api_v1alpha1_agent_templates_proto_rawDescData +} + +var file_kagent_api_v1alpha1_agent_templates_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_kagent_api_v1alpha1_agent_templates_proto_goTypes = []any{ + (*AgentTemplate)(nil), // 0: kagent.api.v1alpha1.AgentTemplate + (*ListAgentTemplatesRequest)(nil), // 1: kagent.api.v1alpha1.ListAgentTemplatesRequest + (*ListAgentTemplatesResponse)(nil), // 2: kagent.api.v1alpha1.ListAgentTemplatesResponse + (*GetAgentTemplateRequest)(nil), // 3: kagent.api.v1alpha1.GetAgentTemplateRequest + (*GetAgentTemplateResponse)(nil), // 4: kagent.api.v1alpha1.GetAgentTemplateResponse + (*CreateAgentTemplateRequest)(nil), // 5: kagent.api.v1alpha1.CreateAgentTemplateRequest + (*CreateAgentTemplateResponse)(nil), // 6: kagent.api.v1alpha1.CreateAgentTemplateResponse + (*UpdateAgentTemplateRequest)(nil), // 7: kagent.api.v1alpha1.UpdateAgentTemplateRequest + (*UpdateAgentTemplateResponse)(nil), // 8: kagent.api.v1alpha1.UpdateAgentTemplateResponse + (*DeleteAgentTemplateRequest)(nil), // 9: kagent.api.v1alpha1.DeleteAgentTemplateRequest + (*DeleteAgentTemplateResponse)(nil), // 10: kagent.api.v1alpha1.DeleteAgentTemplateResponse + (*ResourceReference)(nil), // 11: kagent.api.v1alpha1.ResourceReference + (*StructuredObject)(nil), // 12: kagent.api.v1alpha1.StructuredObject +} +var file_kagent_api_v1alpha1_agent_templates_proto_depIdxs = []int32{ + 11, // 0: kagent.api.v1alpha1.AgentTemplate.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 12, // 1: kagent.api.v1alpha1.AgentTemplate.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 11, // 2: kagent.api.v1alpha1.AgentTemplate.model_config_ref:type_name -> kagent.api.v1alpha1.ResourceReference + 0, // 3: kagent.api.v1alpha1.ListAgentTemplatesResponse.agent_templates:type_name -> kagent.api.v1alpha1.AgentTemplate + 11, // 4: kagent.api.v1alpha1.GetAgentTemplateRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 0, // 5: kagent.api.v1alpha1.GetAgentTemplateResponse.agent_template:type_name -> kagent.api.v1alpha1.AgentTemplate + 11, // 6: kagent.api.v1alpha1.CreateAgentTemplateRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 12, // 7: kagent.api.v1alpha1.CreateAgentTemplateRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 0, // 8: kagent.api.v1alpha1.CreateAgentTemplateResponse.agent_template:type_name -> kagent.api.v1alpha1.AgentTemplate + 11, // 9: kagent.api.v1alpha1.UpdateAgentTemplateRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 12, // 10: kagent.api.v1alpha1.UpdateAgentTemplateRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 0, // 11: kagent.api.v1alpha1.UpdateAgentTemplateResponse.agent_template:type_name -> kagent.api.v1alpha1.AgentTemplate + 11, // 12: kagent.api.v1alpha1.DeleteAgentTemplateRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 1, // 13: kagent.api.v1alpha1.AgentTemplateService.ListAgentTemplates:input_type -> kagent.api.v1alpha1.ListAgentTemplatesRequest + 3, // 14: kagent.api.v1alpha1.AgentTemplateService.GetAgentTemplate:input_type -> kagent.api.v1alpha1.GetAgentTemplateRequest + 5, // 15: kagent.api.v1alpha1.AgentTemplateService.CreateAgentTemplate:input_type -> kagent.api.v1alpha1.CreateAgentTemplateRequest + 7, // 16: kagent.api.v1alpha1.AgentTemplateService.UpdateAgentTemplate:input_type -> kagent.api.v1alpha1.UpdateAgentTemplateRequest + 9, // 17: kagent.api.v1alpha1.AgentTemplateService.DeleteAgentTemplate:input_type -> kagent.api.v1alpha1.DeleteAgentTemplateRequest + 2, // 18: kagent.api.v1alpha1.AgentTemplateService.ListAgentTemplates:output_type -> kagent.api.v1alpha1.ListAgentTemplatesResponse + 4, // 19: kagent.api.v1alpha1.AgentTemplateService.GetAgentTemplate:output_type -> kagent.api.v1alpha1.GetAgentTemplateResponse + 6, // 20: kagent.api.v1alpha1.AgentTemplateService.CreateAgentTemplate:output_type -> kagent.api.v1alpha1.CreateAgentTemplateResponse + 8, // 21: kagent.api.v1alpha1.AgentTemplateService.UpdateAgentTemplate:output_type -> kagent.api.v1alpha1.UpdateAgentTemplateResponse + 10, // 22: kagent.api.v1alpha1.AgentTemplateService.DeleteAgentTemplate:output_type -> kagent.api.v1alpha1.DeleteAgentTemplateResponse + 18, // [18:23] is the sub-list for method output_type + 13, // [13:18] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_kagent_api_v1alpha1_agent_templates_proto_init() } +func file_kagent_api_v1alpha1_agent_templates_proto_init() { + if File_kagent_api_v1alpha1_agent_templates_proto != nil { + return + } + file_kagent_api_v1alpha1_common_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_agent_templates_proto_rawDesc), len(file_kagent_api_v1alpha1_agent_templates_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_kagent_api_v1alpha1_agent_templates_proto_goTypes, + DependencyIndexes: file_kagent_api_v1alpha1_agent_templates_proto_depIdxs, + MessageInfos: file_kagent_api_v1alpha1_agent_templates_proto_msgTypes, + }.Build() + File_kagent_api_v1alpha1_agent_templates_proto = out.File + file_kagent_api_v1alpha1_agent_templates_proto_goTypes = nil + file_kagent_api_v1alpha1_agent_templates_proto_depIdxs = nil +} diff --git a/go/api/gen/kagent/api/v1alpha1/agent_templates_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/agent_templates_grpc.pb.go new file mode 100644 index 000000000..26260e799 --- /dev/null +++ b/go/api/gen/kagent/api/v1alpha1/agent_templates_grpc.pb.go @@ -0,0 +1,283 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: kagent/api/v1alpha1/agent_templates.proto + +package apiv1alpha1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AgentTemplateService_ListAgentTemplates_FullMethodName = "/kagent.api.v1alpha1.AgentTemplateService/ListAgentTemplates" + AgentTemplateService_GetAgentTemplate_FullMethodName = "/kagent.api.v1alpha1.AgentTemplateService/GetAgentTemplate" + AgentTemplateService_CreateAgentTemplate_FullMethodName = "/kagent.api.v1alpha1.AgentTemplateService/CreateAgentTemplate" + AgentTemplateService_UpdateAgentTemplate_FullMethodName = "/kagent.api.v1alpha1.AgentTemplateService/UpdateAgentTemplate" + AgentTemplateService_DeleteAgentTemplate_FullMethodName = "/kagent.api.v1alpha1.AgentTemplateService/DeleteAgentTemplate" +) + +// AgentTemplateServiceClient is the client API for AgentTemplateService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// AgentTemplateService is CRUD over the kagent.dev/v1alpha3 AgentTemplate CRD: +// the portable-behavior half of the (Harness, AgentTemplate) pair that +// CreateAgentInstance names. Without it an AgentTemplate can only be authored +// with kubectl, so no caller can offer a template picker or an edit form. +type AgentTemplateServiceClient interface { + ListAgentTemplates(ctx context.Context, in *ListAgentTemplatesRequest, opts ...grpc.CallOption) (*ListAgentTemplatesResponse, error) + GetAgentTemplate(ctx context.Context, in *GetAgentTemplateRequest, opts ...grpc.CallOption) (*GetAgentTemplateResponse, error) + CreateAgentTemplate(ctx context.Context, in *CreateAgentTemplateRequest, opts ...grpc.CallOption) (*CreateAgentTemplateResponse, error) + UpdateAgentTemplate(ctx context.Context, in *UpdateAgentTemplateRequest, opts ...grpc.CallOption) (*UpdateAgentTemplateResponse, error) + DeleteAgentTemplate(ctx context.Context, in *DeleteAgentTemplateRequest, opts ...grpc.CallOption) (*DeleteAgentTemplateResponse, error) +} + +type agentTemplateServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAgentTemplateServiceClient(cc grpc.ClientConnInterface) AgentTemplateServiceClient { + return &agentTemplateServiceClient{cc} +} + +func (c *agentTemplateServiceClient) ListAgentTemplates(ctx context.Context, in *ListAgentTemplatesRequest, opts ...grpc.CallOption) (*ListAgentTemplatesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListAgentTemplatesResponse) + err := c.cc.Invoke(ctx, AgentTemplateService_ListAgentTemplates_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentTemplateServiceClient) GetAgentTemplate(ctx context.Context, in *GetAgentTemplateRequest, opts ...grpc.CallOption) (*GetAgentTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAgentTemplateResponse) + err := c.cc.Invoke(ctx, AgentTemplateService_GetAgentTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentTemplateServiceClient) CreateAgentTemplate(ctx context.Context, in *CreateAgentTemplateRequest, opts ...grpc.CallOption) (*CreateAgentTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateAgentTemplateResponse) + err := c.cc.Invoke(ctx, AgentTemplateService_CreateAgentTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentTemplateServiceClient) UpdateAgentTemplate(ctx context.Context, in *UpdateAgentTemplateRequest, opts ...grpc.CallOption) (*UpdateAgentTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateAgentTemplateResponse) + err := c.cc.Invoke(ctx, AgentTemplateService_UpdateAgentTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentTemplateServiceClient) DeleteAgentTemplate(ctx context.Context, in *DeleteAgentTemplateRequest, opts ...grpc.CallOption) (*DeleteAgentTemplateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteAgentTemplateResponse) + err := c.cc.Invoke(ctx, AgentTemplateService_DeleteAgentTemplate_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AgentTemplateServiceServer is the server API for AgentTemplateService service. +// All implementations must embed UnimplementedAgentTemplateServiceServer +// for forward compatibility. +// +// AgentTemplateService is CRUD over the kagent.dev/v1alpha3 AgentTemplate CRD: +// the portable-behavior half of the (Harness, AgentTemplate) pair that +// CreateAgentInstance names. Without it an AgentTemplate can only be authored +// with kubectl, so no caller can offer a template picker or an edit form. +type AgentTemplateServiceServer interface { + ListAgentTemplates(context.Context, *ListAgentTemplatesRequest) (*ListAgentTemplatesResponse, error) + GetAgentTemplate(context.Context, *GetAgentTemplateRequest) (*GetAgentTemplateResponse, error) + CreateAgentTemplate(context.Context, *CreateAgentTemplateRequest) (*CreateAgentTemplateResponse, error) + UpdateAgentTemplate(context.Context, *UpdateAgentTemplateRequest) (*UpdateAgentTemplateResponse, error) + DeleteAgentTemplate(context.Context, *DeleteAgentTemplateRequest) (*DeleteAgentTemplateResponse, error) + mustEmbedUnimplementedAgentTemplateServiceServer() +} + +// UnimplementedAgentTemplateServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAgentTemplateServiceServer struct{} + +func (UnimplementedAgentTemplateServiceServer) ListAgentTemplates(context.Context, *ListAgentTemplatesRequest) (*ListAgentTemplatesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListAgentTemplates not implemented") +} +func (UnimplementedAgentTemplateServiceServer) GetAgentTemplate(context.Context, *GetAgentTemplateRequest) (*GetAgentTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAgentTemplate not implemented") +} +func (UnimplementedAgentTemplateServiceServer) CreateAgentTemplate(context.Context, *CreateAgentTemplateRequest) (*CreateAgentTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateAgentTemplate not implemented") +} +func (UnimplementedAgentTemplateServiceServer) UpdateAgentTemplate(context.Context, *UpdateAgentTemplateRequest) (*UpdateAgentTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateAgentTemplate not implemented") +} +func (UnimplementedAgentTemplateServiceServer) DeleteAgentTemplate(context.Context, *DeleteAgentTemplateRequest) (*DeleteAgentTemplateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteAgentTemplate not implemented") +} +func (UnimplementedAgentTemplateServiceServer) mustEmbedUnimplementedAgentTemplateServiceServer() {} +func (UnimplementedAgentTemplateServiceServer) testEmbeddedByValue() {} + +// UnsafeAgentTemplateServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AgentTemplateServiceServer will +// result in compilation errors. +type UnsafeAgentTemplateServiceServer interface { + mustEmbedUnimplementedAgentTemplateServiceServer() +} + +func RegisterAgentTemplateServiceServer(s grpc.ServiceRegistrar, srv AgentTemplateServiceServer) { + // If the following call panics, it indicates UnimplementedAgentTemplateServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AgentTemplateService_ServiceDesc, srv) +} + +func _AgentTemplateService_ListAgentTemplates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAgentTemplatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentTemplateServiceServer).ListAgentTemplates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentTemplateService_ListAgentTemplates_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentTemplateServiceServer).ListAgentTemplates(ctx, req.(*ListAgentTemplatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentTemplateService_GetAgentTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAgentTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentTemplateServiceServer).GetAgentTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentTemplateService_GetAgentTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentTemplateServiceServer).GetAgentTemplate(ctx, req.(*GetAgentTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentTemplateService_CreateAgentTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateAgentTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentTemplateServiceServer).CreateAgentTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentTemplateService_CreateAgentTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentTemplateServiceServer).CreateAgentTemplate(ctx, req.(*CreateAgentTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentTemplateService_UpdateAgentTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateAgentTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentTemplateServiceServer).UpdateAgentTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentTemplateService_UpdateAgentTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentTemplateServiceServer).UpdateAgentTemplate(ctx, req.(*UpdateAgentTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentTemplateService_DeleteAgentTemplate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteAgentTemplateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentTemplateServiceServer).DeleteAgentTemplate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentTemplateService_DeleteAgentTemplate_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentTemplateServiceServer).DeleteAgentTemplate(ctx, req.(*DeleteAgentTemplateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AgentTemplateService_ServiceDesc is the grpc.ServiceDesc for AgentTemplateService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AgentTemplateService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "kagent.api.v1alpha1.AgentTemplateService", + HandlerType: (*AgentTemplateServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListAgentTemplates", + Handler: _AgentTemplateService_ListAgentTemplates_Handler, + }, + { + MethodName: "GetAgentTemplate", + Handler: _AgentTemplateService_GetAgentTemplate_Handler, + }, + { + MethodName: "CreateAgentTemplate", + Handler: _AgentTemplateService_CreateAgentTemplate_Handler, + }, + { + MethodName: "UpdateAgentTemplate", + Handler: _AgentTemplateService_UpdateAgentTemplate_Handler, + }, + { + MethodName: "DeleteAgentTemplate", + Handler: _AgentTemplateService_DeleteAgentTemplate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "kagent/api/v1alpha1/agent_templates.proto", +} diff --git a/go/api/gen/kagent/api/v1alpha1/agents_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/agents_grpc.pb.go index de037e6cb..c729d7439 100644 --- a/go/api/gen/kagent/api/v1alpha1/agents_grpc.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/agents_grpc.pb.go @@ -41,6 +41,10 @@ type AgentServiceClient interface { CreateSandboxAgent(ctx context.Context, in *CreateSandboxAgentRequest, opts ...grpc.CallOption) (*CreateSandboxAgentResponse, error) UpdateSandboxAgent(ctx context.Context, in *UpdateSandboxAgentRequest, opts ...grpc.CallOption) (*UpdateSandboxAgentResponse, error) DeleteSandboxAgent(ctx context.Context, in *DeleteSandboxAgentRequest, opts ...grpc.CallOption) (*DeleteSandboxAgentResponse, error) + // The AgentHarness RPCs below operate on the AgentHarness CRD — one agent + // bound to an external ACP backend. They are unrelated to the Harness CRD + // that AgentInstance pairs with an AgentTemplate; that one is served by + // HarnessService in harnesses.proto. GetAgentHarness(ctx context.Context, in *GetAgentHarnessRequest, opts ...grpc.CallOption) (*GetAgentHarnessResponse, error) CreateAgentHarness(ctx context.Context, in *CreateAgentHarnessRequest, opts ...grpc.CallOption) (*CreateAgentHarnessResponse, error) DeleteAgentHarness(ctx context.Context, in *DeleteAgentHarnessRequest, opts ...grpc.CallOption) (*DeleteAgentHarnessResponse, error) @@ -176,6 +180,10 @@ type AgentServiceServer interface { CreateSandboxAgent(context.Context, *CreateSandboxAgentRequest) (*CreateSandboxAgentResponse, error) UpdateSandboxAgent(context.Context, *UpdateSandboxAgentRequest) (*UpdateSandboxAgentResponse, error) DeleteSandboxAgent(context.Context, *DeleteSandboxAgentRequest) (*DeleteSandboxAgentResponse, error) + // The AgentHarness RPCs below operate on the AgentHarness CRD — one agent + // bound to an external ACP backend. They are unrelated to the Harness CRD + // that AgentInstance pairs with an AgentTemplate; that one is served by + // HarnessService in harnesses.proto. GetAgentHarness(context.Context, *GetAgentHarnessRequest) (*GetAgentHarnessResponse, error) CreateAgentHarness(context.Context, *CreateAgentHarnessRequest) (*CreateAgentHarnessResponse, error) DeleteAgentHarness(context.Context, *DeleteAgentHarnessRequest) (*DeleteAgentHarnessResponse, error) diff --git a/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go b/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go new file mode 100644 index 000000000..080dc50ad --- /dev/null +++ b/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go @@ -0,0 +1,677 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: kagent/api/v1alpha1/harnesses.proto + +package apiv1alpha1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Harness struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + // Resource is the whole Harness CR. The spec is carried verbatim rather than + // re-modelled here so that a CRD change cannot silently drift from the API. + Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + // Runtime is the adapter the spec selects: "kagent", "codex" or "claude". + // Denormalised because callers listing harnesses group and filter by it, and + // would otherwise each reimplement the exactly-one-of check the CRD enforces. + Runtime string `protobuf:"bytes,3,opt,name=runtime,proto3" json:"runtime,omitempty"` + // WorkloadImage is spec.workload.image, the digest-pinned runtime image. + WorkloadImage string `protobuf:"bytes,4,opt,name=workload_image,json=workloadImage,proto3" json:"workload_image,omitempty"` + // Ready mirrors the Ready status condition. False also covers a Harness the + // controller has not yet observed. + Ready bool `protobuf:"varint,5,opt,name=ready,proto3" json:"ready,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Harness) Reset() { + *x = Harness{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Harness) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Harness) ProtoMessage() {} + +func (x *Harness) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Harness.ProtoReflect.Descriptor instead. +func (*Harness) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{0} +} + +func (x *Harness) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *Harness) GetResource() *StructuredObject { + if x != nil { + return x.Resource + } + return nil +} + +func (x *Harness) GetRuntime() string { + if x != nil { + return x.Runtime + } + return "" +} + +func (x *Harness) GetWorkloadImage() string { + if x != nil { + return x.WorkloadImage + } + return "" +} + +func (x *Harness) GetReady() bool { + if x != nil { + return x.Ready + } + return false +} + +type ListHarnessesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHarnessesRequest) Reset() { + *x = ListHarnessesRequest{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHarnessesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHarnessesRequest) ProtoMessage() {} + +func (x *ListHarnessesRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListHarnessesRequest.ProtoReflect.Descriptor instead. +func (*ListHarnessesRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{1} +} + +func (x *ListHarnessesRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +type ListHarnessesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Harnesses []*Harness `protobuf:"bytes,1,rep,name=harnesses,proto3" json:"harnesses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHarnessesResponse) Reset() { + *x = ListHarnessesResponse{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHarnessesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHarnessesResponse) ProtoMessage() {} + +func (x *ListHarnessesResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListHarnessesResponse.ProtoReflect.Descriptor instead. +func (*ListHarnessesResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{2} +} + +func (x *ListHarnessesResponse) GetHarnesses() []*Harness { + if x != nil { + return x.Harnesses + } + return nil +} + +type GetHarnessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetHarnessRequest) Reset() { + *x = GetHarnessRequest{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetHarnessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHarnessRequest) ProtoMessage() {} + +func (x *GetHarnessRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHarnessRequest.ProtoReflect.Descriptor instead. +func (*GetHarnessRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{3} +} + +func (x *GetHarnessRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +type GetHarnessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Harness *Harness `protobuf:"bytes,1,opt,name=harness,proto3" json:"harness,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetHarnessResponse) Reset() { + *x = GetHarnessResponse{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetHarnessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetHarnessResponse) ProtoMessage() {} + +func (x *GetHarnessResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetHarnessResponse.ProtoReflect.Descriptor instead. +func (*GetHarnessResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{4} +} + +func (x *GetHarnessResponse) GetHarness() *Harness { + if x != nil { + return x.Harness + } + return nil +} + +type CreateHarnessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateHarnessRequest) Reset() { + *x = CreateHarnessRequest{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateHarnessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateHarnessRequest) ProtoMessage() {} + +func (x *CreateHarnessRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateHarnessRequest.ProtoReflect.Descriptor instead. +func (*CreateHarnessRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateHarnessRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *CreateHarnessRequest) GetResource() *StructuredObject { + if x != nil { + return x.Resource + } + return nil +} + +type CreateHarnessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Harness *Harness `protobuf:"bytes,1,opt,name=harness,proto3" json:"harness,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateHarnessResponse) Reset() { + *x = CreateHarnessResponse{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateHarnessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateHarnessResponse) ProtoMessage() {} + +func (x *CreateHarnessResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateHarnessResponse.ProtoReflect.Descriptor instead. +func (*CreateHarnessResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{6} +} + +func (x *CreateHarnessResponse) GetHarness() *Harness { + if x != nil { + return x.Harness + } + return nil +} + +type UpdateHarnessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateHarnessRequest) Reset() { + *x = UpdateHarnessRequest{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateHarnessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateHarnessRequest) ProtoMessage() {} + +func (x *UpdateHarnessRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateHarnessRequest.ProtoReflect.Descriptor instead. +func (*UpdateHarnessRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateHarnessRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +func (x *UpdateHarnessRequest) GetResource() *StructuredObject { + if x != nil { + return x.Resource + } + return nil +} + +type UpdateHarnessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Harness *Harness `protobuf:"bytes,1,opt,name=harness,proto3" json:"harness,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateHarnessResponse) Reset() { + *x = UpdateHarnessResponse{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateHarnessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateHarnessResponse) ProtoMessage() {} + +func (x *UpdateHarnessResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateHarnessResponse.ProtoReflect.Descriptor instead. +func (*UpdateHarnessResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdateHarnessResponse) GetHarness() *Harness { + if x != nil { + return x.Harness + } + return nil +} + +type DeleteHarnessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteHarnessRequest) Reset() { + *x = DeleteHarnessRequest{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteHarnessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteHarnessRequest) ProtoMessage() {} + +func (x *DeleteHarnessRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteHarnessRequest.ProtoReflect.Descriptor instead. +func (*DeleteHarnessRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteHarnessRequest) GetRef() *ResourceReference { + if x != nil { + return x.Ref + } + return nil +} + +type DeleteHarnessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteHarnessResponse) Reset() { + *x = DeleteHarnessResponse{} + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteHarnessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteHarnessResponse) ProtoMessage() {} + +func (x *DeleteHarnessResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteHarnessResponse.ProtoReflect.Descriptor instead. +func (*DeleteHarnessResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{10} +} + +var File_kagent_api_v1alpha1_harnesses_proto protoreflect.FileDescriptor + +const file_kagent_api_v1alpha1_harnesses_proto_rawDesc = "" + + "\n" + + "#kagent/api/v1alpha1/harnesses.proto\x12\x13kagent.api.v1alpha1\x1a kagent/api/v1alpha1/common.proto\"\xdd\x01\n" + + "\aHarness\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\x12\x18\n" + + "\aruntime\x18\x03 \x01(\tR\aruntime\x12%\n" + + "\x0eworkload_image\x18\x04 \x01(\tR\rworkloadImage\x12\x14\n" + + "\x05ready\x18\x05 \x01(\bR\x05ready\"4\n" + + "\x14ListHarnessesRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\"S\n" + + "\x15ListHarnessesResponse\x12:\n" + + "\tharnesses\x18\x01 \x03(\v2\x1c.kagent.api.v1alpha1.HarnessR\tharnesses\"M\n" + + "\x11GetHarnessRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\"L\n" + + "\x12GetHarnessResponse\x126\n" + + "\aharness\x18\x01 \x01(\v2\x1c.kagent.api.v1alpha1.HarnessR\aharness\"\x93\x01\n" + + "\x14CreateHarnessRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\"O\n" + + "\x15CreateHarnessResponse\x126\n" + + "\aharness\x18\x01 \x01(\v2\x1c.kagent.api.v1alpha1.HarnessR\aharness\"\x93\x01\n" + + "\x14UpdateHarnessRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\"O\n" + + "\x15UpdateHarnessResponse\x126\n" + + "\aharness\x18\x01 \x01(\v2\x1c.kagent.api.v1alpha1.HarnessR\aharness\"P\n" + + "\x14DeleteHarnessRequest\x128\n" + + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\"\x17\n" + + "\x15DeleteHarnessResponse2\x8f\x04\n" + + "\x0eHarnessService\x12f\n" + + "\rListHarnesses\x12).kagent.api.v1alpha1.ListHarnessesRequest\x1a*.kagent.api.v1alpha1.ListHarnessesResponse\x12]\n" + + "\n" + + "GetHarness\x12&.kagent.api.v1alpha1.GetHarnessRequest\x1a'.kagent.api.v1alpha1.GetHarnessResponse\x12f\n" + + "\rCreateHarness\x12).kagent.api.v1alpha1.CreateHarnessRequest\x1a*.kagent.api.v1alpha1.CreateHarnessResponse\x12f\n" + + "\rUpdateHarness\x12).kagent.api.v1alpha1.UpdateHarnessRequest\x1a*.kagent.api.v1alpha1.UpdateHarnessResponse\x12f\n" + + "\rDeleteHarness\x12).kagent.api.v1alpha1.DeleteHarnessRequest\x1a*.kagent.api.v1alpha1.DeleteHarnessResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" + +var ( + file_kagent_api_v1alpha1_harnesses_proto_rawDescOnce sync.Once + file_kagent_api_v1alpha1_harnesses_proto_rawDescData []byte +) + +func file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP() []byte { + file_kagent_api_v1alpha1_harnesses_proto_rawDescOnce.Do(func() { + file_kagent_api_v1alpha1_harnesses_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_harnesses_proto_rawDesc), len(file_kagent_api_v1alpha1_harnesses_proto_rawDesc))) + }) + return file_kagent_api_v1alpha1_harnesses_proto_rawDescData +} + +var file_kagent_api_v1alpha1_harnesses_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_kagent_api_v1alpha1_harnesses_proto_goTypes = []any{ + (*Harness)(nil), // 0: kagent.api.v1alpha1.Harness + (*ListHarnessesRequest)(nil), // 1: kagent.api.v1alpha1.ListHarnessesRequest + (*ListHarnessesResponse)(nil), // 2: kagent.api.v1alpha1.ListHarnessesResponse + (*GetHarnessRequest)(nil), // 3: kagent.api.v1alpha1.GetHarnessRequest + (*GetHarnessResponse)(nil), // 4: kagent.api.v1alpha1.GetHarnessResponse + (*CreateHarnessRequest)(nil), // 5: kagent.api.v1alpha1.CreateHarnessRequest + (*CreateHarnessResponse)(nil), // 6: kagent.api.v1alpha1.CreateHarnessResponse + (*UpdateHarnessRequest)(nil), // 7: kagent.api.v1alpha1.UpdateHarnessRequest + (*UpdateHarnessResponse)(nil), // 8: kagent.api.v1alpha1.UpdateHarnessResponse + (*DeleteHarnessRequest)(nil), // 9: kagent.api.v1alpha1.DeleteHarnessRequest + (*DeleteHarnessResponse)(nil), // 10: kagent.api.v1alpha1.DeleteHarnessResponse + (*ResourceReference)(nil), // 11: kagent.api.v1alpha1.ResourceReference + (*StructuredObject)(nil), // 12: kagent.api.v1alpha1.StructuredObject +} +var file_kagent_api_v1alpha1_harnesses_proto_depIdxs = []int32{ + 11, // 0: kagent.api.v1alpha1.Harness.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 12, // 1: kagent.api.v1alpha1.Harness.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 0, // 2: kagent.api.v1alpha1.ListHarnessesResponse.harnesses:type_name -> kagent.api.v1alpha1.Harness + 11, // 3: kagent.api.v1alpha1.GetHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 0, // 4: kagent.api.v1alpha1.GetHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness + 11, // 5: kagent.api.v1alpha1.CreateHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 12, // 6: kagent.api.v1alpha1.CreateHarnessRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 0, // 7: kagent.api.v1alpha1.CreateHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness + 11, // 8: kagent.api.v1alpha1.UpdateHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 12, // 9: kagent.api.v1alpha1.UpdateHarnessRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 0, // 10: kagent.api.v1alpha1.UpdateHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness + 11, // 11: kagent.api.v1alpha1.DeleteHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 1, // 12: kagent.api.v1alpha1.HarnessService.ListHarnesses:input_type -> kagent.api.v1alpha1.ListHarnessesRequest + 3, // 13: kagent.api.v1alpha1.HarnessService.GetHarness:input_type -> kagent.api.v1alpha1.GetHarnessRequest + 5, // 14: kagent.api.v1alpha1.HarnessService.CreateHarness:input_type -> kagent.api.v1alpha1.CreateHarnessRequest + 7, // 15: kagent.api.v1alpha1.HarnessService.UpdateHarness:input_type -> kagent.api.v1alpha1.UpdateHarnessRequest + 9, // 16: kagent.api.v1alpha1.HarnessService.DeleteHarness:input_type -> kagent.api.v1alpha1.DeleteHarnessRequest + 2, // 17: kagent.api.v1alpha1.HarnessService.ListHarnesses:output_type -> kagent.api.v1alpha1.ListHarnessesResponse + 4, // 18: kagent.api.v1alpha1.HarnessService.GetHarness:output_type -> kagent.api.v1alpha1.GetHarnessResponse + 6, // 19: kagent.api.v1alpha1.HarnessService.CreateHarness:output_type -> kagent.api.v1alpha1.CreateHarnessResponse + 8, // 20: kagent.api.v1alpha1.HarnessService.UpdateHarness:output_type -> kagent.api.v1alpha1.UpdateHarnessResponse + 10, // 21: kagent.api.v1alpha1.HarnessService.DeleteHarness:output_type -> kagent.api.v1alpha1.DeleteHarnessResponse + 17, // [17:22] is the sub-list for method output_type + 12, // [12:17] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name +} + +func init() { file_kagent_api_v1alpha1_harnesses_proto_init() } +func file_kagent_api_v1alpha1_harnesses_proto_init() { + if File_kagent_api_v1alpha1_harnesses_proto != nil { + return + } + file_kagent_api_v1alpha1_common_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_harnesses_proto_rawDesc), len(file_kagent_api_v1alpha1_harnesses_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_kagent_api_v1alpha1_harnesses_proto_goTypes, + DependencyIndexes: file_kagent_api_v1alpha1_harnesses_proto_depIdxs, + MessageInfos: file_kagent_api_v1alpha1_harnesses_proto_msgTypes, + }.Build() + File_kagent_api_v1alpha1_harnesses_proto = out.File + file_kagent_api_v1alpha1_harnesses_proto_goTypes = nil + file_kagent_api_v1alpha1_harnesses_proto_depIdxs = nil +} diff --git a/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go new file mode 100644 index 000000000..1047715ce --- /dev/null +++ b/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go @@ -0,0 +1,299 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: kagent/api/v1alpha1/harnesses.proto + +package apiv1alpha1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + HarnessService_ListHarnesses_FullMethodName = "/kagent.api.v1alpha1.HarnessService/ListHarnesses" + HarnessService_GetHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/GetHarness" + HarnessService_CreateHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/CreateHarness" + HarnessService_UpdateHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/UpdateHarness" + HarnessService_DeleteHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/DeleteHarness" +) + +// HarnessServiceClient is the client API for HarnessService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// HarnessService is CRUD over the kagent.dev/v1alpha3 Harness CRD: the runtime +// and infrastructure policy half of the (Harness, AgentTemplate) pair that +// CreateAgentInstance names. +// +// Harness is NOT AgentHarness. AgentService carries GetAgentHarness / +// CreateAgentHarness / DeleteAgentHarness, and those operate on the separate +// AgentHarness CRD — a single agent bound to an external ACP backend. The +// Harness here is a reusable runtime (one of the kagent, codex or claude +// adapters, plus a Substrate worker pool and snapshot policy) that admits many +// AgentTemplates through a label selector; the v2 reconciler pairs the two in +// go/core/v2/controller/collections.go. The names collide but the kinds do not, +// so this is a separate service rather than more RPCs on AgentService. +type HarnessServiceClient interface { + ListHarnesses(ctx context.Context, in *ListHarnessesRequest, opts ...grpc.CallOption) (*ListHarnessesResponse, error) + GetHarness(ctx context.Context, in *GetHarnessRequest, opts ...grpc.CallOption) (*GetHarnessResponse, error) + CreateHarness(ctx context.Context, in *CreateHarnessRequest, opts ...grpc.CallOption) (*CreateHarnessResponse, error) + UpdateHarness(ctx context.Context, in *UpdateHarnessRequest, opts ...grpc.CallOption) (*UpdateHarnessResponse, error) + DeleteHarness(ctx context.Context, in *DeleteHarnessRequest, opts ...grpc.CallOption) (*DeleteHarnessResponse, error) +} + +type harnessServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHarnessServiceClient(cc grpc.ClientConnInterface) HarnessServiceClient { + return &harnessServiceClient{cc} +} + +func (c *harnessServiceClient) ListHarnesses(ctx context.Context, in *ListHarnessesRequest, opts ...grpc.CallOption) (*ListHarnessesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListHarnessesResponse) + err := c.cc.Invoke(ctx, HarnessService_ListHarnesses_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *harnessServiceClient) GetHarness(ctx context.Context, in *GetHarnessRequest, opts ...grpc.CallOption) (*GetHarnessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetHarnessResponse) + err := c.cc.Invoke(ctx, HarnessService_GetHarness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *harnessServiceClient) CreateHarness(ctx context.Context, in *CreateHarnessRequest, opts ...grpc.CallOption) (*CreateHarnessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateHarnessResponse) + err := c.cc.Invoke(ctx, HarnessService_CreateHarness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *harnessServiceClient) UpdateHarness(ctx context.Context, in *UpdateHarnessRequest, opts ...grpc.CallOption) (*UpdateHarnessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateHarnessResponse) + err := c.cc.Invoke(ctx, HarnessService_UpdateHarness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *harnessServiceClient) DeleteHarness(ctx context.Context, in *DeleteHarnessRequest, opts ...grpc.CallOption) (*DeleteHarnessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteHarnessResponse) + err := c.cc.Invoke(ctx, HarnessService_DeleteHarness_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HarnessServiceServer is the server API for HarnessService service. +// All implementations must embed UnimplementedHarnessServiceServer +// for forward compatibility. +// +// HarnessService is CRUD over the kagent.dev/v1alpha3 Harness CRD: the runtime +// and infrastructure policy half of the (Harness, AgentTemplate) pair that +// CreateAgentInstance names. +// +// Harness is NOT AgentHarness. AgentService carries GetAgentHarness / +// CreateAgentHarness / DeleteAgentHarness, and those operate on the separate +// AgentHarness CRD — a single agent bound to an external ACP backend. The +// Harness here is a reusable runtime (one of the kagent, codex or claude +// adapters, plus a Substrate worker pool and snapshot policy) that admits many +// AgentTemplates through a label selector; the v2 reconciler pairs the two in +// go/core/v2/controller/collections.go. The names collide but the kinds do not, +// so this is a separate service rather than more RPCs on AgentService. +type HarnessServiceServer interface { + ListHarnesses(context.Context, *ListHarnessesRequest) (*ListHarnessesResponse, error) + GetHarness(context.Context, *GetHarnessRequest) (*GetHarnessResponse, error) + CreateHarness(context.Context, *CreateHarnessRequest) (*CreateHarnessResponse, error) + UpdateHarness(context.Context, *UpdateHarnessRequest) (*UpdateHarnessResponse, error) + DeleteHarness(context.Context, *DeleteHarnessRequest) (*DeleteHarnessResponse, error) + mustEmbedUnimplementedHarnessServiceServer() +} + +// UnimplementedHarnessServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHarnessServiceServer struct{} + +func (UnimplementedHarnessServiceServer) ListHarnesses(context.Context, *ListHarnessesRequest) (*ListHarnessesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListHarnesses not implemented") +} +func (UnimplementedHarnessServiceServer) GetHarness(context.Context, *GetHarnessRequest) (*GetHarnessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetHarness not implemented") +} +func (UnimplementedHarnessServiceServer) CreateHarness(context.Context, *CreateHarnessRequest) (*CreateHarnessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateHarness not implemented") +} +func (UnimplementedHarnessServiceServer) UpdateHarness(context.Context, *UpdateHarnessRequest) (*UpdateHarnessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateHarness not implemented") +} +func (UnimplementedHarnessServiceServer) DeleteHarness(context.Context, *DeleteHarnessRequest) (*DeleteHarnessResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteHarness not implemented") +} +func (UnimplementedHarnessServiceServer) mustEmbedUnimplementedHarnessServiceServer() {} +func (UnimplementedHarnessServiceServer) testEmbeddedByValue() {} + +// UnsafeHarnessServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HarnessServiceServer will +// result in compilation errors. +type UnsafeHarnessServiceServer interface { + mustEmbedUnimplementedHarnessServiceServer() +} + +func RegisterHarnessServiceServer(s grpc.ServiceRegistrar, srv HarnessServiceServer) { + // If the following call panics, it indicates UnimplementedHarnessServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&HarnessService_ServiceDesc, srv) +} + +func _HarnessService_ListHarnesses_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListHarnessesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HarnessServiceServer).ListHarnesses(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HarnessService_ListHarnesses_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HarnessServiceServer).ListHarnesses(ctx, req.(*ListHarnessesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HarnessService_GetHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetHarnessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HarnessServiceServer).GetHarness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HarnessService_GetHarness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HarnessServiceServer).GetHarness(ctx, req.(*GetHarnessRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HarnessService_CreateHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateHarnessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HarnessServiceServer).CreateHarness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HarnessService_CreateHarness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HarnessServiceServer).CreateHarness(ctx, req.(*CreateHarnessRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HarnessService_UpdateHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateHarnessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HarnessServiceServer).UpdateHarness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HarnessService_UpdateHarness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HarnessServiceServer).UpdateHarness(ctx, req.(*UpdateHarnessRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _HarnessService_DeleteHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteHarnessRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HarnessServiceServer).DeleteHarness(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HarnessService_DeleteHarness_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HarnessServiceServer).DeleteHarness(ctx, req.(*DeleteHarnessRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HarnessService_ServiceDesc is the grpc.ServiceDesc for HarnessService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HarnessService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "kagent.api.v1alpha1.HarnessService", + HandlerType: (*HarnessServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListHarnesses", + Handler: _HarnessService_ListHarnesses_Handler, + }, + { + MethodName: "GetHarness", + Handler: _HarnessService_GetHarness_Handler, + }, + { + MethodName: "CreateHarness", + Handler: _HarnessService_CreateHarness_Handler, + }, + { + MethodName: "UpdateHarness", + Handler: _HarnessService_UpdateHarness_Handler, + }, + { + MethodName: "DeleteHarness", + Handler: _HarnessService_DeleteHarness_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "kagent/api/v1alpha1/harnesses.proto", +} diff --git a/go/api/gen/kagent/api/v1alpha1/system.pb.go b/go/api/gen/kagent/api/v1alpha1/system.pb.go index ba2d169ff..1d84c6da9 100644 --- a/go/api/gen/kagent/api/v1alpha1/system.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/system.pb.go @@ -10,6 +10,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -22,6 +23,177 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// SubstrateSortOrder is the direction a paged substrate read is sorted in. +type SubstrateSortOrder int32 + +const ( + // Unspecified sorts ascending, which is what every default order below reads + // naturally in. + SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED SubstrateSortOrder = 0 + SubstrateSortOrder_SUBSTRATE_SORT_ORDER_ASCENDING SubstrateSortOrder = 1 + SubstrateSortOrder_SUBSTRATE_SORT_ORDER_DESCENDING SubstrateSortOrder = 2 +) + +// Enum value maps for SubstrateSortOrder. +var ( + SubstrateSortOrder_name = map[int32]string{ + 0: "SUBSTRATE_SORT_ORDER_UNSPECIFIED", + 1: "SUBSTRATE_SORT_ORDER_ASCENDING", + 2: "SUBSTRATE_SORT_ORDER_DESCENDING", + } + SubstrateSortOrder_value = map[string]int32{ + "SUBSTRATE_SORT_ORDER_UNSPECIFIED": 0, + "SUBSTRATE_SORT_ORDER_ASCENDING": 1, + "SUBSTRATE_SORT_ORDER_DESCENDING": 2, + } +) + +func (x SubstrateSortOrder) Enum() *SubstrateSortOrder { + p := new(SubstrateSortOrder) + *p = x + return p +} + +func (x SubstrateSortOrder) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SubstrateSortOrder) Descriptor() protoreflect.EnumDescriptor { + return file_kagent_api_v1alpha1_system_proto_enumTypes[0].Descriptor() +} + +func (SubstrateSortOrder) Type() protoreflect.EnumType { + return &file_kagent_api_v1alpha1_system_proto_enumTypes[0] +} + +func (x SubstrateSortOrder) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SubstrateSortOrder.Descriptor instead. +func (SubstrateSortOrder) EnumDescriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{0} +} + +// SubstrateActorSortField is the column ListSubstrateActors orders by. +// +// Every order ends in the actor id, which is unique — so a page token, which is +// the sort key of the last row already sent, always identifies exactly one row. +// A key that could tie would skip or repeat rows at a page boundary. +type SubstrateActorSortField int32 + +const ( + // Unspecified groups by status and orders by id within each group. That is the + // order the inventory is most usefully read in, and it is stable: ate-api + // returns actors in whatever order it holds them, so an unsorted list puts a + // different actor on every page each time it is asked. + SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED SubstrateActorSortField = 0 + SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_STATUS SubstrateActorSortField = 1 + SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID SubstrateActorSortField = 2 + SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE SubstrateActorSortField = 3 + SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD SubstrateActorSortField = 4 +) + +// Enum value maps for SubstrateActorSortField. +var ( + SubstrateActorSortField_name = map[int32]string{ + 0: "SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED", + 1: "SUBSTRATE_ACTOR_SORT_FIELD_STATUS", + 2: "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID", + 3: "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE", + 4: "SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD", + } + SubstrateActorSortField_value = map[string]int32{ + "SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED": 0, + "SUBSTRATE_ACTOR_SORT_FIELD_STATUS": 1, + "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID": 2, + "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE": 3, + "SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD": 4, + } +) + +func (x SubstrateActorSortField) Enum() *SubstrateActorSortField { + p := new(SubstrateActorSortField) + *p = x + return p +} + +func (x SubstrateActorSortField) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SubstrateActorSortField) Descriptor() protoreflect.EnumDescriptor { + return file_kagent_api_v1alpha1_system_proto_enumTypes[1].Descriptor() +} + +func (SubstrateActorSortField) Type() protoreflect.EnumType { + return &file_kagent_api_v1alpha1_system_proto_enumTypes[1] +} + +func (x SubstrateActorSortField) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SubstrateActorSortField.Descriptor instead. +func (SubstrateActorSortField) EnumDescriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{1} +} + +// SubstrateWorkerSortField is the column ListSubstrateWorkers orders by. +// Every order ends in the worker pod, which is unique within its namespace. +type SubstrateWorkerSortField int32 + +const ( + // Unspecified groups by pool and orders by pod within each group. + SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED SubstrateWorkerSortField = 0 + SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POOL SubstrateWorkerSortField = 1 + SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POD SubstrateWorkerSortField = 2 + SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_ACTOR SubstrateWorkerSortField = 3 +) + +// Enum value maps for SubstrateWorkerSortField. +var ( + SubstrateWorkerSortField_name = map[int32]string{ + 0: "SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED", + 1: "SUBSTRATE_WORKER_SORT_FIELD_POOL", + 2: "SUBSTRATE_WORKER_SORT_FIELD_POD", + 3: "SUBSTRATE_WORKER_SORT_FIELD_ACTOR", + } + SubstrateWorkerSortField_value = map[string]int32{ + "SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED": 0, + "SUBSTRATE_WORKER_SORT_FIELD_POOL": 1, + "SUBSTRATE_WORKER_SORT_FIELD_POD": 2, + "SUBSTRATE_WORKER_SORT_FIELD_ACTOR": 3, + } +) + +func (x SubstrateWorkerSortField) Enum() *SubstrateWorkerSortField { + p := new(SubstrateWorkerSortField) + *p = x + return p +} + +func (x SubstrateWorkerSortField) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SubstrateWorkerSortField) Descriptor() protoreflect.EnumDescriptor { + return file_kagent_api_v1alpha1_system_proto_enumTypes[2].Descriptor() +} + +func (SubstrateWorkerSortField) Type() protoreflect.EnumType { + return &file_kagent_api_v1alpha1_system_proto_enumTypes[2] +} + +func (x SubstrateWorkerSortField) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SubstrateWorkerSortField.Descriptor instead. +func (SubstrateWorkerSortField) EnumDescriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{2} +} + type GetVersionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -458,6 +630,598 @@ func (x *GetSubstrateStatusResponse) GetWorkers() []*SubstrateWorker { return nil } +type GetSubstrateSummaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Namespace narrows the inventory. Empty means every namespace the + // controller observes, as it does on GetSubstrateStatusRequest. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSubstrateSummaryRequest) Reset() { + *x = GetSubstrateSummaryRequest{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSubstrateSummaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSubstrateSummaryRequest) ProtoMessage() {} + +func (x *GetSubstrateSummaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSubstrateSummaryRequest.ProtoReflect.Descriptor instead. +func (*GetSubstrateSummaryRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{9} +} + +func (x *GetSubstrateSummaryRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +// SubstrateStatusCount is how many rows carry one status. +// +// Status is a plain string on the wire rather than an enum: ate-api and the +// ActorTemplate controller each fill it in their own vocabulary, so a closed +// set here would drop a status a newer substrate reports. Counting whatever +// arrives keeps the tally complete even when a value is one this build has +// never seen. +type SubstrateStatusCount struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + Count int32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubstrateStatusCount) Reset() { + *x = SubstrateStatusCount{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubstrateStatusCount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubstrateStatusCount) ProtoMessage() {} + +func (x *SubstrateStatusCount) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubstrateStatusCount.ProtoReflect.Descriptor instead. +func (*SubstrateStatusCount) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{10} +} + +func (x *SubstrateStatusCount) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SubstrateStatusCount) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +type GetSubstrateSummaryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Enabled is false when the controller has no ate-api endpoint configured, + // which is an ordinary deployment rather than a failure. + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + // AteApiError is set when ate-api answered with an error on an otherwise + // successful read: the Kubernetes-derived halves below are complete while the + // runtime counts may be short. Distinct from the RPC failing, and worth + // reporting differently. + AteApiError string `protobuf:"bytes,2,opt,name=ate_api_error,json=ateApiError,proto3" json:"ate_api_error,omitempty"` + // Worker pools and actor templates are bounded by how the cluster is + // configured rather than by how much work it is doing — a handful either way + // — so they ride inline instead of costing two more round trips. + WorkerPools []*SubstrateWorkerPool `protobuf:"bytes,3,rep,name=worker_pools,json=workerPools,proto3" json:"worker_pools,omitempty"` + ActorTemplates []*SubstrateActorTemplate `protobuf:"bytes,4,rep,name=actor_templates,json=actorTemplates,proto3" json:"actor_templates,omitempty"` + // Totals over everything in scope, before any filter. + ActorCount int32 `protobuf:"varint,5,opt,name=actor_count,json=actorCount,proto3" json:"actor_count,omitempty"` + WorkerCount int32 `protobuf:"varint,6,opt,name=worker_count,json=workerCount,proto3" json:"worker_count,omitempty"` + // RunningActorCount and BusyWorkerCount are the numerators the inventory is + // actually read by: how much of what exists is doing something. A worker is + // busy when an actor is placed on it. + RunningActorCount int32 `protobuf:"varint,7,opt,name=running_actor_count,json=runningActorCount,proto3" json:"running_actor_count,omitempty"` + BusyWorkerCount int32 `protobuf:"varint,8,opt,name=busy_worker_count,json=busyWorkerCount,proto3" json:"busy_worker_count,omitempty"` + // ActorStatusCounts is every status present, with how many actors hold it, + // ordered by status. The whole distribution rather than the running count + // alone, so a caller can say what the rest are without reading them. + ActorStatusCounts []*SubstrateStatusCount `protobuf:"bytes,9,rep,name=actor_status_counts,json=actorStatusCounts,proto3" json:"actor_status_counts,omitempty"` + // ComputedAt is when this answer was produced, which is not necessarily now. + // + // The substrate reads are memoised for a fraction of a second, because each one + // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of + // them. A cache is also exactly how a polling control becomes a lie, so the age + // travels with the answer: a caller can say "as of 0.4s ago" rather than + // implying "now", and a reader can tell a stalled cluster from a stalled read. + ComputedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=computed_at,json=computedAt,proto3" json:"computed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSubstrateSummaryResponse) Reset() { + *x = GetSubstrateSummaryResponse{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSubstrateSummaryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSubstrateSummaryResponse) ProtoMessage() {} + +func (x *GetSubstrateSummaryResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSubstrateSummaryResponse.ProtoReflect.Descriptor instead. +func (*GetSubstrateSummaryResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{11} +} + +func (x *GetSubstrateSummaryResponse) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *GetSubstrateSummaryResponse) GetAteApiError() string { + if x != nil { + return x.AteApiError + } + return "" +} + +func (x *GetSubstrateSummaryResponse) GetWorkerPools() []*SubstrateWorkerPool { + if x != nil { + return x.WorkerPools + } + return nil +} + +func (x *GetSubstrateSummaryResponse) GetActorTemplates() []*SubstrateActorTemplate { + if x != nil { + return x.ActorTemplates + } + return nil +} + +func (x *GetSubstrateSummaryResponse) GetActorCount() int32 { + if x != nil { + return x.ActorCount + } + return 0 +} + +func (x *GetSubstrateSummaryResponse) GetWorkerCount() int32 { + if x != nil { + return x.WorkerCount + } + return 0 +} + +func (x *GetSubstrateSummaryResponse) GetRunningActorCount() int32 { + if x != nil { + return x.RunningActorCount + } + return 0 +} + +func (x *GetSubstrateSummaryResponse) GetBusyWorkerCount() int32 { + if x != nil { + return x.BusyWorkerCount + } + return 0 +} + +func (x *GetSubstrateSummaryResponse) GetActorStatusCounts() []*SubstrateStatusCount { + if x != nil { + return x.ActorStatusCounts + } + return nil +} + +func (x *GetSubstrateSummaryResponse) GetComputedAt() *timestamppb.Timestamp { + if x != nil { + return x.ComputedAt + } + return nil +} + +type ListSubstrateActorsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Filter is matched case-insensitively as a substring against the actor's id, + // status, actor template and worker pod — the fields a row displays. Empty + // matches everything. + Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` + Page *PageRequest `protobuf:"bytes,3,opt,name=page,proto3" json:"page,omitempty"` + // Sorting is server-side because the rows are paged: ordering a page that has + // already been fetched reorders a hundred rows out of hundreds of thousands, + // which looks like sorting and is not. + SortField SubstrateActorSortField `protobuf:"varint,4,opt,name=sort_field,json=sortField,proto3,enum=kagent.api.v1alpha1.SubstrateActorSortField" json:"sort_field,omitempty"` + SortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSubstrateActorsRequest) Reset() { + *x = ListSubstrateActorsRequest{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSubstrateActorsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSubstrateActorsRequest) ProtoMessage() {} + +func (x *ListSubstrateActorsRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSubstrateActorsRequest.ProtoReflect.Descriptor instead. +func (*ListSubstrateActorsRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{12} +} + +func (x *ListSubstrateActorsRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ListSubstrateActorsRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ListSubstrateActorsRequest) GetPage() *PageRequest { + if x != nil { + return x.Page + } + return nil +} + +func (x *ListSubstrateActorsRequest) GetSortField() SubstrateActorSortField { + if x != nil { + return x.SortField + } + return SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED +} + +func (x *ListSubstrateActorsRequest) GetSortOrder() SubstrateSortOrder { + if x != nil { + return x.SortOrder + } + return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED +} + +type ListSubstrateActorsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Actors []*SubstrateActor `protobuf:"bytes,1,rep,name=actors,proto3" json:"actors,omitempty"` + Page *PageResponse `protobuf:"bytes,2,opt,name=page,proto3" json:"page,omitempty"` + // TotalSize is how many actors match the filter across every page, so a + // caller can say "20 of 4,312" rather than implying the page is the whole + // result. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + // The order actually applied, so a caller can say how the rows are sorted + // rather than assuming its request was honoured. An unspecified field and an + // unspecified order both resolve to a concrete value here. + AppliedSortField SubstrateActorSortField `protobuf:"varint,4,opt,name=applied_sort_field,json=appliedSortField,proto3,enum=kagent.api.v1alpha1.SubstrateActorSortField" json:"applied_sort_field,omitempty"` + AppliedSortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=applied_sort_order,json=appliedSortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"applied_sort_order,omitempty"` + // ComputedAt is when this answer was produced, which is not necessarily now. + // + // The substrate reads are memoised for a fraction of a second, because each one + // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of + // them. A cache is also exactly how a polling control becomes a lie, so the age + // travels with the answer: a caller can say "as of 0.4s ago" rather than + // implying "now", and a reader can tell a stalled cluster from a stalled read. + ComputedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=computed_at,json=computedAt,proto3" json:"computed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSubstrateActorsResponse) Reset() { + *x = ListSubstrateActorsResponse{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSubstrateActorsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSubstrateActorsResponse) ProtoMessage() {} + +func (x *ListSubstrateActorsResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSubstrateActorsResponse.ProtoReflect.Descriptor instead. +func (*ListSubstrateActorsResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{13} +} + +func (x *ListSubstrateActorsResponse) GetActors() []*SubstrateActor { + if x != nil { + return x.Actors + } + return nil +} + +func (x *ListSubstrateActorsResponse) GetPage() *PageResponse { + if x != nil { + return x.Page + } + return nil +} + +func (x *ListSubstrateActorsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +func (x *ListSubstrateActorsResponse) GetAppliedSortField() SubstrateActorSortField { + if x != nil { + return x.AppliedSortField + } + return SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED +} + +func (x *ListSubstrateActorsResponse) GetAppliedSortOrder() SubstrateSortOrder { + if x != nil { + return x.AppliedSortOrder + } + return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED +} + +func (x *ListSubstrateActorsResponse) GetComputedAt() *timestamppb.Timestamp { + if x != nil { + return x.ComputedAt + } + return nil +} + +type ListSubstrateWorkersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Filter is matched case-insensitively as a substring against the worker's + // namespace, pod, pool and placed actor. + Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` + Page *PageRequest `protobuf:"bytes,3,opt,name=page,proto3" json:"page,omitempty"` + SortField SubstrateWorkerSortField `protobuf:"varint,4,opt,name=sort_field,json=sortField,proto3,enum=kagent.api.v1alpha1.SubstrateWorkerSortField" json:"sort_field,omitempty"` + SortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSubstrateWorkersRequest) Reset() { + *x = ListSubstrateWorkersRequest{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSubstrateWorkersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSubstrateWorkersRequest) ProtoMessage() {} + +func (x *ListSubstrateWorkersRequest) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSubstrateWorkersRequest.ProtoReflect.Descriptor instead. +func (*ListSubstrateWorkersRequest) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{14} +} + +func (x *ListSubstrateWorkersRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ListSubstrateWorkersRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ListSubstrateWorkersRequest) GetPage() *PageRequest { + if x != nil { + return x.Page + } + return nil +} + +func (x *ListSubstrateWorkersRequest) GetSortField() SubstrateWorkerSortField { + if x != nil { + return x.SortField + } + return SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED +} + +func (x *ListSubstrateWorkersRequest) GetSortOrder() SubstrateSortOrder { + if x != nil { + return x.SortOrder + } + return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED +} + +type ListSubstrateWorkersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Workers []*SubstrateWorker `protobuf:"bytes,1,rep,name=workers,proto3" json:"workers,omitempty"` + Page *PageResponse `protobuf:"bytes,2,opt,name=page,proto3" json:"page,omitempty"` + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + AppliedSortField SubstrateWorkerSortField `protobuf:"varint,4,opt,name=applied_sort_field,json=appliedSortField,proto3,enum=kagent.api.v1alpha1.SubstrateWorkerSortField" json:"applied_sort_field,omitempty"` + AppliedSortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=applied_sort_order,json=appliedSortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"applied_sort_order,omitempty"` + // ComputedAt is when this answer was produced, which is not necessarily now. + // + // The substrate reads are memoised for a fraction of a second, because each one + // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of + // them. A cache is also exactly how a polling control becomes a lie, so the age + // travels with the answer: a caller can say "as of 0.4s ago" rather than + // implying "now", and a reader can tell a stalled cluster from a stalled read. + ComputedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=computed_at,json=computedAt,proto3" json:"computed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSubstrateWorkersResponse) Reset() { + *x = ListSubstrateWorkersResponse{} + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSubstrateWorkersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSubstrateWorkersResponse) ProtoMessage() {} + +func (x *ListSubstrateWorkersResponse) ProtoReflect() protoreflect.Message { + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSubstrateWorkersResponse.ProtoReflect.Descriptor instead. +func (*ListSubstrateWorkersResponse) Descriptor() ([]byte, []int) { + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{15} +} + +func (x *ListSubstrateWorkersResponse) GetWorkers() []*SubstrateWorker { + if x != nil { + return x.Workers + } + return nil +} + +func (x *ListSubstrateWorkersResponse) GetPage() *PageResponse { + if x != nil { + return x.Page + } + return nil +} + +func (x *ListSubstrateWorkersResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +func (x *ListSubstrateWorkersResponse) GetAppliedSortField() SubstrateWorkerSortField { + if x != nil { + return x.AppliedSortField + } + return SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED +} + +func (x *ListSubstrateWorkersResponse) GetAppliedSortOrder() SubstrateSortOrder { + if x != nil { + return x.AppliedSortOrder + } + return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED +} + +func (x *ListSubstrateWorkersResponse) GetComputedAt() *timestamppb.Timestamp { + if x != nil { + return x.ComputedAt + } + return nil +} + type SubstrateWorkerPool struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -470,7 +1234,7 @@ type SubstrateWorkerPool struct { func (x *SubstrateWorkerPool) Reset() { *x = SubstrateWorkerPool{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -482,7 +1246,7 @@ func (x *SubstrateWorkerPool) String() string { func (*SubstrateWorkerPool) ProtoMessage() {} func (x *SubstrateWorkerPool) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -495,7 +1259,7 @@ func (x *SubstrateWorkerPool) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateWorkerPool.ProtoReflect.Descriptor instead. func (*SubstrateWorkerPool) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{9} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{16} } func (x *SubstrateWorkerPool) GetNamespace() string { @@ -543,7 +1307,7 @@ type SubstrateActorTemplate struct { func (x *SubstrateActorTemplate) Reset() { *x = SubstrateActorTemplate{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -555,7 +1319,7 @@ func (x *SubstrateActorTemplate) String() string { func (*SubstrateActorTemplate) ProtoMessage() {} func (x *SubstrateActorTemplate) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -568,7 +1332,7 @@ func (x *SubstrateActorTemplate) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateActorTemplate.ProtoReflect.Descriptor instead. func (*SubstrateActorTemplate) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{10} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{17} } func (x *SubstrateActorTemplate) GetNamespace() string { @@ -654,7 +1418,7 @@ type SubstrateActor struct { func (x *SubstrateActor) Reset() { *x = SubstrateActor{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -666,7 +1430,7 @@ func (x *SubstrateActor) String() string { func (*SubstrateActor) ProtoMessage() {} func (x *SubstrateActor) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -679,7 +1443,7 @@ func (x *SubstrateActor) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateActor.ProtoReflect.Descriptor instead. func (*SubstrateActor) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{11} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{18} } func (x *SubstrateActor) GetActorId() string { @@ -782,7 +1546,7 @@ type SubstrateWorker struct { func (x *SubstrateWorker) Reset() { *x = SubstrateWorker{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -794,7 +1558,7 @@ func (x *SubstrateWorker) String() string { func (*SubstrateWorker) ProtoMessage() {} func (x *SubstrateWorker) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -807,7 +1571,7 @@ func (x *SubstrateWorker) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateWorker.ProtoReflect.Descriptor instead. func (*SubstrateWorker) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{12} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{19} } func (x *SubstrateWorker) GetWorkerNamespace() string { @@ -870,7 +1634,7 @@ var File_kagent_api_v1alpha1_system_proto protoreflect.FileDescriptor const file_kagent_api_v1alpha1_system_proto_rawDesc = "" + "\n" + - " kagent/api/v1alpha1/system.proto\x12\x13kagent.api.v1alpha1\x1a\x1cgoogle/protobuf/struct.proto\"\x13\n" + + " kagent/api/v1alpha1/system.proto\x12\x13kagent.api.v1alpha1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a kagent/api/v1alpha1/common.proto\"\x13\n" + "\x11GetVersionRequest\"y\n" + "\x12GetVersionResponse\x12%\n" + "\x0ekagent_version\x18\x01 \x01(\tR\rkagentVersion\x12\x1d\n" + @@ -897,7 +1661,60 @@ const file_kagent_api_v1alpha1_system_proto_rawDesc = "" + "\fworker_pools\x18\x03 \x03(\v2(.kagent.api.v1alpha1.SubstrateWorkerPoolR\vworkerPools\x12T\n" + "\x0factor_templates\x18\x04 \x03(\v2+.kagent.api.v1alpha1.SubstrateActorTemplateR\x0eactorTemplates\x12;\n" + "\x06actors\x18\x05 \x03(\v2#.kagent.api.v1alpha1.SubstrateActorR\x06actors\x12>\n" + - "\aworkers\x18\x06 \x03(\v2$.kagent.api.v1alpha1.SubstrateWorkerR\aworkers\"\x84\x01\n" + + "\aworkers\x18\x06 \x03(\v2$.kagent.api.v1alpha1.SubstrateWorkerR\aworkers\":\n" + + "\x1aGetSubstrateSummaryRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\"D\n" + + "\x14SubstrateStatusCount\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" + + "\x05count\x18\x02 \x01(\x05R\x05count\"\xb6\x04\n" + + "\x1bGetSubstrateSummaryResponse\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\"\n" + + "\rate_api_error\x18\x02 \x01(\tR\vateApiError\x12K\n" + + "\fworker_pools\x18\x03 \x03(\v2(.kagent.api.v1alpha1.SubstrateWorkerPoolR\vworkerPools\x12T\n" + + "\x0factor_templates\x18\x04 \x03(\v2+.kagent.api.v1alpha1.SubstrateActorTemplateR\x0eactorTemplates\x12\x1f\n" + + "\vactor_count\x18\x05 \x01(\x05R\n" + + "actorCount\x12!\n" + + "\fworker_count\x18\x06 \x01(\x05R\vworkerCount\x12.\n" + + "\x13running_actor_count\x18\a \x01(\x05R\x11runningActorCount\x12*\n" + + "\x11busy_worker_count\x18\b \x01(\x05R\x0fbusyWorkerCount\x12Y\n" + + "\x13actor_status_counts\x18\t \x03(\v2).kagent.api.v1alpha1.SubstrateStatusCountR\x11actorStatusCounts\x12;\n" + + "\vcomputed_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "computedAt\"\x9d\x02\n" + + "\x1aListSubstrateActorsRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06filter\x18\x02 \x01(\tR\x06filter\x124\n" + + "\x04page\x18\x03 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12K\n" + + "\n" + + "sort_field\x18\x04 \x01(\x0e2,.kagent.api.v1alpha1.SubstrateActorSortFieldR\tsortField\x12F\n" + + "\n" + + "sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\tsortOrder\"\xa0\x03\n" + + "\x1bListSubstrateActorsResponse\x12;\n" + + "\x06actors\x18\x01 \x03(\v2#.kagent.api.v1alpha1.SubstrateActorR\x06actors\x125\n" + + "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\x12Z\n" + + "\x12applied_sort_field\x18\x04 \x01(\x0e2,.kagent.api.v1alpha1.SubstrateActorSortFieldR\x10appliedSortField\x12U\n" + + "\x12applied_sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\x10appliedSortOrder\x12;\n" + + "\vcomputed_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "computedAt\"\x9f\x02\n" + + "\x1bListSubstrateWorkersRequest\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06filter\x18\x02 \x01(\tR\x06filter\x124\n" + + "\x04page\x18\x03 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12L\n" + + "\n" + + "sort_field\x18\x04 \x01(\x0e2-.kagent.api.v1alpha1.SubstrateWorkerSortFieldR\tsortField\x12F\n" + + "\n" + + "sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\tsortOrder\"\xa5\x03\n" + + "\x1cListSubstrateWorkersResponse\x12>\n" + + "\aworkers\x18\x01 \x03(\v2$.kagent.api.v1alpha1.SubstrateWorkerR\aworkers\x125\n" + + "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\x12[\n" + + "\x12applied_sort_field\x18\x04 \x01(\x0e2-.kagent.api.v1alpha1.SubstrateWorkerSortFieldR\x10appliedSortField\x12U\n" + + "\x12applied_sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\x10appliedSortOrder\x12;\n" + + "\vcomputed_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "computedAt\"\x84\x01\n" + "\x13SubstrateWorkerPool\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" + @@ -939,13 +1756,31 @@ const file_kagent_api_v1alpha1_system_proto_rawDesc = "" + "\x0eactor_template\x18\x05 \x01(\tR\ractorTemplate\x12\x19\n" + "\bactor_id\x18\x06 \x01(\tR\aactorId\x12\x0e\n" + "\x02ip\x18\a \x01(\tR\x02ip\x12\x18\n" + - "\aversion\x18\b \x01(\x03R\aversion2\xbb\x03\n" + + "\aversion\x18\b \x01(\x03R\aversion*\x83\x01\n" + + "\x12SubstrateSortOrder\x12$\n" + + " SUBSTRATE_SORT_ORDER_UNSPECIFIED\x10\x00\x12\"\n" + + "\x1eSUBSTRATE_SORT_ORDER_ASCENDING\x10\x01\x12#\n" + + "\x1fSUBSTRATE_SORT_ORDER_DESCENDING\x10\x02*\xef\x01\n" + + "\x17SubstrateActorSortField\x12*\n" + + "&SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED\x10\x00\x12%\n" + + "!SUBSTRATE_ACTOR_SORT_FIELD_STATUS\x10\x01\x12'\n" + + "#SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID\x10\x02\x12-\n" + + ")SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE\x10\x03\x12)\n" + + "%SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD\x10\x04*\xb9\x01\n" + + "\x18SubstrateWorkerSortField\x12+\n" + + "'SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED\x10\x00\x12$\n" + + " SUBSTRATE_WORKER_SORT_FIELD_POOL\x10\x01\x12#\n" + + "\x1fSUBSTRATE_WORKER_SORT_FIELD_POD\x10\x02\x12%\n" + + "!SUBSTRATE_WORKER_SORT_FIELD_ACTOR\x10\x032\xac\x06\n" + "\rSystemService\x12]\n" + "\n" + "GetVersion\x12&.kagent.api.v1alpha1.GetVersionRequest\x1a'.kagent.api.v1alpha1.GetVersionResponse\x12i\n" + "\x0eGetCurrentUser\x12*.kagent.api.v1alpha1.GetCurrentUserRequest\x1a+.kagent.api.v1alpha1.GetCurrentUserResponse\x12i\n" + "\x0eListNamespaces\x12*.kagent.api.v1alpha1.ListNamespacesRequest\x1a+.kagent.api.v1alpha1.ListNamespacesResponse\x12u\n" + - "\x12GetSubstrateStatus\x12..kagent.api.v1alpha1.GetSubstrateStatusRequest\x1a/.kagent.api.v1alpha1.GetSubstrateStatusResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" + "\x12GetSubstrateStatus\x12..kagent.api.v1alpha1.GetSubstrateStatusRequest\x1a/.kagent.api.v1alpha1.GetSubstrateStatusResponse\x12x\n" + + "\x13GetSubstrateSummary\x12/.kagent.api.v1alpha1.GetSubstrateSummaryRequest\x1a0.kagent.api.v1alpha1.GetSubstrateSummaryResponse\x12x\n" + + "\x13ListSubstrateActors\x12/.kagent.api.v1alpha1.ListSubstrateActorsRequest\x1a0.kagent.api.v1alpha1.ListSubstrateActorsResponse\x12{\n" + + "\x14ListSubstrateWorkers\x120.kagent.api.v1alpha1.ListSubstrateWorkersRequest\x1a1.kagent.api.v1alpha1.ListSubstrateWorkersResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" var ( file_kagent_api_v1alpha1_system_proto_rawDescOnce sync.Once @@ -959,43 +1794,83 @@ func file_kagent_api_v1alpha1_system_proto_rawDescGZIP() []byte { return file_kagent_api_v1alpha1_system_proto_rawDescData } -var file_kagent_api_v1alpha1_system_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_kagent_api_v1alpha1_system_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_kagent_api_v1alpha1_system_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_kagent_api_v1alpha1_system_proto_goTypes = []any{ - (*GetVersionRequest)(nil), // 0: kagent.api.v1alpha1.GetVersionRequest - (*GetVersionResponse)(nil), // 1: kagent.api.v1alpha1.GetVersionResponse - (*GetCurrentUserRequest)(nil), // 2: kagent.api.v1alpha1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 3: kagent.api.v1alpha1.GetCurrentUserResponse - (*ListNamespacesRequest)(nil), // 4: kagent.api.v1alpha1.ListNamespacesRequest - (*Namespace)(nil), // 5: kagent.api.v1alpha1.Namespace - (*ListNamespacesResponse)(nil), // 6: kagent.api.v1alpha1.ListNamespacesResponse - (*GetSubstrateStatusRequest)(nil), // 7: kagent.api.v1alpha1.GetSubstrateStatusRequest - (*GetSubstrateStatusResponse)(nil), // 8: kagent.api.v1alpha1.GetSubstrateStatusResponse - (*SubstrateWorkerPool)(nil), // 9: kagent.api.v1alpha1.SubstrateWorkerPool - (*SubstrateActorTemplate)(nil), // 10: kagent.api.v1alpha1.SubstrateActorTemplate - (*SubstrateActor)(nil), // 11: kagent.api.v1alpha1.SubstrateActor - (*SubstrateWorker)(nil), // 12: kagent.api.v1alpha1.SubstrateWorker - (*structpb.Struct)(nil), // 13: google.protobuf.Struct + (SubstrateSortOrder)(0), // 0: kagent.api.v1alpha1.SubstrateSortOrder + (SubstrateActorSortField)(0), // 1: kagent.api.v1alpha1.SubstrateActorSortField + (SubstrateWorkerSortField)(0), // 2: kagent.api.v1alpha1.SubstrateWorkerSortField + (*GetVersionRequest)(nil), // 3: kagent.api.v1alpha1.GetVersionRequest + (*GetVersionResponse)(nil), // 4: kagent.api.v1alpha1.GetVersionResponse + (*GetCurrentUserRequest)(nil), // 5: kagent.api.v1alpha1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 6: kagent.api.v1alpha1.GetCurrentUserResponse + (*ListNamespacesRequest)(nil), // 7: kagent.api.v1alpha1.ListNamespacesRequest + (*Namespace)(nil), // 8: kagent.api.v1alpha1.Namespace + (*ListNamespacesResponse)(nil), // 9: kagent.api.v1alpha1.ListNamespacesResponse + (*GetSubstrateStatusRequest)(nil), // 10: kagent.api.v1alpha1.GetSubstrateStatusRequest + (*GetSubstrateStatusResponse)(nil), // 11: kagent.api.v1alpha1.GetSubstrateStatusResponse + (*GetSubstrateSummaryRequest)(nil), // 12: kagent.api.v1alpha1.GetSubstrateSummaryRequest + (*SubstrateStatusCount)(nil), // 13: kagent.api.v1alpha1.SubstrateStatusCount + (*GetSubstrateSummaryResponse)(nil), // 14: kagent.api.v1alpha1.GetSubstrateSummaryResponse + (*ListSubstrateActorsRequest)(nil), // 15: kagent.api.v1alpha1.ListSubstrateActorsRequest + (*ListSubstrateActorsResponse)(nil), // 16: kagent.api.v1alpha1.ListSubstrateActorsResponse + (*ListSubstrateWorkersRequest)(nil), // 17: kagent.api.v1alpha1.ListSubstrateWorkersRequest + (*ListSubstrateWorkersResponse)(nil), // 18: kagent.api.v1alpha1.ListSubstrateWorkersResponse + (*SubstrateWorkerPool)(nil), // 19: kagent.api.v1alpha1.SubstrateWorkerPool + (*SubstrateActorTemplate)(nil), // 20: kagent.api.v1alpha1.SubstrateActorTemplate + (*SubstrateActor)(nil), // 21: kagent.api.v1alpha1.SubstrateActor + (*SubstrateWorker)(nil), // 22: kagent.api.v1alpha1.SubstrateWorker + (*structpb.Struct)(nil), // 23: google.protobuf.Struct + (*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp + (*PageRequest)(nil), // 25: kagent.api.v1alpha1.PageRequest + (*PageResponse)(nil), // 26: kagent.api.v1alpha1.PageResponse } var file_kagent_api_v1alpha1_system_proto_depIdxs = []int32{ - 13, // 0: kagent.api.v1alpha1.GetCurrentUserResponse.claims:type_name -> google.protobuf.Struct - 5, // 1: kagent.api.v1alpha1.ListNamespacesResponse.namespaces:type_name -> kagent.api.v1alpha1.Namespace - 9, // 2: kagent.api.v1alpha1.GetSubstrateStatusResponse.worker_pools:type_name -> kagent.api.v1alpha1.SubstrateWorkerPool - 10, // 3: kagent.api.v1alpha1.GetSubstrateStatusResponse.actor_templates:type_name -> kagent.api.v1alpha1.SubstrateActorTemplate - 11, // 4: kagent.api.v1alpha1.GetSubstrateStatusResponse.actors:type_name -> kagent.api.v1alpha1.SubstrateActor - 12, // 5: kagent.api.v1alpha1.GetSubstrateStatusResponse.workers:type_name -> kagent.api.v1alpha1.SubstrateWorker - 0, // 6: kagent.api.v1alpha1.SystemService.GetVersion:input_type -> kagent.api.v1alpha1.GetVersionRequest - 2, // 7: kagent.api.v1alpha1.SystemService.GetCurrentUser:input_type -> kagent.api.v1alpha1.GetCurrentUserRequest - 4, // 8: kagent.api.v1alpha1.SystemService.ListNamespaces:input_type -> kagent.api.v1alpha1.ListNamespacesRequest - 7, // 9: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:input_type -> kagent.api.v1alpha1.GetSubstrateStatusRequest - 1, // 10: kagent.api.v1alpha1.SystemService.GetVersion:output_type -> kagent.api.v1alpha1.GetVersionResponse - 3, // 11: kagent.api.v1alpha1.SystemService.GetCurrentUser:output_type -> kagent.api.v1alpha1.GetCurrentUserResponse - 6, // 12: kagent.api.v1alpha1.SystemService.ListNamespaces:output_type -> kagent.api.v1alpha1.ListNamespacesResponse - 8, // 13: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:output_type -> kagent.api.v1alpha1.GetSubstrateStatusResponse - 10, // [10:14] is the sub-list for method output_type - 6, // [6:10] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 23, // 0: kagent.api.v1alpha1.GetCurrentUserResponse.claims:type_name -> google.protobuf.Struct + 8, // 1: kagent.api.v1alpha1.ListNamespacesResponse.namespaces:type_name -> kagent.api.v1alpha1.Namespace + 19, // 2: kagent.api.v1alpha1.GetSubstrateStatusResponse.worker_pools:type_name -> kagent.api.v1alpha1.SubstrateWorkerPool + 20, // 3: kagent.api.v1alpha1.GetSubstrateStatusResponse.actor_templates:type_name -> kagent.api.v1alpha1.SubstrateActorTemplate + 21, // 4: kagent.api.v1alpha1.GetSubstrateStatusResponse.actors:type_name -> kagent.api.v1alpha1.SubstrateActor + 22, // 5: kagent.api.v1alpha1.GetSubstrateStatusResponse.workers:type_name -> kagent.api.v1alpha1.SubstrateWorker + 19, // 6: kagent.api.v1alpha1.GetSubstrateSummaryResponse.worker_pools:type_name -> kagent.api.v1alpha1.SubstrateWorkerPool + 20, // 7: kagent.api.v1alpha1.GetSubstrateSummaryResponse.actor_templates:type_name -> kagent.api.v1alpha1.SubstrateActorTemplate + 13, // 8: kagent.api.v1alpha1.GetSubstrateSummaryResponse.actor_status_counts:type_name -> kagent.api.v1alpha1.SubstrateStatusCount + 24, // 9: kagent.api.v1alpha1.GetSubstrateSummaryResponse.computed_at:type_name -> google.protobuf.Timestamp + 25, // 10: kagent.api.v1alpha1.ListSubstrateActorsRequest.page:type_name -> kagent.api.v1alpha1.PageRequest + 1, // 11: kagent.api.v1alpha1.ListSubstrateActorsRequest.sort_field:type_name -> kagent.api.v1alpha1.SubstrateActorSortField + 0, // 12: kagent.api.v1alpha1.ListSubstrateActorsRequest.sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder + 21, // 13: kagent.api.v1alpha1.ListSubstrateActorsResponse.actors:type_name -> kagent.api.v1alpha1.SubstrateActor + 26, // 14: kagent.api.v1alpha1.ListSubstrateActorsResponse.page:type_name -> kagent.api.v1alpha1.PageResponse + 1, // 15: kagent.api.v1alpha1.ListSubstrateActorsResponse.applied_sort_field:type_name -> kagent.api.v1alpha1.SubstrateActorSortField + 0, // 16: kagent.api.v1alpha1.ListSubstrateActorsResponse.applied_sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder + 24, // 17: kagent.api.v1alpha1.ListSubstrateActorsResponse.computed_at:type_name -> google.protobuf.Timestamp + 25, // 18: kagent.api.v1alpha1.ListSubstrateWorkersRequest.page:type_name -> kagent.api.v1alpha1.PageRequest + 2, // 19: kagent.api.v1alpha1.ListSubstrateWorkersRequest.sort_field:type_name -> kagent.api.v1alpha1.SubstrateWorkerSortField + 0, // 20: kagent.api.v1alpha1.ListSubstrateWorkersRequest.sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder + 22, // 21: kagent.api.v1alpha1.ListSubstrateWorkersResponse.workers:type_name -> kagent.api.v1alpha1.SubstrateWorker + 26, // 22: kagent.api.v1alpha1.ListSubstrateWorkersResponse.page:type_name -> kagent.api.v1alpha1.PageResponse + 2, // 23: kagent.api.v1alpha1.ListSubstrateWorkersResponse.applied_sort_field:type_name -> kagent.api.v1alpha1.SubstrateWorkerSortField + 0, // 24: kagent.api.v1alpha1.ListSubstrateWorkersResponse.applied_sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder + 24, // 25: kagent.api.v1alpha1.ListSubstrateWorkersResponse.computed_at:type_name -> google.protobuf.Timestamp + 3, // 26: kagent.api.v1alpha1.SystemService.GetVersion:input_type -> kagent.api.v1alpha1.GetVersionRequest + 5, // 27: kagent.api.v1alpha1.SystemService.GetCurrentUser:input_type -> kagent.api.v1alpha1.GetCurrentUserRequest + 7, // 28: kagent.api.v1alpha1.SystemService.ListNamespaces:input_type -> kagent.api.v1alpha1.ListNamespacesRequest + 10, // 29: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:input_type -> kagent.api.v1alpha1.GetSubstrateStatusRequest + 12, // 30: kagent.api.v1alpha1.SystemService.GetSubstrateSummary:input_type -> kagent.api.v1alpha1.GetSubstrateSummaryRequest + 15, // 31: kagent.api.v1alpha1.SystemService.ListSubstrateActors:input_type -> kagent.api.v1alpha1.ListSubstrateActorsRequest + 17, // 32: kagent.api.v1alpha1.SystemService.ListSubstrateWorkers:input_type -> kagent.api.v1alpha1.ListSubstrateWorkersRequest + 4, // 33: kagent.api.v1alpha1.SystemService.GetVersion:output_type -> kagent.api.v1alpha1.GetVersionResponse + 6, // 34: kagent.api.v1alpha1.SystemService.GetCurrentUser:output_type -> kagent.api.v1alpha1.GetCurrentUserResponse + 9, // 35: kagent.api.v1alpha1.SystemService.ListNamespaces:output_type -> kagent.api.v1alpha1.ListNamespacesResponse + 11, // 36: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:output_type -> kagent.api.v1alpha1.GetSubstrateStatusResponse + 14, // 37: kagent.api.v1alpha1.SystemService.GetSubstrateSummary:output_type -> kagent.api.v1alpha1.GetSubstrateSummaryResponse + 16, // 38: kagent.api.v1alpha1.SystemService.ListSubstrateActors:output_type -> kagent.api.v1alpha1.ListSubstrateActorsResponse + 18, // 39: kagent.api.v1alpha1.SystemService.ListSubstrateWorkers:output_type -> kagent.api.v1alpha1.ListSubstrateWorkersResponse + 33, // [33:40] is the sub-list for method output_type + 26, // [26:33] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name } func init() { file_kagent_api_v1alpha1_system_proto_init() } @@ -1003,18 +1878,20 @@ func file_kagent_api_v1alpha1_system_proto_init() { if File_kagent_api_v1alpha1_system_proto != nil { return } + file_kagent_api_v1alpha1_common_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_system_proto_rawDesc), len(file_kagent_api_v1alpha1_system_proto_rawDesc)), - NumEnums: 0, - NumMessages: 13, + NumEnums: 3, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, GoTypes: file_kagent_api_v1alpha1_system_proto_goTypes, DependencyIndexes: file_kagent_api_v1alpha1_system_proto_depIdxs, + EnumInfos: file_kagent_api_v1alpha1_system_proto_enumTypes, MessageInfos: file_kagent_api_v1alpha1_system_proto_msgTypes, }.Build() File_kagent_api_v1alpha1_system_proto = out.File diff --git a/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go index 53c072b16..cddc38a96 100644 --- a/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go @@ -19,10 +19,13 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - SystemService_GetVersion_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetVersion" - SystemService_GetCurrentUser_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetCurrentUser" - SystemService_ListNamespaces_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListNamespaces" - SystemService_GetSubstrateStatus_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetSubstrateStatus" + SystemService_GetVersion_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetVersion" + SystemService_GetCurrentUser_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetCurrentUser" + SystemService_ListNamespaces_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListNamespaces" + SystemService_GetSubstrateStatus_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetSubstrateStatus" + SystemService_GetSubstrateSummary_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetSubstrateSummary" + SystemService_ListSubstrateActors_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListSubstrateActors" + SystemService_ListSubstrateWorkers_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListSubstrateWorkers" ) // SystemServiceClient is the client API for SystemService service. @@ -32,7 +35,38 @@ type SystemServiceClient interface { GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) + // GetSubstrateStatus returns the entire inventory in one message: every + // worker pool, actor template, actor and worker, unpaginated and unfiltered. + // + // It does not survive a real cluster. A deployment reporting 103,134 actors + // answers with a message the gRPC client refuses outright — "trying to send + // message larger than max (43016460 vs. 16777216)" — so the caller gets no + // inventory at all rather than a large one. Raising the ceiling moves the + // number without changing the shape. + // + // Prefer GetSubstrateSummary with ListSubstrateActors and + // ListSubstrateWorkers, which bound what any single response can carry. This + // RPC is kept for callers that predate them and for the small clusters where + // it still works. GetSubstrateStatus(ctx context.Context, in *GetSubstrateStatusRequest, opts ...grpc.CallOption) (*GetSubstrateStatusResponse, error) + // GetSubstrateSummary returns counts computed server-side, plus the two lists + // that are inherently small. + // + // This is the only honest source of a total. A caller that counts a page and + // presents the result as a total reports "3 actors" for a cluster running a + // hundred thousand, which is the specific failure the paged RPCs below would + // otherwise introduce. + GetSubstrateSummary(ctx context.Context, in *GetSubstrateSummaryRequest, opts ...grpc.CallOption) (*GetSubstrateSummaryResponse, error) + // ListSubstrateActors pages the actors, narrowing them server-side. + // + // Paged because this is one of the two lists whose length is set by the + // cluster rather than by configuration, and filtered server-side for the same + // reason: narrowing a page that has already been fetched searches only what + // was fetched, so a match on page nine reads on screen as "no matches". + ListSubstrateActors(ctx context.Context, in *ListSubstrateActorsRequest, opts ...grpc.CallOption) (*ListSubstrateActorsResponse, error) + // ListSubstrateWorkers pages the worker assignments. The mirror of + // ListSubstrateActors. + ListSubstrateWorkers(ctx context.Context, in *ListSubstrateWorkersRequest, opts ...grpc.CallOption) (*ListSubstrateWorkersResponse, error) } type systemServiceClient struct { @@ -83,6 +117,36 @@ func (c *systemServiceClient) GetSubstrateStatus(ctx context.Context, in *GetSub return out, nil } +func (c *systemServiceClient) GetSubstrateSummary(ctx context.Context, in *GetSubstrateSummaryRequest, opts ...grpc.CallOption) (*GetSubstrateSummaryResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSubstrateSummaryResponse) + err := c.cc.Invoke(ctx, SystemService_GetSubstrateSummary_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) ListSubstrateActors(ctx context.Context, in *ListSubstrateActorsRequest, opts ...grpc.CallOption) (*ListSubstrateActorsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSubstrateActorsResponse) + err := c.cc.Invoke(ctx, SystemService_ListSubstrateActors_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) ListSubstrateWorkers(ctx context.Context, in *ListSubstrateWorkersRequest, opts ...grpc.CallOption) (*ListSubstrateWorkersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSubstrateWorkersResponse) + err := c.cc.Invoke(ctx, SystemService_ListSubstrateWorkers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // SystemServiceServer is the server API for SystemService service. // All implementations must embed UnimplementedSystemServiceServer // for forward compatibility. @@ -90,7 +154,38 @@ type SystemServiceServer interface { GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) + // GetSubstrateStatus returns the entire inventory in one message: every + // worker pool, actor template, actor and worker, unpaginated and unfiltered. + // + // It does not survive a real cluster. A deployment reporting 103,134 actors + // answers with a message the gRPC client refuses outright — "trying to send + // message larger than max (43016460 vs. 16777216)" — so the caller gets no + // inventory at all rather than a large one. Raising the ceiling moves the + // number without changing the shape. + // + // Prefer GetSubstrateSummary with ListSubstrateActors and + // ListSubstrateWorkers, which bound what any single response can carry. This + // RPC is kept for callers that predate them and for the small clusters where + // it still works. GetSubstrateStatus(context.Context, *GetSubstrateStatusRequest) (*GetSubstrateStatusResponse, error) + // GetSubstrateSummary returns counts computed server-side, plus the two lists + // that are inherently small. + // + // This is the only honest source of a total. A caller that counts a page and + // presents the result as a total reports "3 actors" for a cluster running a + // hundred thousand, which is the specific failure the paged RPCs below would + // otherwise introduce. + GetSubstrateSummary(context.Context, *GetSubstrateSummaryRequest) (*GetSubstrateSummaryResponse, error) + // ListSubstrateActors pages the actors, narrowing them server-side. + // + // Paged because this is one of the two lists whose length is set by the + // cluster rather than by configuration, and filtered server-side for the same + // reason: narrowing a page that has already been fetched searches only what + // was fetched, so a match on page nine reads on screen as "no matches". + ListSubstrateActors(context.Context, *ListSubstrateActorsRequest) (*ListSubstrateActorsResponse, error) + // ListSubstrateWorkers pages the worker assignments. The mirror of + // ListSubstrateActors. + ListSubstrateWorkers(context.Context, *ListSubstrateWorkersRequest) (*ListSubstrateWorkersResponse, error) mustEmbedUnimplementedSystemServiceServer() } @@ -113,6 +208,15 @@ func (UnimplementedSystemServiceServer) ListNamespaces(context.Context, *ListNam func (UnimplementedSystemServiceServer) GetSubstrateStatus(context.Context, *GetSubstrateStatusRequest) (*GetSubstrateStatusResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSubstrateStatus not implemented") } +func (UnimplementedSystemServiceServer) GetSubstrateSummary(context.Context, *GetSubstrateSummaryRequest) (*GetSubstrateSummaryResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSubstrateSummary not implemented") +} +func (UnimplementedSystemServiceServer) ListSubstrateActors(context.Context, *ListSubstrateActorsRequest) (*ListSubstrateActorsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSubstrateActors not implemented") +} +func (UnimplementedSystemServiceServer) ListSubstrateWorkers(context.Context, *ListSubstrateWorkersRequest) (*ListSubstrateWorkersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSubstrateWorkers not implemented") +} func (UnimplementedSystemServiceServer) mustEmbedUnimplementedSystemServiceServer() {} func (UnimplementedSystemServiceServer) testEmbeddedByValue() {} @@ -206,6 +310,60 @@ func _SystemService_GetSubstrateStatus_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } +func _SystemService_GetSubstrateSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSubstrateSummaryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetSubstrateSummary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetSubstrateSummary_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetSubstrateSummary(ctx, req.(*GetSubstrateSummaryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_ListSubstrateActors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSubstrateActorsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).ListSubstrateActors(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_ListSubstrateActors_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).ListSubstrateActors(ctx, req.(*ListSubstrateActorsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_ListSubstrateWorkers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSubstrateWorkersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).ListSubstrateWorkers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_ListSubstrateWorkers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).ListSubstrateWorkers(ctx, req.(*ListSubstrateWorkersRequest)) + } + return interceptor(ctx, in, info, handler) +} + // SystemService_ServiceDesc is the grpc.ServiceDesc for SystemService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -229,6 +387,18 @@ var SystemService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetSubstrateStatus", Handler: _SystemService_GetSubstrateStatus_Handler, }, + { + MethodName: "GetSubstrateSummary", + Handler: _SystemService_GetSubstrateSummary_Handler, + }, + { + MethodName: "ListSubstrateActors", + Handler: _SystemService_ListSubstrateActors_Handler, + }, + { + MethodName: "ListSubstrateWorkers", + Handler: _SystemService_ListSubstrateWorkers_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "kagent/api/v1alpha1/system.proto", diff --git a/go/api/v1alpha3/harness_types.go b/go/api/v1alpha3/harness_types.go index 3a0380213..9652ca4fa 100644 --- a/go/api/v1alpha3/harness_types.go +++ b/go/api/v1alpha3/harness_types.go @@ -161,6 +161,11 @@ type HarnessCapabilities struct { Checkpoint bool `json:"checkpoint"` } +// HarnessConditionType enumerates the condition types a Harness may report. +const ( + HarnessConditionTypeReady = "Ready" +) + // HarnessStatus reports controller-derived capabilities and current health. type HarnessStatus struct { // ObservedGeneration is the latest Harness generation observed by the controller. diff --git a/go/core/cmd/controller-v2/main.go b/go/core/cmd/controller-v2/main.go index dbd036f4b..2a954d9c1 100644 --- a/go/core/cmd/controller-v2/main.go +++ b/go/core/cmd/controller-v2/main.go @@ -28,9 +28,12 @@ import ( "syscall" "time" + kagentv1alpha3 "github.com/kagent-dev/kagent/go/api/v1alpha3" "github.com/kagent-dev/kagent/go/core/internal/database" "github.com/kagent-dev/kagent/go/core/internal/grpcserver" authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" + agenttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/agenttemplate" + harnessservice "github.com/kagent-dev/kagent/go/core/internal/service/harness" sessionservice "github.com/kagent-dev/kagent/go/core/internal/service/session" taskservice "github.com/kagent-dev/kagent/go/core/internal/service/task" "github.com/kagent-dev/kagent/go/core/pkg/migrations" @@ -40,6 +43,9 @@ import ( "github.com/kagent-dev/kagent/go/core/v2/checkpoint" v2controller "github.com/kagent-dev/kagent/go/core/v2/controller" "golang.org/x/sync/errgroup" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/clientcmd" ctrl "sigs.k8s.io/controller-runtime" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -69,7 +75,15 @@ func main() { if err != nil { log.Fatalf("load Kubernetes config: %v", err) } + // The manager's client is what serves the Harness and AgentTemplate RPCs, so + // it needs v1alpha3 in its scheme; the controller-runtime default carries + // only the built-in kinds and would fail every one of those calls at runtime + // rather than at startup. + managerScheme := k8sruntime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(managerScheme)) + utilruntime.Must(kagentv1alpha3.AddToScheme(managerScheme)) manager, err := ctrl.NewManager(kubeConfig, ctrl.Options{ + Scheme: managerScheme, Metrics: metricsserver.Options{BindAddress: "0"}, LeaderElection: envBool("LEADER_ELECT"), LeaderElectionID: "0e9f6799.kagent.dev", @@ -121,7 +135,13 @@ func main() { SessionService: sessionservice.NewService(store), TaskService: taskservice.NewService(store), AgentInstanceService: instances, + // Both halves of the pair CreateAgentInstance names. Without these two + // the only way to author a Harness or an AgentTemplate is kubectl. + AgentTemplateService: agenttemplateservice.NewService(manager.GetClient(), authorizer), + HarnessService: harnessservice.NewService(manager.GetClient(), authorizer), CheckpointService: checkpoints, + // `instanceWorkflow` is what upstream added: the gateway needs it to suspend an + // instance once a turn reaches a quiescent boundary. A2AHandler: a2agateway.New(store, authorizer, gatewayDialer, instanceWorkflow, env("A2A_GATEWAY_URL", "http://127.0.0.1:8084")), }) @@ -129,9 +149,20 @@ func main() { log.Fatal(err) } - health := &http.Server{Addr: ":8083", Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + // The HTTP port serves health *and* gRPC-Web, because a browser cannot speak + // gRPC and this is the only port a page can reach: the chart's nginx proxies + // /api here, while :8084 speaks native gRPC that `fetch` has no way to talk to. + // + // Worth stating because the previous shape of this looked correct and was not. + // It answered every path with an empty 200 and ignored the request entirely, so + // a browser calling an RPC got a success with no body — which reads as a + // serialisation fault in the client rather than as a server that never had the + // endpoint. The router below hands anything that is not gRPC-Web to the same + // health response as before. + httpHandler := server.WebHandlerOr(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) - })} + })) + health := &http.Server{Addr: env("HTTP_BIND_ADDRESS", ":8083"), Handler: httpHandler} group, ctx := errgroup.WithContext(ctx) group.Go(func() error { return runtime.Start(ctx) }) group.Go(func() error { return manager.Start(ctx) }) diff --git a/go/core/internal/database/client_agent_instance_test.go b/go/core/internal/database/client_agent_instance_test.go index dc649f18a..22faf5ede 100644 --- a/go/core/internal/database/client_agent_instance_test.go +++ b/go/core/internal/database/client_agent_instance_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "strings" "testing" "time" @@ -25,7 +26,7 @@ func TestToAgentInstanceUsesIndexedLifecycleColumns(t *testing.T) { } instance, err := toAgentInstance(dbgen.AgentInstance{ - ID: "instance-1", Data: data, State: "SUSPENDED", Operation: "RESUME", + ID: "instance-1", Data: data, State: "SUSPENDED", Operation: "RESUME", Name: "Renamed later", }) if err != nil { t.Fatal(err) @@ -34,6 +35,31 @@ func TestToAgentInstanceUsesIndexedLifecycleColumns(t *testing.T) { instance.GetOperation() != apiv1alpha1.AgentInstanceOperation_AGENT_INSTANCE_OPERATION_RESUME { t.Fatalf("lifecycle = %s/%s, want SUSPENDED/RESUME", instance.GetState(), instance.GetOperation()) } + // The name has to come from the column too: a rename writes only the column, + // so reading it from the blob would serve the original name forever. + if instance.GetName() != "Renamed later" { + t.Fatalf("name = %q, want the column's value", instance.GetName()) + } +} + +// TestToAgentInstanceLeavesAnEmptyNameEmpty pins the additive property: a row +// written before the column existed reads as unnamed, not as its id and not as +// some placeholder. +func TestToAgentInstanceLeavesAnEmptyNameEmpty(t *testing.T) { + data, err := proto.Marshal(&apiv1alpha1.AgentInstance{ + Id: "instance-1", + State: apiv1alpha1.AgentInstanceState_AGENT_INSTANCE_STATE_READY, + }) + if err != nil { + t.Fatal(err) + } + instance, err := toAgentInstance(dbgen.AgentInstance{ID: "instance-1", Data: data, State: "READY", Operation: "NONE"}) + if err != nil { + t.Fatal(err) + } + if instance.GetName() != "" { + t.Fatalf("name = %q, want empty", instance.GetName()) + } } func TestAgentInstanceTasksAreDurableAndExclusive(t *testing.T) { @@ -367,7 +393,9 @@ func TestForkAgentInstanceCopiesBoundedHistory(t *testing.T) { fork.GetLabels()["app"] != "assistant" { t.Fatalf("fork = %+v", fork) } - instances, err := client.ListAgentInstances(ctx, "team-a", "alice", false, nil, "", 10) + instances, err := client.ListAgentInstances(ctx, dbpkg.AgentInstanceQuery{ + Namespace: "team-a", UserID: "alice", Limit: 10, + }) if err != nil || len(instances) != 1 || instances[0].GetId() != fork.GetId() { t.Fatalf("listed forks = %+v, error %v", instances, err) } @@ -479,7 +507,9 @@ func TestAgentInstanceCreateAndTransitions(t *testing.T) { if len(replayed.GetLabels()) != 0 { t.Fatalf("labels = %v", replayed.GetLabels()) } - instances, err := client.ListAgentInstances(ctx, "team-a", "alice", false, nil, "", 10) + instances, err := client.ListAgentInstances(ctx, dbpkg.AgentInstanceQuery{ + Namespace: "team-a", UserID: "alice", Limit: 10, + }) if err != nil || len(instances) != 1 { t.Fatalf("ListAgentInstances() = %v, error %v", instances, err) } @@ -587,3 +617,347 @@ func TestInterruptActiveAgentInstanceTaskRequiresMatchingTaskAndReusesSlot(t *te t.Fatalf("events recorded for the interrupted task = %d, want the send and the interruption", events) } } + +// agentInstanceFixture installs a runnable agent — a template/harness pair with a +// successful revision — so instances can be created against it. +func agentInstanceFixture(t *testing.T, client dbpkg.Client, ctx context.Context, revisionID, template, harness string) { + t.Helper() + revision := dbpkg.RuntimeRevision{ + Revision: revisionID, Namespace: "team-a", + AgentTemplateName: template, AgentTemplateUID: template + "-uid", + HarnessName: harness, HarnessUID: harness + "-uid", + SourceSnapshot: []byte("{}"), AgentCard: []byte("{}"), EgressDestinations: []string{}, + ActorTemplateNamespace: "team-a", ActorTemplateName: revisionID + "-actor-template", + ActorTemplateUID: revisionID + "-actor-uid", Phase: "Ready", + } + if err := client.UpsertRuntimeRevision(ctx, revision); err != nil { + t.Fatal(err) + } + pair := dbpkg.AgentTemplateHarnessPair{ + Namespace: "team-a", AgentTemplateName: template, AgentTemplateUID: template + "-uid", + HarnessName: harness, HarnessUID: harness + "-uid", DesiredRevision: revisionID, + } + if err := client.UpsertAgentTemplateHarnessPair(ctx, pair); err != nil { + t.Fatal(err) + } + if err := client.MarkRuntimeRevisionSuccessful(ctx, pair); err != nil { + t.Fatal(err) + } +} + +func newAgentInstanceRequest(id, template, harness, name string) *apiv1alpha1.AgentInstance { + return &apiv1alpha1.AgentInstance{ + Id: id, Namespace: "team-a", Creator: "alice", Name: name, + Harness: &apiv1alpha1.ResourceReference{Namespace: "team-a", Name: harness}, + AgentTemplate: &apiv1alpha1.ResourceReference{Namespace: "team-a", Name: template}, + } +} + +func TestAgentInstanceNameRoundTripsAndRenames(t *testing.T) { + client := NewClient(setupTestDB(t)) + ctx := context.Background() + agentInstanceFixture(t, client, ctx, "revision-1", "assistant", "kagent") + + for _, test := range []struct { + name string + id string + given string + wantName string + }{ + {name: "a name round-trips", id: "instance-named", given: "Debugging the ingress", wantName: "Debugging the ingress"}, + // An instance created without a name must read back empty, which is how + // every row written before the column existed reads. + {name: "an omitted name stays empty", id: "instance-unnamed", given: "", wantName: ""}, + } { + t.Run(test.name, func(t *testing.T) { + created, wasCreated, err := client.CreateAgentInstance(ctx, newAgentInstanceRequest(test.id, "assistant", "kagent", test.given), test.id) + if err != nil || !wasCreated { + t.Fatalf("CreateAgentInstance() = created %v, error %v", wasCreated, err) + } + if created.GetName() != test.wantName { + t.Fatalf("created name = %q, want %q", created.GetName(), test.wantName) + } + read, err := client.GetAgentInstance(ctx, "team-a", test.id, "alice") + if err != nil || read.GetName() != test.wantName { + t.Fatalf("re-read name = %q (%v), want %q", read.GetName(), err, test.wantName) + } + }) + } + + renamed, err := client.RenameAgentInstance(ctx, "team-a", "instance-unnamed", "alice", "Named afterwards") + if err != nil || renamed.GetName() != "Named afterwards" { + t.Fatalf("RenameAgentInstance() = %+v, error %v", renamed, err) + } + // The rename has to survive a re-read, not just be echoed back: the name lives + // in a column while the rest of the message lives in a blob the rename does not + // rewrite, so an echoed value proves nothing about what was stored. + read, err := client.GetAgentInstance(ctx, "team-a", "instance-unnamed", "alice") + if err != nil || read.GetName() != "Named afterwards" { + t.Fatalf("re-read after rename = %+v, error %v", read, err) + } + // Renaming back to empty must be possible, or a name can never be undone. + cleared, err := client.RenameAgentInstance(ctx, "team-a", "instance-unnamed", "alice", "") + if err != nil || cleared.GetName() != "" { + t.Fatalf("RenameAgentInstance(\"\") = %+v, error %v", cleared, err) + } + // A rename is scoped to the owner, so it cannot reach another reader's row. + if _, err := client.RenameAgentInstance(ctx, "team-a", "instance-named", "bob", "Stolen"); !errors.Is(err, dbpkg.ErrNotFound) { + t.Fatalf("RenameAgentInstance() as another user error = %v, want %v", err, dbpkg.ErrNotFound) + } + if _, err := client.RenameAgentInstance(ctx, "team-a", "missing", "alice", "Nothing"); !errors.Is(err, dbpkg.ErrNotFound) { + t.Fatalf("RenameAgentInstance() of a missing instance error = %v, want %v", err, dbpkg.ErrNotFound) + } +} + +// TestListAgentInstancesFiltersByAgentPair covers the server-side filter behind +// "this agent's conversations". The pair is resolved through the instance's +// prepared revision rather than its labels, because the labels an instance +// carries are the *template's* own Kubernetes labels and are identical for two +// harnesses admitting one template. +func TestListAgentInstancesFiltersByAgentPair(t *testing.T) { + client := NewClient(setupTestDB(t)) + ctx := context.Background() + agentInstanceFixture(t, client, ctx, "revision-1", "assistant", "kagent") + agentInstanceFixture(t, client, ctx, "revision-2", "assistant", "claude") + agentInstanceFixture(t, client, ctx, "revision-3", "researcher", "kagent") + + for id, pair := range map[string][2]string{ + "instance-1": {"assistant", "kagent"}, + "instance-2": {"assistant", "claude"}, + "instance-3": {"researcher", "kagent"}, + } { + if _, _, err := client.CreateAgentInstance(ctx, newAgentInstanceRequest(id, pair[0], pair[1], ""), id); err != nil { + t.Fatalf("CreateAgentInstance(%s) error %v", id, err) + } + } + + for _, test := range []struct { + name string + query dbpkg.AgentInstanceQuery + want []string + }{ + { + name: "no filter lists every conversation", + query: dbpkg.AgentInstanceQuery{}, + want: []string{"instance-1", "instance-2", "instance-3"}, + }, + { + name: "one agent, which is one pair", + query: dbpkg.AgentInstanceQuery{AgentTemplate: "assistant", Harness: "kagent"}, + want: []string{"instance-1"}, + }, + { + // The case labels could never serve: one template, two harnesses, two + // agents, and identical labels on both instances. + name: "the same template on a different harness is a different agent", + query: dbpkg.AgentInstanceQuery{AgentTemplate: "assistant", Harness: "claude"}, + want: []string{"instance-2"}, + }, + { + name: "template alone spans its harnesses", + query: dbpkg.AgentInstanceQuery{AgentTemplate: "assistant"}, + want: []string{"instance-1", "instance-2"}, + }, + { + name: "harness alone spans its templates", + query: dbpkg.AgentInstanceQuery{Harness: "kagent"}, + want: []string{"instance-1", "instance-3"}, + }, + { + name: "an unknown agent matches nothing rather than everything", + query: dbpkg.AgentInstanceQuery{AgentTemplate: "absent", Harness: "kagent"}, + want: []string{}, + }, + } { + t.Run(test.name, func(t *testing.T) { + query := test.query + query.Namespace, query.UserID, query.Limit = "team-a", "alice", 10 + instances, err := client.ListAgentInstances(ctx, query) + if err != nil { + t.Fatal(err) + } + got := make([]string, 0, len(instances)) + for _, instance := range instances { + got = append(got, instance.GetId()) + } + if strings.Join(got, ",") != strings.Join(test.want, ",") { + t.Fatalf("ListAgentInstances() = %v, want %v", got, test.want) + } + }) + } +} + +// TestAbandonActiveAgentInstanceTaskFreesTheSlotForTheNextTurn is the store +// primitive behind a reader-requested cancel. +// +// A parked turn no longer blocks the next one: the active-task query now excludes +// INPUT_REQUIRED and AUTH_REQUIRED, so an unanswered question does not wedge the +// instance the way it used to. This test asserted that wedge as a fact, which it is +// not any more. +// +// Abandoning is still the primitive it was, and still worth having. A parked task +// cannot reach a terminal state on its own — nothing clears it, because the question +// stays valid until the reader gives it up — so this is how a reader says they will +// not be answering, and how the task gets an ending rather than staying open forever. +func TestAbandonActiveAgentInstanceTaskFreesTheSlotForTheNextTurn(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + if _, err := db.Exec(ctx, ` + INSERT INTO a2a_context (id, namespace, user_id) + VALUES ('instance-1', 'team-a', 'alice'); + + INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data) + VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\x00') + `); err != nil { + t.Fatal(err) + } + client := NewClient(db) + + parked := newAgentInstanceTask("task-1", "message-1") + if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-1"), parked); err != nil { + t.Fatal(err) + } + parked.Status.State = a2a.TaskStateInputRequired + if err := client.StoreAgentInstanceTaskEvent(ctx, "instance-1", parked, parked, nil); err != nil { + t.Fatal(err) + } + if abandoned, err := client.AbandonActiveAgentInstanceTask(ctx, "instance-1", "different-task"); err != nil || abandoned { + t.Fatalf("AbandonActiveAgentInstanceTask(wrong task) = %v, %v", abandoned, err) + } + if abandoned, err := client.AbandonActiveAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || !abandoned { + t.Fatalf("AbandonActiveAgentInstanceTask() = %v, %v", abandoned, err) + } + stored, created, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-2"), newAgentInstanceTask("task-2", "message-2")) + if err != nil || !created || stored.ID != "task-2" { + t.Fatalf("send after abandoning = %#v, created %v, error %v", stored, created, err) + } + + closed, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") + if err != nil { + t.Fatal(err) + } + // Canceled, not failed: nothing went wrong with that turn, and its own message + // has to say what happened rather than borrowing the interruption wording. + if closed.Status.State != a2a.TaskStateCanceled { + t.Fatalf("abandoned task state = %s, want %s", closed.Status.State, a2a.TaskStateCanceled) + } + last := closed.History[len(closed.History)-1] + if last.Role != a2a.MessageRoleAgent || len(last.Parts) == 0 { + t.Fatalf("abandoned task's last message = %#v", last) + } + if text, ok := last.Parts[0].Content.(a2a.Text); !ok || string(text) != taskAbandonedMessage { + t.Fatalf("abandoned task's explanation = %#v, want the abandoned wording", last.Parts[0].Content) + } +} + +// TestClaimParkedAgentInstanceTaskIsTheReplayGuard pins the property the reply +// path relies on for idempotency, against real Postgres. The claim is the guard: +// it needs no extra bookkeeping because moving the task out of its parked state +// under a row lock is exactly what makes a second reply refusable. +func TestClaimParkedAgentInstanceTaskIsTheReplayGuard(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + if _, err := db.Exec(ctx, ` + INSERT INTO a2a_context (id, namespace, user_id) + VALUES ('instance-1', 'team-a', 'alice'); + + INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data) + VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\x00') + `); err != nil { + t.Fatal(err) + } + client := NewClient(db) + + task := newAgentInstanceTask("task-1", "message-1") + if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-1"), task); err != nil { + t.Fatal(err) + } + + // A turn that is working, not parked, cannot be replied to. + if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || claimed { + t.Fatalf("ClaimParkedAgentInstanceTask(working) = %v, %v", claimed, err) + } + + task.Status.State = a2a.TaskStateInputRequired + if err := client.StoreAgentInstanceTaskEvent(ctx, "instance-1", task, task, nil); err != nil { + t.Fatal(err) + } + + // A reply naming a different task is refused, so it cannot answer for another. + if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-2"); err != nil || claimed { + t.Fatalf("ClaimParkedAgentInstanceTask(other task) = %v, %v", claimed, err) + } + + parked, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1") + if err != nil || !claimed { + t.Fatalf("ClaimParkedAgentInstanceTask() = %v, %v", claimed, err) + } + // The returned task is the parked one, which is what a failed delivery restores. + if parked.Status.State != a2a.TaskStateInputRequired { + t.Fatalf("returned task state = %s, want the parked state", parked.Status.State) + } + // The stored task has moved on, which is what refuses the duplicate below. + claimedTask, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") + if err != nil || claimedTask.Status.State != a2a.TaskStateWorking { + t.Fatalf("claimed task state = %v (%v), want working", claimedTask.Status.State, err) + } + if _, again, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || again { + t.Fatalf("second ClaimParkedAgentInstanceTask() = %v, %v — a duplicate reply was not refused", again, err) + } + + // Restoring puts the question back, and it is claimable again. + if err := client.RestoreParkedAgentInstanceTask(ctx, "instance-1", parked); err != nil { + t.Fatal(err) + } + restored, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") + if err != nil || restored.Status.State != a2a.TaskStateInputRequired { + t.Fatalf("restored task state = %v (%v), want the parked state", restored.Status.State, err) + } + if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || !claimed { + t.Fatalf("restored question is not answerable: %v, %v", claimed, err) + } + + // Restoring puts the question back, and a new turn is now allowed alongside it. + // + // This asserted the opposite until the active-task query stopped counting + // INPUT_REQUIRED: a standing question used to occupy the instance's one slot, so a + // reader who never answered could not start another turn at all. Answering is still + // the way to finish *that* turn; it is no longer the only way to have any turn. + if err := client.RestoreParkedAgentInstanceTask(ctx, "instance-1", parked); err != nil { + t.Fatal(err) + } + if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-2"), newAgentInstanceTask("task-2", "message-2")); err != nil { + t.Fatalf("new turn while a question stands = %v, want it accepted", err) + } + // And the question is still there to be answered, rather than having been + // displaced by the turn that started beside it. + restored, restoreErr := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") + if restoreErr != nil || !dbpkg.TaskParkedAwaitingUser(restored.Status.State) { + t.Fatalf("restored task = %v (%v), want it still parked", restored.Status.State, restoreErr) + } +} + +// TestClaimParkedAgentInstanceTaskRefusesATaskThatIsNotThere covers a reply naming a +// task this instance does not have. +// +// It used to answer `ErrNotFound`, because it asked for the instance's active task and +// there was none. It now addresses the task by id — a parked task stopped being the +// active one — so "no such task" and "that task is not parked" are the same answer: +// nothing was claimed, and the caller refuses the reply on that alone. +func TestClaimParkedAgentInstanceTaskRefusesATaskThatIsNotThere(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + if _, err := db.Exec(ctx, ` + INSERT INTO a2a_context (id, namespace, user_id) + VALUES ('instance-1', 'team-a', 'alice'); + + INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data) + VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\x00') + `); err != nil { + t.Fatal(err) + } + client := NewClient(db) + if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || claimed { + t.Fatalf("ClaimParkedAgentInstanceTask() for a task that is not there = %v, %v", claimed, err) + } +} diff --git a/go/core/internal/database/client_postgres.go b/go/core/internal/database/client_postgres.go index 2a384873a..0c2cad789 100644 --- a/go/core/internal/database/client_postgres.go +++ b/go/core/internal/database/client_postgres.go @@ -479,6 +479,10 @@ func toAgentInstance(row dbgen.AgentInstance) (*apiv1alpha1.AgentInstance, error // migrations can backfill them without rewriting the protobuf blob. instance.State = apiv1alpha1.AgentInstanceState(state) instance.Operation = apiv1alpha1.AgentInstanceOperation(operationValue) + // The name is a column for the same reason, and because a rename writes only + // the column: reading it from the blob would serve the name the row was + // created with for the rest of the instance's life. + instance.Name = row.Name return instance, nil } @@ -542,7 +546,8 @@ func (c *postgresClient) CreateAgentInstance(ctx context.Context, request *apiv1 } row, err = q.InsertAgentInstance(ctx, dbgen.InsertAgentInstanceParams{ ID: request.GetId(), Namespace: request.GetNamespace(), UserID: request.GetCreator(), RequestID: requestID, - ContextID: request.GetId(), PreparedRevision: &revision.Revision, Labels: revision.AgentTemplateLabels, Data: data, + ContextID: request.GetId(), PreparedRevision: &revision.Revision, Labels: revision.AgentTemplateLabels, + Name: request.GetName(), Data: data, }) return err }) @@ -806,7 +811,8 @@ func (c *postgresClient) GetAgentInstance(ctx context.Context, namespace, id, us return toAgentInstance(row) } -func (c *postgresClient) ListAgentInstances(ctx context.Context, namespace, userID string, allUsers bool, matchLabels map[string]string, afterID string, limit int) ([]*apiv1alpha1.AgentInstance, error) { +func (c *postgresClient) ListAgentInstances(ctx context.Context, query dbpkg.AgentInstanceQuery) ([]*apiv1alpha1.AgentInstance, error) { + matchLabels := query.MatchLabels if matchLabels == nil { matchLabels = map[string]string{} } @@ -815,8 +821,10 @@ func (c *postgresClient) ListAgentInstances(ctx context.Context, namespace, user return nil, fmt.Errorf("marshal AgentInstance label selector: %w", err) } rows, err := c.q.ListAgentInstances(ctx, dbgen.ListAgentInstancesParams{ - Namespace: namespace, UserID: userID, AllUsers: allUsers, - AfterID: afterID, MatchLabels: labels, PageSize: int32(limit), + Namespace: query.Namespace, UserID: query.UserID, AllUsers: query.AllUsers, + AfterID: query.AfterID, MatchLabels: labels, + AgentTemplate: query.AgentTemplate, Harness: query.Harness, + PageSize: int32(query.Limit), }) if err != nil { return nil, fmt.Errorf("list AgentInstances: %w", err) @@ -832,6 +840,18 @@ func (c *postgresClient) ListAgentInstances(ctx context.Context, namespace, user return result, nil } +// RenameAgentInstance writes only the name column, scoped to the instance's +// owner so a rename cannot reach another reader's conversation. +func (c *postgresClient) RenameAgentInstance(ctx context.Context, namespace, id, userID, name string) (*apiv1alpha1.AgentInstance, error) { + row, err := c.q.RenameAgentInstance(ctx, dbgen.RenameAgentInstanceParams{ + Namespace: namespace, ID: id, UserID: userID, Name: name, + }) + if err != nil { + return nil, fmt.Errorf("rename AgentInstance %s/%s: %w", namespace, id, notFoundOr(err)) + } + return toAgentInstance(row) +} + func (c *postgresClient) MarkAgentInstanceReady(ctx context.Context, id, authority string) (*apiv1alpha1.AgentInstance, error) { row, err := c.q.GetAgentInstanceByID(ctx, id) if err != nil { @@ -933,6 +953,23 @@ func (c *postgresClient) CreateAgentInstanceShare(ctx context.Context, share dbp return &result, nil } +// GetAgentInstanceShareByTokenHash resolves a share token to its share. +// +// Takes the hash rather than the token: only the digest is stored, which is what +// keeps a database dump from being a set of working share links. +func (c *postgresClient) GetAgentInstanceShareByTokenHash(ctx context.Context, tokenHash []byte) (*dbpkg.AgentInstanceShare, error) { + row, err := c.q.GetAgentInstanceShareByTokenHash(ctx, tokenHash) + if err != nil { + return nil, fmt.Errorf("get AgentInstance share by token: %w", notFoundOr(err)) + } + return &dbpkg.AgentInstanceShare{ + ID: row.ID, Namespace: row.Namespace, InstanceID: row.InstanceID, + Creator: row.Creator, Permission: row.Permission, + TokenHash: row.TokenHash, CreatedAt: row.CreatedAt, + OwnerUserID: row.OwnerUserID, + }, nil +} + func (c *postgresClient) ListAgentInstanceShares(ctx context.Context, namespace, instanceID, creator, afterID string, limit int) ([]dbpkg.AgentInstanceShare, error) { rows, err := c.q.ListAgentInstanceShares(ctx, dbgen.ListAgentInstanceSharesParams{ Namespace: namespace, InstanceID: instanceID, UserID: creator, @@ -1023,6 +1060,12 @@ func (c *postgresClient) CreateAgentInstanceTask(ctx context.Context, instanceID // longer has an active execution for it. const taskInterruptedMessage = "The turn was interrupted before it completed, and the process running it is no longer reporting progress." +// taskAbandonedMessage explains a task closed because it was waiting on the +// reader and the reader started a new turn instead. It is deliberately not the +// interrupted wording: nothing went wrong, so saying the runtime stopped +// reporting progress would be a falsehood in the transcript. +const taskAbandonedMessage = "This turn was waiting for a reply and was closed when a new message started the next turn." + func (c *postgresClient) GetActiveAgentInstanceTask(ctx context.Context, instanceID string) (*a2a.Task, error) { row, err := c.q.GetActiveAgentInstanceTask(ctx, instanceID) if err != nil { @@ -1038,14 +1081,120 @@ func (c *postgresClient) GetActiveAgentInstanceTask(ctx context.Context, instanc // InterruptActiveAgentInstanceTask atomically fails taskID only if it is still the // instance's active task. func (c *postgresClient) InterruptActiveAgentInstanceTask(ctx context.Context, instanceID, taskID string) (bool, error) { + return c.terminateAgentInstanceTask(ctx, instanceID, taskID, a2a.TaskStateFailed, taskInterruptedMessage, true) +} + +// AbandonActiveAgentInstanceTask closes a task that was parked awaiting the +// reader, so the instance's single active-task slot is released. It is canceled +// rather than failed because nothing failed: the turn was waiting for input +// that never came. +func (c *postgresClient) AbandonActiveAgentInstanceTask(ctx context.Context, instanceID, taskID string) (bool, error) { + return c.terminateAgentInstanceTask(ctx, instanceID, taskID, a2a.TaskStateCanceled, taskAbandonedMessage, false) +} + +// ClaimParkedAgentInstanceTask moves a task that is waiting on the reader into +// TASK_STATE_WORKING so a reply can be delivered, and reports whether this call +// is the one that did it. +// +// The row lock is what makes it a replay guard: a duplicate reply serialises +// behind the first, sees a task that is no longer parked, and is refused rather +// than delivered twice. The returned task is the parked one as it stood *before* +// the claim, so a caller whose delivery fails can put the question back. +func (c *postgresClient) ClaimParkedAgentInstanceTask(ctx context.Context, instanceID, taskID string) (*a2a.Task, bool, error) { + var parked *a2a.Task + claimed := false + err := c.withTx(ctx, func(q *dbgen.Queries) error { + // By id, not "the active task". A parked turn no longer holds the instance's + // slot — an unanswered question must not stop the next turn — so asking for the + // active task finds something else, or nothing, and the reply lands nowhere. + row, err := q.LockAgentInstanceTask(ctx, dbgen.LockAgentInstanceTaskParams{ + ContextID: instanceID, ID: taskID, + }) + // Not claimed rather than an error: a reply naming a task this instance does + // not have is the same answer as one naming a task that is not parked — there + // is nothing here to reply to. Both leave `claimed` false, and the caller + // refuses the reply on that alone. + if errors.Is(err, pgx.ErrNoRows) { + return nil + } + if err != nil { + return fmt.Errorf("lock AgentInstance task: %w", err) + } + task, err := unmarshalAgentInstanceTask(row.Data) + if err != nil { + return err + } + if !dbpkg.TaskParkedAwaitingUser(task.Status.State) { + return nil + } + working := *task + now := time.Now() + working.Status = a2a.TaskStatus{State: a2a.TaskStateWorking, Timestamp: &now} + data, err := marshalAgentInstanceTask(&working) + if err != nil { + return err + } + if err := q.UpsertAgentInstanceTask(ctx, dbgen.UpsertAgentInstanceTaskParams{ + ContextID: instanceID, ID: string(working.ID), State: string(working.Status.State), + StatusTimestamp: working.Status.Timestamp, Data: data, + }); err != nil { + return fmt.Errorf("claim parked AgentInstance task %s: %w", working.ID, err) + } + parked, claimed = task, true + return nil + }) + if err != nil { + return nil, false, err + } + return parked, claimed, nil +} + +// RestoreParkedAgentInstanceTask puts a claimed task back exactly as it was, for +// a reply that never reached the runtime. The question stays answerable, which +// failing the task would not allow. +func (c *postgresClient) RestoreParkedAgentInstanceTask(ctx context.Context, instanceID string, task *a2a.Task) error { + data, err := marshalAgentInstanceTask(task) + if err != nil { + return err + } + if err := c.q.UpsertAgentInstanceTask(ctx, dbgen.UpsertAgentInstanceTaskParams{ + ContextID: instanceID, ID: string(task.ID), State: string(task.Status.State), + StatusTimestamp: task.Status.Timestamp, Data: data, + }); err != nil { + return fmt.Errorf("restore parked AgentInstance task %s: %w", task.ID, err) + } + return nil +} + +// terminateAgentInstanceTask atomically moves taskID to a terminal state. +// +// `requireActive` says which task the caller means, and the two callers mean +// different things. Interrupting is about the turn *in flight*, so it must not touch a +// task that a concurrent turn has already replaced — it asks for the active task and +// gives up unless that is the one named. Abandoning is about a turn parked awaiting an +// answer, and a parked task is no longer the active one: it stopped holding the +// instance's slot when the active-task query began excluding INPUT_REQUIRED, so asking +// for the active task would find something else, or nothing, and quietly do nothing. +func (c *postgresClient) terminateAgentInstanceTask( + ctx context.Context, instanceID, taskID string, state a2a.TaskState, reason string, + requireActive bool, +) (bool, error) { interruptedTask := false err := c.withTx(ctx, func(q *dbgen.Queries) error { - row, err := q.LockActiveAgentInstanceTask(ctx, instanceID) + var row dbgen.AgentInstanceTask + var err error + if requireActive { + row, err = q.LockActiveAgentInstanceTask(ctx, instanceID) + } else { + row, err = q.LockAgentInstanceTask(ctx, dbgen.LockAgentInstanceTaskParams{ + ContextID: instanceID, ID: taskID, + }) + } if errors.Is(err, pgx.ErrNoRows) { return nil } if err != nil { - return fmt.Errorf("lock active AgentInstance task: %w", err) + return fmt.Errorf("lock AgentInstance task: %w", err) } if row.ID != taskID { return nil @@ -1057,10 +1206,10 @@ func (c *postgresClient) InterruptActiveAgentInstanceTask(ctx context.Context, i if err := loadAgentInstanceTaskHistories(ctx, q, instanceID, []*a2a.Task{task}); err != nil { return err } - interrupted := a2a.NewMessageForTask(a2a.MessageRoleAgent, task, a2a.NewTextPart(taskInterruptedMessage)) + interrupted := a2a.NewMessageForTask(a2a.MessageRoleAgent, task, a2a.NewTextPart(reason)) now := time.Now() task.History = append(task.History, interrupted) - task.Status = a2a.TaskStatus{State: a2a.TaskStateFailed, Message: interrupted, Timestamp: &now} + task.Status = a2a.TaskStatus{State: state, Message: interrupted, Timestamp: &now} data, err := marshalAgentInstanceTask(task) if err != nil { return err diff --git a/go/core/internal/database/gen/agent_instance_tasks.sql.go b/go/core/internal/database/gen/agent_instance_tasks.sql.go index 7068230ae..9bac4cf46 100644 --- a/go/core/internal/database/gen/agent_instance_tasks.sql.go +++ b/go/core/internal/database/gen/agent_instance_tasks.sql.go @@ -363,8 +363,6 @@ WHERE context_id = $1 FOR UPDATE ` -// LockActiveAgentInstanceTask holds the instance's non-terminal task for the -// rest of the transaction so reclamation cannot overwrite concurrent progress. func (q *Queries) LockActiveAgentInstanceTask(ctx context.Context, contextID string) (AgentInstanceTask, error) { row := q.db.QueryRow(ctx, lockActiveAgentInstanceTask, contextID) var i AgentInstanceTask @@ -387,6 +385,49 @@ func (q *Queries) LockActiveAgentInstanceTask(ctx context.Context, contextID str return i, err } +const lockAgentInstanceTask = `-- name: LockAgentInstanceTask :one +SELECT context_id, id, state, status_timestamp, data, created_at, updated_at, initial_message_id, request_hash, snapshot_atespace, snapshot_name, snapshot_uid, snapshot_content_scope, history_sequence FROM agent_instance_task +WHERE context_id = $1 AND id = $2 +FOR UPDATE +` + +type LockAgentInstanceTaskParams struct { + ContextID string + ID string +} + +// LockActiveAgentInstanceTask holds the instance's non-terminal task for the +// rest of the transaction so reclamation cannot overwrite concurrent progress. +// +// One task by id, whatever state it is in. +// +// Distinct from LockActiveAgentInstanceTask, which finds whichever task currently +// holds the instance's turn — and deliberately no longer counts a parked one, since a +// question awaiting an answer must not stop the next turn starting. The parked-task +// operations still need to reach that exact task to answer it or give it up, so they +// name it instead of asking for the active one. +func (q *Queries) LockAgentInstanceTask(ctx context.Context, arg LockAgentInstanceTaskParams) (AgentInstanceTask, error) { + row := q.db.QueryRow(ctx, lockAgentInstanceTask, arg.ContextID, arg.ID) + var i AgentInstanceTask + err := row.Scan( + &i.ContextID, + &i.ID, + &i.State, + &i.StatusTimestamp, + &i.Data, + &i.CreatedAt, + &i.UpdatedAt, + &i.InitialMessageID, + &i.RequestHash, + &i.SnapshotAtespace, + &i.SnapshotName, + &i.SnapshotUid, + &i.SnapshotContentScope, + &i.HistorySequence, + ) + return i, err +} + const setAgentInstanceTaskSnapshot = `-- name: SetAgentInstanceTaskSnapshot :exec UPDATE agent_instance_task SET snapshot_atespace = $3, diff --git a/go/core/internal/database/gen/agent_instances.sql.go b/go/core/internal/database/gen/agent_instances.sql.go index d582c8f95..54d83612e 100644 --- a/go/core/internal/database/gen/agent_instances.sql.go +++ b/go/core/internal/database/gen/agent_instances.sql.go @@ -79,7 +79,7 @@ func (q *Queries) DeleteAgentInstanceShare(ctx context.Context, arg DeleteAgentI } const getAgentInstanceByID = `-- name: GetAgentInstanceByID :one -SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id FROM agent_instance WHERE id = $1 +SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name FROM agent_instance WHERE id = $1 ` func (q *Queries) GetAgentInstanceByID(ctx context.Context, id string) (AgentInstance, error) { @@ -97,12 +97,13 @@ func (q *Queries) GetAgentInstanceByID(ctx context.Context, id string) (AgentIns &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ) return i, err } const getAgentInstanceByRequest = `-- name: GetAgentInstanceByRequest :one -SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id FROM agent_instance +SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name FROM agent_instance WHERE user_id = $1 AND namespace = $2 AND request_id = $3 ` @@ -127,12 +128,13 @@ func (q *Queries) GetAgentInstanceByRequest(ctx context.Context, arg GetAgentIns &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ) return i, err } const getAgentInstanceForUser = `-- name: GetAgentInstanceForUser :one -SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id FROM agent_instance WHERE namespace = $1 AND id = $2 AND user_id = $3 +SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name FROM agent_instance WHERE namespace = $1 AND id = $2 AND user_id = $3 ` type GetAgentInstanceForUserParams struct { @@ -156,6 +158,47 @@ func (q *Queries) GetAgentInstanceForUser(ctx context.Context, arg GetAgentInsta &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, + ) + return i, err +} + +const getAgentInstanceShareByTokenHash = `-- name: GetAgentInstanceShareByTokenHash :one +SELECT s.id, s.namespace, s.instance_id, s.creator, s.permission, s.token_hash, s.created_at, i.user_id AS owner_user_id +FROM agent_instance_share s +JOIN agent_instance i ON i.id = s.instance_id +WHERE s.token_hash = $1 +` + +type GetAgentInstanceShareByTokenHashRow struct { + ID string + Namespace string + InstanceID string + Creator string + Permission string + TokenHash []byte + CreatedAt time.Time + OwnerUserID string +} + +// Resolves a share token to the share and the instance's owner. +// +// The owner is joined in because that is what the share grants: the reader is +// authenticated as themselves, and the token widens what that account may read to +// what the *owner* can see. Without the owner's user id the instance lookup would +// run as the visitor and find nothing. +func (q *Queries) GetAgentInstanceShareByTokenHash(ctx context.Context, tokenHash []byte) (GetAgentInstanceShareByTokenHashRow, error) { + row := q.db.QueryRow(ctx, getAgentInstanceShareByTokenHash, tokenHash) + var i GetAgentInstanceShareByTokenHashRow + err := row.Scan( + &i.ID, + &i.Namespace, + &i.InstanceID, + &i.Creator, + &i.Permission, + &i.TokenHash, + &i.CreatedAt, + &i.OwnerUserID, ) return i, err } @@ -239,10 +282,10 @@ func (q *Queries) InsertA2AContext(ctx context.Context, arg InsertA2AContextPara const insertAgentInstance = `-- name: InsertAgentInstance :one INSERT INTO agent_instance ( - id, namespace, user_id, request_id, context_id, prepared_revision, state, operation, labels, data -) VALUES ($1, $2, $3, $4, $5, $6, 'CREATING', 'CREATE', $7, $8) + id, namespace, user_id, request_id, context_id, prepared_revision, state, operation, labels, name, data +) VALUES ($1, $2, $3, $4, $5, $6, 'CREATING', 'CREATE', $7, $8, $9) ON CONFLICT (user_id, namespace, request_id) DO NOTHING -RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id +RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name ` type InsertAgentInstanceParams struct { @@ -253,6 +296,7 @@ type InsertAgentInstanceParams struct { ContextID string PreparedRevision *string Labels []byte + Name string Data []byte } @@ -265,6 +309,7 @@ func (q *Queries) InsertAgentInstance(ctx context.Context, arg InsertAgentInstan arg.ContextID, arg.PreparedRevision, arg.Labels, + arg.Name, arg.Data, ) var i AgentInstance @@ -280,6 +325,7 @@ func (q *Queries) InsertAgentInstance(ctx context.Context, arg InsertAgentInstan &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ) return i, err } @@ -290,7 +336,7 @@ INSERT INTO agent_instance ( state, operation, labels, data ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'CREATING', 'CREATE', $8, $9) ON CONFLICT (user_id, namespace, request_id) DO NOTHING -RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id +RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name ` type InsertForkedAgentInstanceParams struct { @@ -330,6 +376,7 @@ func (q *Queries) InsertForkedAgentInstance(ctx context.Context, arg InsertForke &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ) return i, err } @@ -386,24 +433,37 @@ func (q *Queries) ListAgentInstanceShares(ctx context.Context, arg ListAgentInst } const listAgentInstances = `-- name: ListAgentInstances :many -SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id FROM agent_instance -WHERE namespace = $1 - AND ($2::boolean OR user_id = $3) - AND id > $4 - AND labels @> $5::jsonb -ORDER BY id -LIMIT $6 +SELECT i.id, i.namespace, i.user_id, i.request_id, i.prepared_revision, i.state, i.labels, i.data, i.operation, i.context_id, i.source_checkpoint_id, i.name FROM agent_instance i +LEFT JOIN runtime_revision r ON r.revision = i.prepared_revision +WHERE i.namespace = $1 + AND ($2::boolean OR i.user_id = $3) + AND i.id > $4 + AND i.labels @> $5::jsonb + AND ($6::text = '' OR r.agent_template_name = $6) + AND ($7::text = '' OR r.harness_name = $7) +ORDER BY i.id +LIMIT $8 ` type ListAgentInstancesParams struct { - Namespace string - AllUsers bool - UserID string - AfterID string - MatchLabels []byte - PageSize int32 + Namespace string + AllUsers bool + UserID string + AfterID string + MatchLabels []byte + AgentTemplate string + Harness string + PageSize int32 } +// Lists the conversations an instance is, optionally narrowed to one agent. +// +// An agent is an (AgentTemplate, Harness) pair, and the instance row carries +// neither name as a column -- both live inside `data`. They are resolved through +// `prepared_revision`, which is a foreign key to `runtime_revision` and does +// carry them, so the filter needs no new column and matches rows written before +// it existed. An instance with no prepared revision belongs to no pair and +// therefore matches no template or harness filter. func (q *Queries) ListAgentInstances(ctx context.Context, arg ListAgentInstancesParams) ([]AgentInstance, error) { rows, err := q.db.Query(ctx, listAgentInstances, arg.Namespace, @@ -411,6 +471,8 @@ func (q *Queries) ListAgentInstances(ctx context.Context, arg ListAgentInstances arg.UserID, arg.AfterID, arg.MatchLabels, + arg.AgentTemplate, + arg.Harness, arg.PageSize, ) if err != nil { @@ -432,6 +494,7 @@ func (q *Queries) ListAgentInstances(ctx context.Context, arg ListAgentInstances &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ); err != nil { return nil, err } @@ -444,7 +507,7 @@ func (q *Queries) ListAgentInstances(ctx context.Context, arg ListAgentInstances } const lockAgentInstance = `-- name: LockAgentInstance :one -SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id FROM agent_instance WHERE id = $1 FOR UPDATE +SELECT id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name FROM agent_instance WHERE id = $1 FOR UPDATE ` func (q *Queries) LockAgentInstance(ctx context.Context, id string) (AgentInstance, error) { @@ -462,6 +525,7 @@ func (q *Queries) LockAgentInstance(ctx context.Context, id string) (AgentInstan &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ) return i, err } @@ -470,7 +534,7 @@ const markAgentInstanceReady = `-- name: MarkAgentInstanceReady :one UPDATE agent_instance SET state = 'READY', operation = 'NONE', data = $2 WHERE id = $1 AND state = 'CREATING' AND operation = 'CREATE' -RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id +RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name ` type MarkAgentInstanceReadyParams struct { @@ -493,6 +557,50 @@ func (q *Queries) MarkAgentInstanceReady(ctx context.Context, arg MarkAgentInsta &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, + ) + return i, err +} + +const renameAgentInstance = `-- name: RenameAgentInstance :one +UPDATE agent_instance +SET name = $1 +WHERE namespace = $2 AND id = $3 AND user_id = $4 +RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name +` + +type RenameAgentInstanceParams struct { + Name string + Namespace string + ID string + UserID string +} + +// Renames an instance in place. The row's `data` blob also carries the message, +// but `toAgentInstance` reads the name from this column, exactly as it does for +// `state` and `operation`, so the column is the single authority and the two +// cannot drift. +func (q *Queries) RenameAgentInstance(ctx context.Context, arg RenameAgentInstanceParams) (AgentInstance, error) { + row := q.db.QueryRow(ctx, renameAgentInstance, + arg.Name, + arg.Namespace, + arg.ID, + arg.UserID, + ) + var i AgentInstance + err := row.Scan( + &i.ID, + &i.Namespace, + &i.UserID, + &i.RequestID, + &i.PreparedRevision, + &i.State, + &i.Labels, + &i.Data, + &i.Operation, + &i.ContextID, + &i.SourceCheckpointID, + &i.Name, ) return i, err } @@ -510,7 +618,7 @@ WHERE agent_instance.id = $4 WHERE c.source_instance_id = agent_instance.id AND c.state = 'CREATING' ) ) -RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id +RETURNING id, namespace, user_id, request_id, prepared_revision, state, labels, data, operation, context_id, source_checkpoint_id, name ` type TransitionAgentInstanceParams struct { @@ -544,6 +652,7 @@ func (q *Queries) TransitionAgentInstance(ctx context.Context, arg TransitionAge &i.Operation, &i.ContextID, &i.SourceCheckpointID, + &i.Name, ) return i, err } diff --git a/go/core/internal/database/gen/models.go b/go/core/internal/database/gen/models.go index bd36c3e23..fad3a2ea6 100644 --- a/go/core/internal/database/gen/models.go +++ b/go/core/internal/database/gen/models.go @@ -42,6 +42,7 @@ type AgentInstance struct { Operation string ContextID string SourceCheckpointID *string + Name string } type AgentInstanceCheckpoint struct { diff --git a/go/core/internal/database/gen/querier.go b/go/core/internal/database/gen/querier.go index 27982c941..94e424507 100644 --- a/go/core/internal/database/gen/querier.go +++ b/go/core/internal/database/gen/querier.go @@ -34,6 +34,13 @@ type Querier interface { GetAgentInstanceCheckpoint(ctx context.Context, arg GetAgentInstanceCheckpointParams) (AgentInstanceCheckpoint, error) GetAgentInstanceCheckpointByRequest(ctx context.Context, arg GetAgentInstanceCheckpointByRequestParams) (AgentInstanceCheckpoint, error) GetAgentInstanceForUser(ctx context.Context, arg GetAgentInstanceForUserParams) (AgentInstance, error) + // Resolves a share token to the share and the instance's owner. + // + // The owner is joined in because that is what the share grants: the reader is + // authenticated as themselves, and the token widens what that account may read to + // what the *owner* can see. Without the owner's user id the instance lookup would + // run as the visitor and find nothing. + GetAgentInstanceShareByTokenHash(ctx context.Context, tokenHash []byte) (GetAgentInstanceShareByTokenHashRow, error) GetAgentInstanceTask(ctx context.Context, arg GetAgentInstanceTaskParams) (AgentInstanceTask, error) GetAgentInstanceTaskByMessageID(ctx context.Context, arg GetAgentInstanceTaskByMessageIDParams) (AgentInstanceTask, error) GetCheckpoint(ctx context.Context, arg GetCheckpointParams) (LgCheckpoint, error) @@ -80,6 +87,14 @@ type Querier interface { ListAgentInstanceShares(ctx context.Context, arg ListAgentInstanceSharesParams) ([]AgentInstanceShare, error) ListAgentInstanceTaskHistory(ctx context.Context, arg ListAgentInstanceTaskHistoryParams) ([]ListAgentInstanceTaskHistoryRow, error) ListAgentInstanceTasks(ctx context.Context, arg ListAgentInstanceTasksParams) ([]AgentInstanceTask, error) + // Lists the conversations an instance is, optionally narrowed to one agent. + // + // An agent is an (AgentTemplate, Harness) pair, and the instance row carries + // neither name as a column -- both live inside `data`. They are resolved through + // `prepared_revision`, which is a foreign key to `runtime_revision` and does + // carry them, so the filter needs no new column and matches rows written before + // it existed. An instance with no prepared revision belongs to no pair and + // therefore matches no template or harness filter. ListAgentInstances(ctx context.Context, arg ListAgentInstancesParams) ([]AgentInstance, error) ListAgentMemories(ctx context.Context, arg ListAgentMemoriesParams) ([]Memory, error) ListAgents(ctx context.Context) ([]Agent, error) @@ -103,13 +118,27 @@ type Querier interface { ListTools(ctx context.Context) ([]Tool, error) ListToolsForServer(ctx context.Context, arg ListToolsForServerParams) ([]Tool, error) ListUnreferencedRuntimeRevisions(ctx context.Context) ([]RuntimeRevision, error) - // LockActiveAgentInstanceTask holds the instance's non-terminal task for the - // rest of the transaction so reclamation cannot overwrite concurrent progress. LockActiveAgentInstanceTask(ctx context.Context, contextID string) (AgentInstanceTask, error) LockAgentInstance(ctx context.Context, id string) (AgentInstance, error) + // LockActiveAgentInstanceTask holds the instance's non-terminal task for the + // rest of the transaction so reclamation cannot overwrite concurrent progress. + // + // One task by id, whatever state it is in. + // + // Distinct from LockActiveAgentInstanceTask, which finds whichever task currently + // holds the instance's turn — and deliberately no longer counts a parked one, since a + // question awaiting an answer must not stop the next turn starting. The parked-task + // operations still need to reach that exact task to answer it or give it up, so they + // name it instead of asking for the active one. + LockAgentInstanceTask(ctx context.Context, arg LockAgentInstanceTaskParams) (AgentInstanceTask, error) LockReadyAgentInstanceCheckpoint(ctx context.Context, arg LockReadyAgentInstanceCheckpointParams) (AgentInstanceCheckpoint, error) MarkAgentInstanceReady(ctx context.Context, arg MarkAgentInstanceReadyParams) (AgentInstance, error) MarkRuntimeRevisionSuccessful(ctx context.Context, arg MarkRuntimeRevisionSuccessfulParams) error + // Renames an instance in place. The row's `data` blob also carries the message, + // but `toAgentInstance` reads the name from this column, exactly as it does for + // `state` and `operation`, so the column is the single authority and the two + // cannot drift. + RenameAgentInstance(ctx context.Context, arg RenameAgentInstanceParams) (AgentInstance, error) RetireAgentTemplateHarnessPair(ctx context.Context, arg RetireAgentTemplateHarnessPairParams) error RetireAgentTemplateHarnessPairs(ctx context.Context, arg RetireAgentTemplateHarnessPairsParams) error RetireOtherAgentTemplateHarnessPairs(ctx context.Context, arg RetireOtherAgentTemplateHarnessPairsParams) error diff --git a/go/core/internal/database/queries/agent_instance_tasks.sql b/go/core/internal/database/queries/agent_instance_tasks.sql index 17ee40bc1..50e6d85f6 100644 --- a/go/core/internal/database/queries/agent_instance_tasks.sql +++ b/go/core/internal/database/queries/agent_instance_tasks.sql @@ -98,6 +98,19 @@ INSERT INTO agent_instance_task ( -- LockActiveAgentInstanceTask holds the instance's non-terminal task for the -- rest of the transaction so reclamation cannot overwrite concurrent progress. +-- name: LockAgentInstanceTask :one +-- +-- One task by id, whatever state it is in. +-- +-- Distinct from LockActiveAgentInstanceTask, which finds whichever task currently +-- holds the instance's turn — and deliberately no longer counts a parked one, since a +-- question awaiting an answer must not stop the next turn starting. The parked-task +-- operations still need to reach that exact task to answer it or give it up, so they +-- name it instead of asking for the active one. +SELECT * FROM agent_instance_task +WHERE context_id = $1 AND id = $2 +FOR UPDATE; + -- name: LockActiveAgentInstanceTask :one SELECT * FROM agent_instance_task WHERE context_id = $1 diff --git a/go/core/internal/database/queries/agent_instances.sql b/go/core/internal/database/queries/agent_instances.sql index 75ed84248..4c9eda0b3 100644 --- a/go/core/internal/database/queries/agent_instances.sql +++ b/go/core/internal/database/queries/agent_instances.sql @@ -13,8 +13,8 @@ WHERE p.namespace = $1 -- name: InsertAgentInstance :one INSERT INTO agent_instance ( - id, namespace, user_id, request_id, context_id, prepared_revision, state, operation, labels, data -) VALUES ($1, $2, $3, $4, $5, $6, 'CREATING', 'CREATE', $7, $8) + id, namespace, user_id, request_id, context_id, prepared_revision, state, operation, labels, name, data +) VALUES ($1, $2, $3, $4, $5, $6, 'CREATING', 'CREATE', $7, $8, $9) ON CONFLICT (user_id, namespace, request_id) DO NOTHING RETURNING *; @@ -39,13 +39,24 @@ SELECT * FROM agent_instance WHERE id = $1 FOR UPDATE; -- name: GetAgentInstanceForUser :one SELECT * FROM agent_instance WHERE namespace = $1 AND id = $2 AND user_id = $3; +-- Lists the conversations an instance is, optionally narrowed to one agent. +-- +-- An agent is an (AgentTemplate, Harness) pair, and the instance row carries +-- neither name as a column -- both live inside `data`. They are resolved through +-- `prepared_revision`, which is a foreign key to `runtime_revision` and does +-- carry them, so the filter needs no new column and matches rows written before +-- it existed. An instance with no prepared revision belongs to no pair and +-- therefore matches no template or harness filter. -- name: ListAgentInstances :many -SELECT * FROM agent_instance -WHERE namespace = sqlc.arg(namespace) - AND (sqlc.arg(all_users)::boolean OR user_id = sqlc.arg(user_id)) - AND id > sqlc.arg(after_id) - AND labels @> sqlc.arg(match_labels)::jsonb -ORDER BY id +SELECT i.* FROM agent_instance i +LEFT JOIN runtime_revision r ON r.revision = i.prepared_revision +WHERE i.namespace = sqlc.arg(namespace) + AND (sqlc.arg(all_users)::boolean OR i.user_id = sqlc.arg(user_id)) + AND i.id > sqlc.arg(after_id) + AND i.labels @> sqlc.arg(match_labels)::jsonb + AND (sqlc.arg(agent_template)::text = '' OR r.agent_template_name = sqlc.arg(agent_template)) + AND (sqlc.arg(harness)::text = '' OR r.harness_name = sqlc.arg(harness)) +ORDER BY i.id LIMIT sqlc.arg(page_size); -- name: MarkAgentInstanceReady :one @@ -69,6 +80,16 @@ WHERE agent_instance.id = sqlc.arg(id) ) RETURNING *; +-- Renames an instance in place. The row's `data` blob also carries the message, +-- but `toAgentInstance` reads the name from this column, exactly as it does for +-- `state` and `operation`, so the column is the single authority and the two +-- cannot drift. +-- name: RenameAgentInstance :one +UPDATE agent_instance +SET name = sqlc.arg(name) +WHERE namespace = sqlc.arg(namespace) AND id = sqlc.arg(id) AND user_id = sqlc.arg(user_id) +RETURNING *; + -- name: DeleteAgentInstance :exec DELETE FROM agent_instance WHERE id = $1; @@ -78,6 +99,18 @@ INSERT INTO agent_instance_share ( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; +-- Resolves a share token to the share and the instance's owner. +-- +-- The owner is joined in because that is what the share grants: the reader is +-- authenticated as themselves, and the token widens what that account may read to +-- what the *owner* can see. Without the owner's user id the instance lookup would +-- run as the visitor and find nothing. +-- name: GetAgentInstanceShareByTokenHash :one +SELECT s.*, i.user_id AS owner_user_id +FROM agent_instance_share s +JOIN agent_instance i ON i.id = s.instance_id +WHERE s.token_hash = $1; + -- name: ListAgentInstanceShares :many SELECT s.* FROM agent_instance_share s JOIN agent_instance i ON i.id = s.instance_id diff --git a/go/core/internal/grpcserver/agenttemplate.go b/go/core/internal/grpcserver/agenttemplate.go new file mode 100644 index 000000000..4cedf8706 --- /dev/null +++ b/go/core/internal/grpcserver/agenttemplate.go @@ -0,0 +1,151 @@ +package grpcserver + +import ( + "context" + + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/structuredobject" + "github.com/kagent-dev/kagent/go/api/v1alpha3" + agenttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/agenttemplate" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "k8s.io/apimachinery/pkg/types" +) + +const agentTemplateKind = "AgentTemplate" + +type agentTemplateServer struct { + apiv1alpha1.UnimplementedAgentTemplateServiceServer + service *agenttemplateservice.Service + maxMessageBytes int +} + +func newAgentTemplateServer(service *agenttemplateservice.Service, maxMessageBytes int) *agentTemplateServer { + return &agentTemplateServer{service: service, maxMessageBytes: maxMessageBytes} +} + +func (s *agentTemplateServer) ListAgentTemplates(ctx context.Context, request *apiv1alpha1.ListAgentTemplatesRequest) (*apiv1alpha1.ListAgentTemplatesResponse, error) { + items, err := s.service.List(ctx, request.GetNamespace()) + if err != nil { + return nil, err + } + templates := make([]*apiv1alpha1.AgentTemplate, 0, len(items)) + for index := range items { + template, err := s.agentTemplate(&items[index]) + if err != nil { + return nil, err + } + templates = append(templates, template) + } + return &apiv1alpha1.ListAgentTemplatesResponse{AgentTemplates: templates}, nil +} + +func (s *agentTemplateServer) GetAgentTemplate(ctx context.Context, request *apiv1alpha1.GetAgentTemplateRequest) (*apiv1alpha1.GetAgentTemplateResponse, error) { + ref, err := requiredAgentTemplateRef(request.GetRef()) + if err != nil { + return nil, err + } + result, err := s.service.Get(ctx, ref) + if err != nil { + return nil, err + } + template, err := s.agentTemplate(result) + if err != nil { + return nil, err + } + return &apiv1alpha1.GetAgentTemplateResponse{AgentTemplate: template}, nil +} + +func (s *agentTemplateServer) CreateAgentTemplate(ctx context.Context, request *apiv1alpha1.CreateAgentTemplateRequest) (*apiv1alpha1.CreateAgentTemplateResponse, error) { + incoming := &v1alpha3.AgentTemplate{} + if err := s.decodeResource(request.GetRef(), request.GetResource(), incoming); err != nil { + return nil, err + } + result, err := s.service.Create(ctx, incoming) + if err != nil { + return nil, err + } + template, err := s.agentTemplate(result) + if err != nil { + return nil, err + } + return &apiv1alpha1.CreateAgentTemplateResponse{AgentTemplate: template}, nil +} + +func (s *agentTemplateServer) UpdateAgentTemplate(ctx context.Context, request *apiv1alpha1.UpdateAgentTemplateRequest) (*apiv1alpha1.UpdateAgentTemplateResponse, error) { + ref, err := requiredAgentTemplateRef(request.GetRef()) + if err != nil { + return nil, err + } + incoming := &v1alpha3.AgentTemplate{} + if err := s.decodeResource(request.GetRef(), request.GetResource(), incoming); err != nil { + return nil, err + } + result, err := s.service.Update(ctx, ref, incoming) + if err != nil { + return nil, err + } + template, err := s.agentTemplate(result) + if err != nil { + return nil, err + } + return &apiv1alpha1.UpdateAgentTemplateResponse{AgentTemplate: template}, nil +} + +func (s *agentTemplateServer) DeleteAgentTemplate(ctx context.Context, request *apiv1alpha1.DeleteAgentTemplateRequest) (*apiv1alpha1.DeleteAgentTemplateResponse, error) { + ref, err := requiredAgentTemplateRef(request.GetRef()) + if err != nil { + return nil, err + } + if err := s.service.Delete(ctx, ref); err != nil { + return nil, err + } + return &apiv1alpha1.DeleteAgentTemplateResponse{}, nil +} + +func (s *agentTemplateServer) agentTemplate(template *v1alpha3.AgentTemplate) (*apiv1alpha1.AgentTemplate, error) { + resource, err := structuredobject.FromGo(template, v1alpha3.GroupVersion.String(), agentTemplateKind, s.maxMessageBytes) + if err != nil { + return nil, serviceerrors.NewInternal("Failed to encode AgentTemplate resource", err) + } + admitting := make([]string, 0, len(template.Status.Harnesses)) + for _, status := range template.Status.Harnesses { + admitting = append(admitting, status.Harness) + } + return &apiv1alpha1.AgentTemplate{ + Ref: &apiv1alpha1.ResourceReference{Namespace: template.Namespace, Name: template.Name}, + // The model config lives in the template's own namespace: the CRD's + // reference is name-only and same-namespace by construction. + ModelConfigRef: &apiv1alpha1.ResourceReference{Namespace: template.Namespace, Name: template.Spec.ModelConfig.Name}, + Resource: resource, + Description: template.Spec.Description, + AdmittingHarnesses: admitting, + }, nil +} + +// decodeResource reads the CR out of the request and forces its metadata to +// agree with the ref, so a payload naming a different object cannot be used to +// write outside the namespace the caller was authorized against. +func (s *agentTemplateServer) decodeResource(ref *apiv1alpha1.ResourceReference, resource *apiv1alpha1.StructuredObject, destination *v1alpha3.AgentTemplate) error { + if ref == nil || ref.GetNamespace() == "" || ref.GetName() == "" { + return serviceerrors.NewInvalidArgument("AgentTemplate namespace and name are required", nil) + } + if err := structuredobject.ToGo(resource, agentTemplateKind, destination, s.maxMessageBytes); err != nil { + return serviceerrors.NewInvalidArgument("Invalid AgentTemplate resource", err) + } + if destination.GetName() != "" && destination.GetName() != ref.GetName() { + return serviceerrors.NewInvalidArgument("AgentTemplate reference does not match resource metadata", nil) + } + if destination.GetNamespace() != "" && destination.GetNamespace() != ref.GetNamespace() { + return serviceerrors.NewInvalidArgument("AgentTemplate reference does not match resource metadata", nil) + } + destination.SetName(ref.GetName()) + destination.SetNamespace(ref.GetNamespace()) + return nil +} + +func requiredAgentTemplateRef(ref *apiv1alpha1.ResourceReference) (types.NamespacedName, error) { + if ref == nil || ref.GetNamespace() == "" || ref.GetName() == "" { + return types.NamespacedName{}, serviceerrors.NewInvalidArgument("AgentTemplate namespace and name are required", nil) + } + return types.NamespacedName{Namespace: ref.GetNamespace(), Name: ref.GetName()}, nil +} diff --git a/go/core/internal/grpcserver/agenttemplate_harness_test.go b/go/core/internal/grpcserver/agenttemplate_harness_test.go new file mode 100644 index 000000000..71456295b --- /dev/null +++ b/go/core/internal/grpcserver/agenttemplate_harness_test.go @@ -0,0 +1,290 @@ +package grpcserver + +import ( + "context" + "net" + "testing" + + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/structuredobject" + "github.com/kagent-dev/kagent/go/api/v1alpha3" + authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" + agenttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/agenttemplate" + harnessservice "github.com/kagent-dev/kagent/go/core/internal/service/harness" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const testHarnessImage = "example.test/runtime@sha256:0000000000000000000000000000000000000000000000000000000000000000" + +func newTemplateAndHarnessConnection(t *testing.T, objects ...ctrlclient.Object) *grpc.ClientConn { + t.Helper() + scheme := runtime.NewScheme() + if err := v1alpha3.AddToScheme(scheme); err != nil { + t.Fatalf("v1alpha3.AddToScheme() error = %v", err) + } + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + + listener := bufconn.Listen(DefaultMaxMessageSize) + server, err := New(Config{ + Listener: listener, + Registerer: prometheus.NewRegistry(), + Authenticator: &authimpl.UnsecureAuthenticator{}, + AgentTemplateService: agenttemplateservice.NewService(kubeClient, &authimpl.NoopAuthorizer{}), + HarnessService: harnessservice.NewService(kubeClient, &authimpl.NoopAuthorizer{}), + }) + if err != nil { + t.Fatalf("New() error = %v", err) + } + serverContext, cancelServer := context.WithCancel(t.Context()) + done := make(chan error, 1) + go func() { done <- server.Start(serverContext) }() + t.Cleanup(func() { + cancelServer() + if err := <-done; err != nil { + t.Errorf("gRPC server shutdown error = %v", err) + } + }) + + connection, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }), + ) + if err != nil { + t.Fatalf("grpc.NewClient() error = %v", err) + } + t.Cleanup(func() { _ = connection.Close() }) + return connection +} + +func testAgentTemplate(namespace, name, modelConfig string) *v1alpha3.AgentTemplate { + return &v1alpha3.AgentTemplate{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, + Spec: v1alpha3.AgentTemplateSpec{ + ModelConfig: v1alpha3.AgentTemplateLocalReference{Name: modelConfig}, + Description: "a template", + }, + } +} + +func testHarness(namespace, name, workerPool string) *v1alpha3.Harness { + return &v1alpha3.Harness{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, + Spec: v1alpha3.HarnessSpec{ + Codex: &v1alpha3.CodexHarness{}, + Workload: v1alpha3.HarnessWorkload{Image: testHarnessImage}, + Substrate: v1alpha3.HarnessSubstratePolicy{ + WorkerPoolRef: corev1.LocalObjectReference{Name: workerPool}, + SnapshotPolicy: v1alpha3.HarnessSnapshotPolicy{Location: "s3://snapshots"}, + }, + }, + } +} + +func structured(t *testing.T, value any, kind string) *apiv1alpha1.StructuredObject { + t.Helper() + resource, err := structuredobject.FromGo(value, v1alpha3.GroupVersion.String(), kind, DefaultMaxMessageSize) + if err != nil { + t.Fatalf("structuredobject.FromGo() error = %v", err) + } + return resource +} + +func assertCode(t *testing.T, err error, want codes.Code) { + t.Helper() + if got, ok := status.FromError(err); !ok || got.Code() != want { + t.Fatalf("error = %v, want code %v", err, want) + } +} + +func TestAgentTemplateServiceGeneratedClient(t *testing.T) { + // The existing template carries controller-written status so the response + // can be checked for the admitting-harness denormalisation, which a caller + // cannot derive from the template alone. + existing := testAgentTemplate("team", "z-existing", "gpt") + existing.Status = v1alpha3.AgentTemplateStatus{ + Harnesses: []v1alpha3.AgentTemplateHarnessStatus{{Harness: "shared", DesiredRevision: "rev-1"}}, + } + client := apiv1alpha1.NewAgentTemplateServiceClient(newTemplateAndHarnessConnection(t, existing)) + ctx := metadata.NewOutgoingContext(t.Context(), metadata.Pairs("x-user-id", "template-user")) + ref := &apiv1alpha1.ResourceReference{Namespace: "team", Name: "a-created"} + + created, err := client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{ + Ref: ref, + Resource: structured(t, testAgentTemplate("team", "a-created", "gpt"), agentTemplateKind), + }) + if err != nil { + t.Fatalf("CreateAgentTemplate() error = %v", err) + } + if got := created.GetAgentTemplate().GetModelConfigRef(); got.GetNamespace() != "team" || got.GetName() != "gpt" { + t.Fatalf("CreateAgentTemplate() modelConfigRef = %+v, want team/gpt", got) + } + if got := created.GetAgentTemplate().GetResource().GetKind(); got != agentTemplateKind { + t.Fatalf("CreateAgentTemplate() resource kind = %q, want %q", got, agentTemplateKind) + } + + _, err = client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{ + Ref: ref, + Resource: structured(t, testAgentTemplate("team", "a-created", "gpt"), agentTemplateKind), + }) + assertCode(t, err, codes.AlreadyExists) + + got, err := client.GetAgentTemplate(ctx, &apiv1alpha1.GetAgentTemplateRequest{Ref: ref}) + if err != nil { + t.Fatalf("GetAgentTemplate() error = %v", err) + } + if got.GetAgentTemplate().GetDescription() != "a template" { + t.Fatalf("GetAgentTemplate() description = %q", got.GetAgentTemplate().GetDescription()) + } + + updated, err := client.UpdateAgentTemplate(ctx, &apiv1alpha1.UpdateAgentTemplateRequest{ + Ref: ref, + Resource: structured(t, testAgentTemplate("team", "a-created", "claude"), agentTemplateKind), + }) + if err != nil { + t.Fatalf("UpdateAgentTemplate() error = %v", err) + } + if updated.GetAgentTemplate().GetModelConfigRef().GetName() != "claude" { + t.Fatalf("UpdateAgentTemplate() modelConfigRef = %+v", updated.GetAgentTemplate().GetModelConfigRef()) + } + + listed, err := client.ListAgentTemplates(ctx, &apiv1alpha1.ListAgentTemplatesRequest{Namespace: "team"}) + if err != nil { + t.Fatalf("ListAgentTemplates() error = %v", err) + } + if len(listed.GetAgentTemplates()) != 2 { + t.Fatalf("ListAgentTemplates() count = %d, want 2", len(listed.GetAgentTemplates())) + } + if name := listed.GetAgentTemplates()[0].GetRef().GetName(); name != "a-created" { + t.Fatalf("ListAgentTemplates()[0] = %q, want a-created first", name) + } + if harnesses := listed.GetAgentTemplates()[1].GetAdmittingHarnesses(); len(harnesses) != 1 || harnesses[0] != "shared" { + t.Fatalf("ListAgentTemplates()[1].admittingHarnesses = %v, want [shared]", harnesses) + } + + // A resource whose metadata names a different object must be rejected rather + // than silently written to the ref the caller was authorized against. + _, err = client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "mismatch"}, + Resource: structured(t, testAgentTemplate("other", "elsewhere", "gpt"), agentTemplateKind), + }) + assertCode(t, err, codes.InvalidArgument) + + _, err = client.CreateAgentTemplate(ctx, &apiv1alpha1.CreateAgentTemplateRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "wrong-kind"}, + Resource: structured(t, testHarness("team", "wrong-kind", "pool-a"), harnessKind), + }) + assertCode(t, err, codes.InvalidArgument) + + _, err = client.ListAgentTemplates(ctx, &apiv1alpha1.ListAgentTemplatesRequest{}) + assertCode(t, err, codes.InvalidArgument) + _, err = client.GetAgentTemplate(ctx, &apiv1alpha1.GetAgentTemplateRequest{}) + assertCode(t, err, codes.InvalidArgument) + _, err = client.GetAgentTemplate(ctx, &apiv1alpha1.GetAgentTemplateRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "absent"}, + }) + assertCode(t, err, codes.NotFound) + + if _, err := client.DeleteAgentTemplate(ctx, &apiv1alpha1.DeleteAgentTemplateRequest{Ref: ref}); err != nil { + t.Fatalf("DeleteAgentTemplate() error = %v", err) + } + _, err = client.DeleteAgentTemplate(ctx, &apiv1alpha1.DeleteAgentTemplateRequest{Ref: ref}) + assertCode(t, err, codes.NotFound) +} + +func TestHarnessServiceGeneratedClient(t *testing.T) { + existing := testHarness("team", "z-existing", "pool-z") + existing.Status = v1alpha3.HarnessStatus{ + Conditions: []metav1.Condition{{ + Type: v1alpha3.HarnessConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: "Ready", + LastTransitionTime: metav1.Now(), + }}, + } + client := apiv1alpha1.NewHarnessServiceClient(newTemplateAndHarnessConnection(t, existing)) + ctx := metadata.NewOutgoingContext(t.Context(), metadata.Pairs("x-user-id", "harness-user")) + ref := &apiv1alpha1.ResourceReference{Namespace: "team", Name: "a-created"} + + created, err := client.CreateHarness(ctx, &apiv1alpha1.CreateHarnessRequest{ + Ref: ref, + Resource: structured(t, testHarness("team", "a-created", "pool-a"), harnessKind), + }) + if err != nil { + t.Fatalf("CreateHarness() error = %v", err) + } + if got := created.GetHarness(); got.GetRuntime() != harnessRuntimeCodex || got.GetWorkloadImage() != testHarnessImage { + t.Fatalf("CreateHarness() = %+v, want codex runtime and pinned image", got) + } + if created.GetHarness().GetReady() { + t.Fatal("CreateHarness() ready = true, want false before the controller observes it") + } + + _, err = client.CreateHarness(ctx, &apiv1alpha1.CreateHarnessRequest{ + Ref: ref, + Resource: structured(t, testHarness("team", "a-created", "pool-a"), harnessKind), + }) + assertCode(t, err, codes.AlreadyExists) + + updated, err := client.UpdateHarness(ctx, &apiv1alpha1.UpdateHarnessRequest{ + Ref: ref, + Resource: structured(t, testHarness("team", "a-created", "pool-b"), harnessKind), + }) + if err != nil { + t.Fatalf("UpdateHarness() error = %v", err) + } + if updated.GetHarness().GetRuntime() != harnessRuntimeCodex { + t.Fatalf("UpdateHarness() runtime = %q", updated.GetHarness().GetRuntime()) + } + + listed, err := client.ListHarnesses(ctx, &apiv1alpha1.ListHarnessesRequest{Namespace: "team"}) + if err != nil { + t.Fatalf("ListHarnesses() error = %v", err) + } + if len(listed.GetHarnesses()) != 2 { + t.Fatalf("ListHarnesses() count = %d, want 2", len(listed.GetHarnesses())) + } + if !listed.GetHarnesses()[1].GetReady() { + t.Fatal("ListHarnesses()[1].ready = false, want the Ready condition reflected") + } + + _, err = client.CreateHarness(ctx, &apiv1alpha1.CreateHarnessRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "mismatch"}, + Resource: structured(t, testHarness("other", "elsewhere", "pool-a"), harnessKind), + }) + assertCode(t, err, codes.InvalidArgument) + + // An AgentHarness payload must not be accepted here: the two kinds are + // distinct and the kind check is what keeps them from being confused. + _, err = client.CreateHarness(ctx, &apiv1alpha1.CreateHarnessRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "wrong-kind"}, + Resource: structured(t, &v1alpha3.AgentHarness{}, "AgentHarness"), + }) + assertCode(t, err, codes.InvalidArgument) + + _, err = client.ListHarnesses(ctx, &apiv1alpha1.ListHarnessesRequest{}) + assertCode(t, err, codes.InvalidArgument) + _, err = client.GetHarness(ctx, &apiv1alpha1.GetHarnessRequest{}) + assertCode(t, err, codes.InvalidArgument) + _, err = client.GetHarness(ctx, &apiv1alpha1.GetHarnessRequest{ + Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "absent"}, + }) + assertCode(t, err, codes.NotFound) + + if _, err := client.DeleteHarness(ctx, &apiv1alpha1.DeleteHarnessRequest{Ref: ref}); err != nil { + t.Fatalf("DeleteHarness() error = %v", err) + } + _, err = client.DeleteHarness(ctx, &apiv1alpha1.DeleteHarnessRequest{Ref: ref}) + assertCode(t, err, codes.NotFound) +} diff --git a/go/core/internal/grpcserver/grpcweb.go b/go/core/internal/grpcserver/grpcweb.go new file mode 100644 index 000000000..79c443033 --- /dev/null +++ b/go/core/internal/grpcserver/grpcweb.go @@ -0,0 +1,78 @@ +package grpcserver + +import ( + "net/http" + "strings" + + "github.com/improbable-eng/grpc-web/go/grpcweb" +) + +// WebHandler exposes the same services over gRPC-Web. +// +// A browser cannot speak gRPC. The protocol needs HTTP/2 trailers, and `fetch` +// gives a page no way to read them, which is why the services registered in +// New() are unreachable from a page however the network is arranged. gRPC-Web +// carries the same frames over HTTP/1.1 with the trailers moved into the body, +// so this wrapper is what makes this API callable from a browser at all. +// +// Wrapping is deliberately all it does: the returned handler serves the very +// server built in New(), so a service registered there is reachable both ways +// and the interceptor chain — authentication included — runs identically for a +// call arriving either way. Nothing here decides who may call what. +// +// Most callers want WebHandlerOr, which also says what to do with everything +// that is not a gRPC-Web request. +func (s *Server) WebHandler() *grpcweb.WrappedGrpcServer { + return grpcweb.WrapServer(s.server, + // The UI is served from the same origin as this API in every deployment + // the chart produces — nginx proxies /api to the controller — so no + // cross-origin allowance is granted here. A deployment that genuinely + // serves the two from different origins configures that on its ingress, + // where the rest of its CORS policy already lives, rather than having + // this server assert a policy it cannot see the whole of. + grpcweb.WithCorsForRegisteredEndpointsOnly(true), + ) +} + +// WebHandlerOr routes gRPC-Web requests to the services and everything else to next. +// +// This is the one statement of the rule, because there is more than one binary +// serving HTTP beside this gRPC server and a second copy would drift. Both the +// v1 controller's HTTP server and the v2 controller's compose their handler +// through here. +// +// Two things it settles that are easy to get wrong: +// +// Requests are told apart by content type rather than by path, so the service +// names never have to be restated. And a leading `/api` is stripped, because the +// wrapper matches the gRPC path a generated client sends — +// `/./` — while the chart's nginx serves the whole API +// under /api on the UI's own origin. That prefix is where a same-origin browser +// has to address it from and nowhere the wrapper can be told about. +func (s *Server) WebHandlerOr(next http.Handler) http.Handler { + web := s.WebHandler() + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !web.IsGrpcWebRequest(r) && !web.IsAcceptableGrpcCorsRequest(r) { + next.ServeHTTP(w, r) + return + } + if trimmed, ok := trimAPIPrefix(r.URL.Path); ok { + r = r.Clone(r.Context()) + r.URL.Path = trimmed + } + web.ServeHTTP(w, r) + }) +} + +// trimAPIPrefix removes a leading /api, reporting whether there was one. +// +// A request that arrives without it — a client addressing the gRPC path directly +// — is passed through untouched rather than rejected, so the same server answers +// both spellings. +func trimAPIPrefix(path string) (string, bool) { + const prefix = "/api" + if !strings.HasPrefix(path, prefix+"/") { + return path, false + } + return strings.TrimPrefix(path, prefix), true +} diff --git a/go/core/internal/grpcserver/grpcweb_test.go b/go/core/internal/grpcserver/grpcweb_test.go new file mode 100644 index 000000000..b996c8c58 --- /dev/null +++ b/go/core/internal/grpcserver/grpcweb_test.go @@ -0,0 +1,28 @@ +package grpcserver + +import ( + "testing" +) + +// The prefix rule, which is the subtle half of WebHandlerOr. +func TestTrimAPIPrefix(t *testing.T) { + tests := []struct { + name string + path string + want string + wantHad bool + }{ + {"a same-origin browser call arrives under /api", "/api/kagent.api.v1alpha1.AgentService/ListAgents", "/kagent.api.v1alpha1.AgentService/ListAgents", true}, + {"a client addressing the gRPC path directly is untouched", "/kagent.api.v1alpha1.AgentService/ListAgents", "/kagent.api.v1alpha1.AgentService/ListAgents", false}, + {"/api alone is not a prefix to strip", "/api", "/api", false}, + {"a path merely starting with the letters api is untouched", "/apifoo/Bar", "/apifoo/Bar", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, had := trimAPIPrefix(tt.path) + if got != tt.want || had != tt.wantHad { + t.Errorf("trimAPIPrefix(%q) = (%q,%v), want (%q,%v)", tt.path, got, had, tt.want, tt.wantHad) + } + }) + } +} diff --git a/go/core/internal/grpcserver/harness.go b/go/core/internal/grpcserver/harness.go new file mode 100644 index 000000000..63ca7480c --- /dev/null +++ b/go/core/internal/grpcserver/harness.go @@ -0,0 +1,171 @@ +package grpcserver + +import ( + "context" + + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/structuredobject" + "github.com/kagent-dev/kagent/go/api/v1alpha3" + harnessservice "github.com/kagent-dev/kagent/go/core/internal/service/harness" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/types" +) + +// harnessKind is the Harness CRD, not the AgentHarness one that agent.go +// serves. See the comment on HarnessService in harnesses.proto. +const harnessKind = "Harness" + +// Harness runtime adapter names, as reported in the denormalised runtime field. +const ( + harnessRuntimeKagent = "kagent" + harnessRuntimeCodex = "codex" + harnessRuntimeClaude = "claude" +) + +type harnessServer struct { + apiv1alpha1.UnimplementedHarnessServiceServer + service *harnessservice.Service + maxMessageBytes int +} + +func newHarnessServer(service *harnessservice.Service, maxMessageBytes int) *harnessServer { + return &harnessServer{service: service, maxMessageBytes: maxMessageBytes} +} + +func (s *harnessServer) ListHarnesses(ctx context.Context, request *apiv1alpha1.ListHarnessesRequest) (*apiv1alpha1.ListHarnessesResponse, error) { + items, err := s.service.List(ctx, request.GetNamespace()) + if err != nil { + return nil, err + } + harnesses := make([]*apiv1alpha1.Harness, 0, len(items)) + for index := range items { + encoded, err := s.harness(&items[index]) + if err != nil { + return nil, err + } + harnesses = append(harnesses, encoded) + } + return &apiv1alpha1.ListHarnessesResponse{Harnesses: harnesses}, nil +} + +func (s *harnessServer) GetHarness(ctx context.Context, request *apiv1alpha1.GetHarnessRequest) (*apiv1alpha1.GetHarnessResponse, error) { + ref, err := requiredHarnessRef(request.GetRef()) + if err != nil { + return nil, err + } + result, err := s.service.Get(ctx, ref) + if err != nil { + return nil, err + } + encoded, err := s.harness(result) + if err != nil { + return nil, err + } + return &apiv1alpha1.GetHarnessResponse{Harness: encoded}, nil +} + +func (s *harnessServer) CreateHarness(ctx context.Context, request *apiv1alpha1.CreateHarnessRequest) (*apiv1alpha1.CreateHarnessResponse, error) { + incoming := &v1alpha3.Harness{} + if err := s.decodeResource(request.GetRef(), request.GetResource(), incoming); err != nil { + return nil, err + } + result, err := s.service.Create(ctx, incoming) + if err != nil { + return nil, err + } + encoded, err := s.harness(result) + if err != nil { + return nil, err + } + return &apiv1alpha1.CreateHarnessResponse{Harness: encoded}, nil +} + +func (s *harnessServer) UpdateHarness(ctx context.Context, request *apiv1alpha1.UpdateHarnessRequest) (*apiv1alpha1.UpdateHarnessResponse, error) { + ref, err := requiredHarnessRef(request.GetRef()) + if err != nil { + return nil, err + } + incoming := &v1alpha3.Harness{} + if err := s.decodeResource(request.GetRef(), request.GetResource(), incoming); err != nil { + return nil, err + } + result, err := s.service.Update(ctx, ref, incoming) + if err != nil { + return nil, err + } + encoded, err := s.harness(result) + if err != nil { + return nil, err + } + return &apiv1alpha1.UpdateHarnessResponse{Harness: encoded}, nil +} + +func (s *harnessServer) DeleteHarness(ctx context.Context, request *apiv1alpha1.DeleteHarnessRequest) (*apiv1alpha1.DeleteHarnessResponse, error) { + ref, err := requiredHarnessRef(request.GetRef()) + if err != nil { + return nil, err + } + if err := s.service.Delete(ctx, ref); err != nil { + return nil, err + } + return &apiv1alpha1.DeleteHarnessResponse{}, nil +} + +func (s *harnessServer) harness(object *v1alpha3.Harness) (*apiv1alpha1.Harness, error) { + resource, err := structuredobject.FromGo(object, v1alpha3.GroupVersion.String(), harnessKind, s.maxMessageBytes) + if err != nil { + return nil, serviceerrors.NewInternal("Failed to encode Harness resource", err) + } + return &apiv1alpha1.Harness{ + Ref: &apiv1alpha1.ResourceReference{Namespace: object.Namespace, Name: object.Name}, + Resource: resource, + Runtime: harnessRuntime(object), + WorkloadImage: object.Spec.Workload.Image, + Ready: meta.IsStatusConditionTrue(object.Status.Conditions, v1alpha3.HarnessConditionTypeReady), + }, nil +} + +// harnessRuntime reports which adapter the spec selects. The CRD's CEL rule +// admits exactly one, so the empty string means an object that predates or +// violates that rule rather than a fourth kind of runtime. +func harnessRuntime(object *v1alpha3.Harness) string { + switch { + case object.Spec.Kagent != nil: + return harnessRuntimeKagent + case object.Spec.Codex != nil: + return harnessRuntimeCodex + case object.Spec.Claude != nil: + return harnessRuntimeClaude + default: + return "" + } +} + +// decodeResource reads the CR out of the request and forces its metadata to +// agree with the ref, so a payload naming a different object cannot be used to +// write outside the namespace the caller was authorized against. +func (s *harnessServer) decodeResource(ref *apiv1alpha1.ResourceReference, resource *apiv1alpha1.StructuredObject, destination *v1alpha3.Harness) error { + if ref == nil || ref.GetNamespace() == "" || ref.GetName() == "" { + return serviceerrors.NewInvalidArgument("Harness namespace and name are required", nil) + } + if err := structuredobject.ToGo(resource, harnessKind, destination, s.maxMessageBytes); err != nil { + return serviceerrors.NewInvalidArgument("Invalid Harness resource", err) + } + if destination.GetName() != "" && destination.GetName() != ref.GetName() { + return serviceerrors.NewInvalidArgument("Harness reference does not match resource metadata", nil) + } + if destination.GetNamespace() != "" && destination.GetNamespace() != ref.GetNamespace() { + return serviceerrors.NewInvalidArgument("Harness reference does not match resource metadata", nil) + } + destination.SetName(ref.GetName()) + destination.SetNamespace(ref.GetNamespace()) + return nil +} + +func requiredHarnessRef(ref *apiv1alpha1.ResourceReference) (types.NamespacedName, error) { + if ref == nil || ref.GetNamespace() == "" || ref.GetName() == "" { + return types.NamespacedName{}, serviceerrors.NewInvalidArgument("Harness namespace and name are required", nil) + } + return types.NamespacedName{Namespace: ref.GetNamespace(), Name: ref.GetName()}, nil +} diff --git a/go/core/internal/grpcserver/interceptors.go b/go/core/internal/grpcserver/interceptors.go index 2925ff0f8..6ec7a5777 100644 --- a/go/core/internal/grpcserver/interceptors.go +++ b/go/core/internal/grpcserver/interceptors.go @@ -2,6 +2,7 @@ package grpcserver import ( "context" + "crypto/sha256" "errors" "fmt" "net/http" @@ -76,28 +77,70 @@ func authenticate(ctx context.Context, fullMethod string, authenticator auth.Aut return ctx, status.Error(codes.Internal, "share-token validation is unavailable") } + /* + * Two kinds of share arrive on this one header. + * + * A session share is the older one, over a chat session. An AgentInstance share + * is over the conversation itself, which is what an instance now is. A link + * carries a token and nothing else, so the reader opening it cannot say which + * kind it is — and neither can this. So the session store is asked first, + * because that is the shape that has always worked, and an instance share is + * tried only when there is no session share by that token. + * + * Both resolve to a ShareContext naming exactly one of the two resources, so + * nothing downstream can mistake one for the other. + */ share, err := shareStore.GetSessionShareByToken(authenticatedContext, shareToken) + if err == nil { + if share.ReadOnly && access != AccessPublic && access != AccessRead { + return ctx, status.Error(codes.PermissionDenied, "this share link is read-only") + } + if err := shareStore.RecordShareAccess(authenticatedContext, session.Principal().User.ID, share.ID); err != nil { + ctrllog.FromContext(authenticatedContext).Error(err, "failed to record gRPC share access", "shareID", share.ID) + } + return auth.ShareContextTo(authenticatedContext, &auth.ShareContext{ + Token: shareToken, + SessionID: share.SessionID, + UserID: share.UserID, + ReadOnly: share.ReadOnly, + }), nil + } + if !errors.Is(err, dbpkg.ErrNotFound) { + return ctx, status.Error(codes.Internal, "failed to validate share token") + } + + // Only the digest is stored, which is what stops a database dump being a set of + // working share links — so the token is hashed the same way it was on creation. + digest := sha256.Sum256([]byte(shareToken)) + instanceShare, err := shareStore.GetAgentInstanceShareByTokenHash(authenticatedContext, digest[:]) if err != nil { if errors.Is(err, dbpkg.ErrNotFound) { return ctx, status.Error(codes.PermissionDenied, "invalid or expired share token") } return ctx, status.Error(codes.Internal, "failed to validate share token") } - if share.ReadOnly && access != AccessPublic && access != AccessRead { + // READ_WRITE also allows A2A send and cancel; anything else is read-only. + readOnly := instanceShare.Permission != agentInstanceShareReadWrite + if readOnly && access != AccessPublic && access != AccessRead { return ctx, status.Error(codes.PermissionDenied, "this share link is read-only") } - - if err := shareStore.RecordShareAccess(authenticatedContext, session.Principal().User.ID, share.ID); err != nil { - ctrllog.FromContext(authenticatedContext).Error(err, "failed to record gRPC share access", "shareID", share.ID) - } return auth.ShareContextTo(authenticatedContext, &auth.ShareContext{ - Token: shareToken, - SessionID: share.SessionID, - UserID: share.UserID, - ReadOnly: share.ReadOnly, + Token: shareToken, + // The owner, not the visitor: the token widens what this account may reach + // to what the owner can see, and the instance read runs as the owner. + UserID: instanceShare.OwnerUserID, + ReadOnly: readOnly, + AgentInstanceID: instanceShare.InstanceID, }), nil } +// agentInstanceShareReadWrite is the permission that allows more than reading. +// +// Spelled as the column's own value rather than derived from the proto enum: the +// database stores 'READ_ONLY' or 'READ_WRITE' under a CHECK constraint, and that +// string is what this has to match. +const agentInstanceShareReadWrite = "READ_WRITE" + func incomingHTTPHeaders(ctx context.Context) http.Header { headers := make(http.Header) md, ok := metadata.FromIncomingContext(ctx) diff --git a/go/core/internal/grpcserver/interceptors_test.go b/go/core/internal/grpcserver/interceptors_test.go index 90fa94ffa..031f28122 100644 --- a/go/core/internal/grpcserver/interceptors_test.go +++ b/go/core/internal/grpcserver/interceptors_test.go @@ -51,12 +51,24 @@ type testShareStore struct { err error recordedUserID string recordedShare int64 + + // The AgentInstance half. A share link carries one token and the reader cannot + // know which kind it is, so the interceptor tries both stores. + instanceShare *dbpkg.AgentInstanceShare + instanceShareErr error } func (s *testShareStore) GetSessionShareByToken(context.Context, string) (*dbpkg.SessionShare, error) { return s.share, s.err } +func (s *testShareStore) GetAgentInstanceShareByTokenHash(context.Context, []byte) (*dbpkg.AgentInstanceShare, error) { + if s.instanceShare == nil && s.instanceShareErr == nil { + return nil, dbpkg.ErrNotFound + } + return s.instanceShare, s.instanceShareErr +} + func (s *testShareStore) RecordShareAccess(_ context.Context, userID string, shareID int64) error { s.recordedUserID = userID s.recordedShare = shareID @@ -166,6 +178,123 @@ func TestAuthenticationUnaryInterceptor(t *testing.T) { } }) + /* + * AgentInstance shares. A share link carries one token and the reader opening it + * cannot know which kind it is, so the interceptor tries the session store and + * then the instance store — and the resulting context names exactly one of the + * two resources, so nothing downstream can mistake one for the other. + */ + t.Run("an AgentInstance share is resolved when no session share matches", func(t *testing.T) { + store := &testShareStore{ + // No session share by this token. + err: dbpkg.ErrNotFound, + instanceShare: &dbpkg.AgentInstanceShare{ + ID: "share-1", Namespace: "kagent", InstanceID: "instance-1", + Permission: "READ_ONLY", OwnerUserID: "owner", + }, + } + ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("x-share-token", "share")) + _, err := authenticationUnaryInterceptor(&testAuthenticator{session: session}, store, policies)( + ctx, nil, &grpc.UnaryServerInfo{FullMethod: readMethod}, + func(ctx context.Context, _ any) (any, error) { + share, ok := pkgauth.ShareContextFrom(ctx) + if !ok { + t.Fatal("no share context") + } + if !share.IsForAgentInstance("instance-1") { + t.Errorf("share is not for instance-1: %#v", share) + } + // The owner, not the visitor: the instance read runs as the owner or + // it finds nothing, because an instance is scoped to its creator. + if share.UserID != "owner" { + t.Errorf("UserID = %q, want the owner", share.UserID) + } + // A session share and an instance share must never be confusable. + if share.SessionID != "" { + t.Errorf("SessionID = %q, want empty on an instance share", share.SessionID) + } + if !share.ReadOnly { + t.Error("READ_ONLY should be read-only") + } + return nil, nil + }, + ) + if err != nil { + t.Fatalf("interceptor error = %v", err) + } + }) + + t.Run("a read-only AgentInstance share cannot send", func(t *testing.T) { + store := &testShareStore{ + err: dbpkg.ErrNotFound, + instanceShare: &dbpkg.AgentInstanceShare{ + InstanceID: "instance-1", Permission: "READ_ONLY", OwnerUserID: "owner", + }, + } + ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("x-share-token", "share")) + _, err := authenticationUnaryInterceptor(&testAuthenticator{session: session}, store, policies)( + ctx, nil, &grpc.UnaryServerInfo{FullMethod: createMethod}, + func(context.Context, any) (any, error) { + t.Fatal("handler should not run") + return nil, nil + }, + ) + if got := status.Code(err); got != codes.PermissionDenied { + t.Fatalf("code = %v, want PermissionDenied", got) + } + }) + + t.Run("a READ_WRITE AgentInstance share may send", func(t *testing.T) { + store := &testShareStore{ + err: dbpkg.ErrNotFound, + instanceShare: &dbpkg.AgentInstanceShare{ + InstanceID: "instance-1", Permission: "READ_WRITE", OwnerUserID: "owner", + }, + } + ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("x-share-token", "share")) + ran := false + _, err := authenticationUnaryInterceptor(&testAuthenticator{session: session}, store, policies)( + ctx, nil, &grpc.UnaryServerInfo{FullMethod: createMethod}, + func(ctx context.Context, _ any) (any, error) { + ran = true + share, _ := pkgauth.ShareContextFrom(ctx) + if share.ReadOnly { + t.Error("READ_WRITE should not be read-only") + } + return nil, nil + }, + ) + if err != nil { + t.Fatalf("interceptor error = %v", err) + } + if !ran { + t.Fatal("handler did not run") + } + }) + + t.Run("a session share is not authority over an instance", func(t *testing.T) { + // The whole reason the two ids are separate fields. A session share reaching + // the A2A gateway must not be treated as authority over an instance whose id + // happens to match. + store := &testShareStore{share: &dbpkg.SessionShare{ + ID: 7, Token: "share", SessionID: "instance-1", UserID: "owner", ReadOnly: true, + }} + ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("x-share-token", "share")) + _, err := authenticationUnaryInterceptor(&testAuthenticator{session: session}, store, policies)( + ctx, nil, &grpc.UnaryServerInfo{FullMethod: readMethod}, + func(ctx context.Context, _ any) (any, error) { + share, _ := pkgauth.ShareContextFrom(ctx) + if share.IsForAgentInstance("instance-1") { + t.Error("a session share must not read as authority over an instance") + } + return nil, nil + }, + ) + if err != nil { + t.Fatalf("interceptor error = %v", err) + } + }) + t.Run("invalid share token is denied", func(t *testing.T) { store := &testShareStore{err: dbpkg.ErrNotFound} ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("x-share-token", "missing")) diff --git a/go/core/internal/grpcserver/policy.go b/go/core/internal/grpcserver/policy.go index da9f67a65..c69d2a7e2 100644 --- a/go/core/internal/grpcserver/policy.go +++ b/go/core/internal/grpcserver/policy.go @@ -81,10 +81,26 @@ func DefaultMethodPolicies() MethodPolicies { grpc_health_v1.Health_Watch_FullMethodName: AccessPublic, "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo": AccessPublic, "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo": AccessPublic, + apiv1alpha1.SystemService_GetSubstrateSummary_FullMethodName: AccessRead, + apiv1alpha1.SystemService_ListSubstrateActors_FullMethodName: AccessRead, + apiv1alpha1.SystemService_ListSubstrateWorkers_FullMethodName: AccessRead, + apiv1alpha1.AgentTemplateService_ListAgentTemplates_FullMethodName: AccessRead, + apiv1alpha1.AgentTemplateService_GetAgentTemplate_FullMethodName: AccessRead, + apiv1alpha1.AgentTemplateService_CreateAgentTemplate_FullMethodName: AccessCreate, + apiv1alpha1.AgentTemplateService_UpdateAgentTemplate_FullMethodName: AccessUpdate, + apiv1alpha1.AgentTemplateService_DeleteAgentTemplate_FullMethodName: AccessDelete, + apiv1alpha1.HarnessService_ListHarnesses_FullMethodName: AccessRead, + apiv1alpha1.HarnessService_GetHarness_FullMethodName: AccessRead, + apiv1alpha1.HarnessService_CreateHarness_FullMethodName: AccessCreate, + apiv1alpha1.HarnessService_UpdateHarness_FullMethodName: AccessUpdate, + apiv1alpha1.HarnessService_DeleteHarness_FullMethodName: AccessDelete, } policies[apiv1alpha1.AgentInstanceService_CreateAgentInstance_FullMethodName] = AccessCreate policies[apiv1alpha1.AgentInstanceService_GetAgentInstance_FullMethodName] = AccessRead policies[apiv1alpha1.AgentInstanceService_ListAgentInstances_FullMethodName] = AccessRead + // A rename is the only write on this service that is not a lifecycle + // operation, and it must not inherit the read mode its neighbours carry. + policies[apiv1alpha1.AgentInstanceService_RenameAgentInstance_FullMethodName] = AccessUpdate policies[apiv1alpha1.AgentInstanceService_SuspendAgentInstance_FullMethodName] = AccessUpdate policies[apiv1alpha1.AgentInstanceService_ResumeAgentInstance_FullMethodName] = AccessUpdate policies[apiv1alpha1.AgentInstanceService_DeleteAgentInstance_FullMethodName] = AccessDelete diff --git a/go/core/internal/grpcserver/policy_test.go b/go/core/internal/grpcserver/policy_test.go new file mode 100644 index 000000000..08a87657a --- /dev/null +++ b/go/core/internal/grpcserver/policy_test.go @@ -0,0 +1,88 @@ +package grpcserver + +import ( + "testing" + + dbpkg "github.com/kagent-dev/kagent/go/api/database" + apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" + pkgauth "github.com/kagent-dev/kagent/go/core/pkg/auth" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// TestAgentInstanceServicePoliciesMatchTheirEffect pins the access mode of every +// method on the service. A wrong mode here fails silently in one of two ways: a +// write classified as a read is reachable through a read-only share link, and a +// read classified as a write is refused for a legitimate caller. Neither shows +// up as an error anywhere near the policy table. +func TestAgentInstanceServicePoliciesMatchTheirEffect(t *testing.T) { + policies := DefaultMethodPolicies() + for _, test := range []struct { + name string + method string + want AccessMode + }{ + {name: "create", method: apiv1alpha1.AgentInstanceService_CreateAgentInstance_FullMethodName, want: AccessCreate}, + {name: "get", method: apiv1alpha1.AgentInstanceService_GetAgentInstance_FullMethodName, want: AccessRead}, + {name: "list", method: apiv1alpha1.AgentInstanceService_ListAgentInstances_FullMethodName, want: AccessRead}, + {name: "rename is a write", method: apiv1alpha1.AgentInstanceService_RenameAgentInstance_FullMethodName, want: AccessUpdate}, + {name: "suspend", method: apiv1alpha1.AgentInstanceService_SuspendAgentInstance_FullMethodName, want: AccessUpdate}, + {name: "resume", method: apiv1alpha1.AgentInstanceService_ResumeAgentInstance_FullMethodName, want: AccessUpdate}, + {name: "delete", method: apiv1alpha1.AgentInstanceService_DeleteAgentInstance_FullMethodName, want: AccessDelete}, + {name: "create share", method: apiv1alpha1.AgentInstanceService_CreateAgentInstanceShare_FullMethodName, want: AccessCreate}, + {name: "list shares", method: apiv1alpha1.AgentInstanceService_ListAgentInstanceShares_FullMethodName, want: AccessRead}, + {name: "revoke share", method: apiv1alpha1.AgentInstanceService_RevokeAgentInstanceShare_FullMethodName, want: AccessDelete}, + } { + t.Run(test.name, func(t *testing.T) { + got, ok := policies[test.method] + if !ok { + t.Fatalf("%s has no policy; an unconfigured method is denied outright", test.method) + } + if got != test.want { + t.Fatalf("%s policy = %q, want %q", test.method, got, test.want) + } + }) + } +} + +// TestReadOnlyShareCannotRenameAConversation is the property the policy entry +// exists for, measured through the interceptor rather than read off the table: a +// read-only share link may open a conversation and must not be able to retitle +// it for everyone who holds the link. +func TestReadOnlyShareCannotRenameAConversation(t *testing.T) { + session := &testSession{principal: pkgauth.Principal{User: pkgauth.User{ID: "visitor"}}} + for _, test := range []struct { + name string + permission string + method string + wantCode codes.Code + }{ + { + name: "read-only share may read", permission: "READ_ONLY", + method: apiv1alpha1.AgentInstanceService_GetAgentInstance_FullMethodName, wantCode: codes.OK, + }, + { + name: "read-only share may not rename", permission: "READ_ONLY", + method: apiv1alpha1.AgentInstanceService_RenameAgentInstance_FullMethodName, wantCode: codes.PermissionDenied, + }, + { + name: "read-write share may rename", permission: "READ_WRITE", + method: apiv1alpha1.AgentInstanceService_RenameAgentInstance_FullMethodName, wantCode: codes.OK, + }, + } { + t.Run(test.name, func(t *testing.T) { + shareStore := &testShareStore{ + err: dbpkg.ErrNotFound, + instanceShare: &dbpkg.AgentInstanceShare{ + ID: "share-1", InstanceID: "instance-1", Permission: test.permission, OwnerUserID: "owner", + }, + } + ctx := metadata.NewIncomingContext(t.Context(), metadata.Pairs("x-share-token", "token")) + _, err := authenticate(ctx, test.method, &testAuthenticator{session: session}, shareStore, DefaultMethodPolicies()) + if got := status.Code(err); got != test.wantCode { + t.Fatalf("authenticate(%s) code = %v (%v), want %v", test.method, got, err, test.wantCode) + } + }) + } +} diff --git a/go/core/internal/grpcserver/server.go b/go/core/internal/grpcserver/server.go index 530acfe0e..429acc27c 100644 --- a/go/core/internal/grpcserver/server.go +++ b/go/core/internal/grpcserver/server.go @@ -15,7 +15,9 @@ import ( dbpkg "github.com/kagent-dev/kagent/go/api/database" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" agentservice "github.com/kagent-dev/kagent/go/core/internal/service/agent" + agenttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/agenttemplate" feedbackservice "github.com/kagent-dev/kagent/go/core/internal/service/feedback" + harnessservice "github.com/kagent-dev/kagent/go/core/internal/service/harness" memoryservice "github.com/kagent-dev/kagent/go/core/internal/service/memory" modelservice "github.com/kagent-dev/kagent/go/core/internal/service/model" prompttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/prompttemplate" @@ -52,6 +54,8 @@ type Config struct { ShareStore ShareStore Registerer prometheus.Registerer AgentService *agentservice.Service + AgentTemplateService *agenttemplateservice.Service + HarnessService *harnessservice.Service ModelService *modelservice.Service ToolService *toolservice.Service PromptTemplateService *prompttemplateservice.Service @@ -132,6 +136,15 @@ func New(config Config) (*Server, error) { if config.AgentService != nil { apiv1alpha1.RegisterAgentServiceServer(grpcServer, newAgentServer(config.AgentService, config.MaxMessageBytes)) } + if config.AgentTemplateService != nil { + apiv1alpha1.RegisterAgentTemplateServiceServer(grpcServer, newAgentTemplateServer(config.AgentTemplateService, config.MaxMessageBytes)) + } + // Registered separately from AgentService on purpose: this serves the + // Harness CRD that AgentInstance pairs with an AgentTemplate, not the + // AgentHarness CRD that AgentService's *AgentHarness RPCs serve. + if config.HarnessService != nil { + apiv1alpha1.RegisterHarnessServiceServer(grpcServer, newHarnessServer(config.HarnessService, config.MaxMessageBytes)) + } if config.ModelService != nil { apiv1alpha1.RegisterModelServiceServer(grpcServer, newModelServer(config.ModelService, config.MaxMessageBytes)) } @@ -176,6 +189,12 @@ func New(config Config) (*Server, error) { type ShareStore interface { GetSessionShareByToken(context.Context, string) (*dbpkg.SessionShare, error) RecordShareAccess(context.Context, string, int64) error + // GetAgentInstanceShareByTokenHash resolves an AgentInstance share. + // + // Two kinds of share reach the same header, because a share link carries one + // token and the reader opening it cannot know which kind it is. So the + // interceptor tries both — see `authenticate`. + GetAgentInstanceShareByTokenHash(context.Context, []byte) (*dbpkg.AgentInstanceShare, error) } func (s *Server) Start(ctx context.Context) error { diff --git a/go/core/internal/grpcserver/system.go b/go/core/internal/grpcserver/system.go index 32c42222a..2667ab909 100644 --- a/go/core/internal/grpcserver/system.go +++ b/go/core/internal/grpcserver/system.go @@ -7,6 +7,7 @@ import ( "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" systemservice "github.com/kagent-dev/kagent/go/core/internal/service/system" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" ) type systemServer struct { @@ -54,6 +55,203 @@ func (s *systemServer) ListNamespaces(ctx context.Context, _ *apiv1alpha1.ListNa return &apiv1alpha1.ListNamespacesResponse{Namespaces: namespaces}, nil } +func (s *systemServer) GetSubstrateSummary(ctx context.Context, request *apiv1alpha1.GetSubstrateSummaryRequest) (*apiv1alpha1.GetSubstrateSummaryResponse, error) { + result, err := s.service.GetSubstrateSummary(ctx, request.GetNamespace()) + if err != nil { + return nil, err + } + response := &apiv1alpha1.GetSubstrateSummaryResponse{ + Enabled: result.Enabled, + AteApiError: result.ATEAPIError, + WorkerPools: make([]*apiv1alpha1.SubstrateWorkerPool, 0, len(result.WorkerPools)), + ActorTemplates: make([]*apiv1alpha1.SubstrateActorTemplate, 0, len(result.ActorTemplates)), + ActorCount: result.ActorCount, + WorkerCount: result.WorkerCount, + RunningActorCount: result.RunningActorCount, + BusyWorkerCount: result.BusyWorkerCount, + ActorStatusCounts: make([]*apiv1alpha1.SubstrateStatusCount, 0, len(result.ActorStatusCounts)), + ComputedAt: timestamppb.New(result.ComputedAt), + } + for _, workerPool := range result.WorkerPools { + response.WorkerPools = append(response.WorkerPools, workerPoolToProto(workerPool)) + } + for _, actorTemplate := range result.ActorTemplates { + response.ActorTemplates = append(response.ActorTemplates, actorTemplateToProto(actorTemplate)) + } + for _, statusCount := range result.ActorStatusCounts { + response.ActorStatusCounts = append(response.ActorStatusCounts, &apiv1alpha1.SubstrateStatusCount{ + Status: statusCount.Status, + Count: statusCount.Count, + }) + } + return response, nil +} + +/* + * The sort enums, mapped both ways. + * + * Written out rather than derived, and keyed by the generated enum so a member + * added to the proto fails the build here instead of being served as a zero. The + * response reports the order that was *applied*, which is why the outbound + * direction exists at all: a caller should be able to say how its rows are sorted + * rather than assume its request was honoured. + */ +var actorSortFieldFromProto = map[apiv1alpha1.SubstrateActorSortField]systemservice.ActorSortField{ + apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED: systemservice.ActorSortDefault, + apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_STATUS: systemservice.ActorSortStatus, + apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID: systemservice.ActorSortID, + apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE: systemservice.ActorSortTemplate, + apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD: systemservice.ActorSortWorker, +} + +var actorSortFieldToProto = map[systemservice.ActorSortField]apiv1alpha1.SubstrateActorSortField{ + systemservice.ActorSortDefault: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED, + systemservice.ActorSortStatus: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_STATUS, + systemservice.ActorSortID: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID, + systemservice.ActorSortTemplate: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE, + systemservice.ActorSortWorker: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD, +} + +var workerSortFieldFromProto = map[apiv1alpha1.SubstrateWorkerSortField]systemservice.WorkerSortField{ + apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED: systemservice.WorkerSortDefault, + apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POOL: systemservice.WorkerSortPool, + apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POD: systemservice.WorkerSortPod, + apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_ACTOR: systemservice.WorkerSortActor, +} + +var workerSortFieldToProto = map[systemservice.WorkerSortField]apiv1alpha1.SubstrateWorkerSortField{ + systemservice.WorkerSortDefault: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED, + systemservice.WorkerSortPool: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POOL, + systemservice.WorkerSortPod: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POD, + systemservice.WorkerSortActor: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_ACTOR, +} + +func sortOrderFromProto(order apiv1alpha1.SubstrateSortOrder) systemservice.SortOrder { + if order == apiv1alpha1.SubstrateSortOrder_SUBSTRATE_SORT_ORDER_DESCENDING { + return systemservice.SortDescending + } + return systemservice.SortAscending +} + +func sortOrderToProto(order systemservice.SortOrder) apiv1alpha1.SubstrateSortOrder { + if order == systemservice.SortDescending { + return apiv1alpha1.SubstrateSortOrder_SUBSTRATE_SORT_ORDER_DESCENDING + } + return apiv1alpha1.SubstrateSortOrder_SUBSTRATE_SORT_ORDER_ASCENDING +} + +func (s *systemServer) ListSubstrateActors(ctx context.Context, request *apiv1alpha1.ListSubstrateActorsRequest) (*apiv1alpha1.ListSubstrateActorsResponse, error) { + result, err := s.service.ListSubstrateActors(ctx, systemservice.ListActorsRequest{ + Namespace: request.GetNamespace(), + Filter: request.GetFilter(), + Limit: request.GetPage().GetLimit(), + PageToken: request.GetPage().GetPageToken(), + // A field this build does not know maps to the default order rather than + // being refused: a newer caller asking for a column added later gets a + // coherent page, and the response says which order it actually got. + SortField: actorSortFieldFromProto[request.GetSortField()], + SortOrder: sortOrderFromProto(request.GetSortOrder()), + }) + if err != nil { + return nil, err + } + response := &apiv1alpha1.ListSubstrateActorsResponse{ + Actors: make([]*apiv1alpha1.SubstrateActor, 0, len(result.Actors)), + Page: &apiv1alpha1.PageResponse{NextPageToken: result.NextPageToken}, + TotalSize: result.TotalSize, + AppliedSortField: actorSortFieldToProto[result.SortField], + AppliedSortOrder: sortOrderToProto(result.SortOrder), + ComputedAt: timestamppb.New(result.ComputedAt), + } + for _, actor := range result.Actors { + response.Actors = append(response.Actors, actorToProto(actor)) + } + return response, nil +} + +func (s *systemServer) ListSubstrateWorkers(ctx context.Context, request *apiv1alpha1.ListSubstrateWorkersRequest) (*apiv1alpha1.ListSubstrateWorkersResponse, error) { + result, err := s.service.ListSubstrateWorkers(ctx, systemservice.ListWorkersRequest{ + Namespace: request.GetNamespace(), + Filter: request.GetFilter(), + Limit: request.GetPage().GetLimit(), + PageToken: request.GetPage().GetPageToken(), + SortField: workerSortFieldFromProto[request.GetSortField()], + SortOrder: sortOrderFromProto(request.GetSortOrder()), + }) + if err != nil { + return nil, err + } + response := &apiv1alpha1.ListSubstrateWorkersResponse{ + Workers: make([]*apiv1alpha1.SubstrateWorker, 0, len(result.Workers)), + Page: &apiv1alpha1.PageResponse{NextPageToken: result.NextPageToken}, + TotalSize: result.TotalSize, + AppliedSortField: workerSortFieldToProto[result.SortField], + AppliedSortOrder: sortOrderToProto(result.SortOrder), + ComputedAt: timestamppb.New(result.ComputedAt), + } + for _, worker := range result.Workers { + response.Workers = append(response.Workers, workerToProto(worker)) + } + return response, nil +} + +// The four row conversions, shared by GetSubstrateStatus and the paged reads +// that replaced it, so the same record cannot arrive shaped differently +// depending on which RPC a caller used. + +func workerPoolToProto(workerPool systemservice.SubstrateWorkerPool) *apiv1alpha1.SubstrateWorkerPool { + return &apiv1alpha1.SubstrateWorkerPool{ + Namespace: workerPool.Namespace, + Name: workerPool.Name, + Replicas: workerPool.Replicas, + AteomImage: workerPool.AteomImage, + } +} + +func actorTemplateToProto(actorTemplate systemservice.SubstrateActorTemplate) *apiv1alpha1.SubstrateActorTemplate { + return &apiv1alpha1.SubstrateActorTemplate{ + Namespace: actorTemplate.Namespace, + Name: actorTemplate.Name, + Phase: actorTemplate.Phase, + GoldenActorId: actorTemplate.GoldenActorID, + GoldenSnapshot: actorTemplate.GoldenSnapshot, + SandboxClass: actorTemplate.SandboxClass, + WorkerSelector: actorTemplate.WorkerSelector, + HarnessName: actorTemplate.HarnessName, + ManagedByKagent: actorTemplate.ManagedByKagent, + } +} + +func actorToProto(actor systemservice.SubstrateActor) *apiv1alpha1.SubstrateActor { + return &apiv1alpha1.SubstrateActor{ + ActorId: actor.ActorID, + Atespace: actor.Atespace, + Status: actor.Status, + ActorTemplateNamespace: actor.ActorTemplateNamespace, + ActorTemplateName: actor.ActorTemplateName, + AteomPodNamespace: actor.AteomPodNamespace, + AteomPodName: actor.AteomPodName, + AteomPodIp: actor.AteomPodIP, + LatestSnapshot: actor.LatestSnapshot, + WorkerPoolName: actor.WorkerPoolName, + InProgressSnapshot: actor.InProgressSnapshot, + Version: actor.Version, + } +} + +func workerToProto(worker systemservice.SubstrateWorker) *apiv1alpha1.SubstrateWorker { + return &apiv1alpha1.SubstrateWorker{ + WorkerNamespace: worker.WorkerNamespace, + WorkerPool: worker.WorkerPool, + WorkerPod: worker.WorkerPod, + ActorNamespace: worker.ActorNamespace, + ActorTemplate: worker.ActorTemplate, + ActorId: worker.ActorID, + Ip: worker.IP, + Version: worker.Version, + } +} + func (s *systemServer) GetSubstrateStatus(ctx context.Context, request *apiv1alpha1.GetSubstrateStatusRequest) (*apiv1alpha1.GetSubstrateStatusResponse, error) { result, err := s.service.GetSubstrateStatus(ctx, request.GetNamespace()) if err != nil { @@ -68,53 +266,16 @@ func (s *systemServer) GetSubstrateStatus(ctx context.Context, request *apiv1alp Workers: make([]*apiv1alpha1.SubstrateWorker, 0, len(result.Workers)), } for _, workerPool := range result.WorkerPools { - response.WorkerPools = append(response.WorkerPools, &apiv1alpha1.SubstrateWorkerPool{ - Namespace: workerPool.Namespace, - Name: workerPool.Name, - Replicas: workerPool.Replicas, - AteomImage: workerPool.AteomImage, - }) + response.WorkerPools = append(response.WorkerPools, workerPoolToProto(workerPool)) } for _, actorTemplate := range result.ActorTemplates { - response.ActorTemplates = append(response.ActorTemplates, &apiv1alpha1.SubstrateActorTemplate{ - Namespace: actorTemplate.Namespace, - Name: actorTemplate.Name, - Phase: actorTemplate.Phase, - GoldenActorId: actorTemplate.GoldenActorID, - GoldenSnapshot: actorTemplate.GoldenSnapshot, - SandboxClass: actorTemplate.SandboxClass, - WorkerSelector: actorTemplate.WorkerSelector, - HarnessName: actorTemplate.HarnessName, - ManagedByKagent: actorTemplate.ManagedByKagent, - }) + response.ActorTemplates = append(response.ActorTemplates, actorTemplateToProto(actorTemplate)) } for _, actor := range result.Actors { - response.Actors = append(response.Actors, &apiv1alpha1.SubstrateActor{ - ActorId: actor.ActorID, - Atespace: actor.Atespace, - Status: actor.Status, - ActorTemplateNamespace: actor.ActorTemplateNamespace, - ActorTemplateName: actor.ActorTemplateName, - AteomPodNamespace: actor.AteomPodNamespace, - AteomPodName: actor.AteomPodName, - AteomPodIp: actor.AteomPodIP, - LatestSnapshot: actor.LatestSnapshot, - WorkerPoolName: actor.WorkerPoolName, - InProgressSnapshot: actor.InProgressSnapshot, - Version: actor.Version, - }) + response.Actors = append(response.Actors, actorToProto(actor)) } for _, worker := range result.Workers { - response.Workers = append(response.Workers, &apiv1alpha1.SubstrateWorker{ - WorkerNamespace: worker.WorkerNamespace, - WorkerPool: worker.WorkerPool, - WorkerPod: worker.WorkerPod, - ActorNamespace: worker.ActorNamespace, - ActorTemplate: worker.ActorTemplate, - ActorId: worker.ActorID, - Ip: worker.IP, - Version: worker.Version, - }) + response.Workers = append(response.Workers, workerToProto(worker)) } return response, nil } diff --git a/go/core/internal/httpserver/server.go b/go/core/internal/httpserver/server.go index ac894d77b..f00ac392b 100644 --- a/go/core/internal/httpserver/server.go +++ b/go/core/internal/httpserver/server.go @@ -27,6 +27,15 @@ type ServerConfig struct { KubeClient ctrl_client.Client DbClient dbpkg.Client Authenticator auth.AuthProvider + + // GrpcWebRouter composes this server's handler so that gRPC-Web calls reach the + // gRPC services and everything else reaches the router. Supplied by the gRPC + // server (see grpcserver.Server.WebHandlerOr), which owns the rule — there is + // more than one binary serving HTTP beside that server, and a second copy of + // the rule would drift. + // + // Optional: left nil, this server behaves exactly as it did before one existed. + GrpcWebRouter func(next http.Handler) http.Handler } // HTTPServer is the structure that manages the HTTP server @@ -62,7 +71,7 @@ func (s *HTTPServer) Start(ctx context.Context) error { // and W3C TraceContext propagation on every incoming request. s.httpServer = &http.Server{ Addr: s.config.BindAddr, - Handler: otelhttp.NewHandler(s.router, "http.server", + Handler: otelhttp.NewHandler(s.withGrpcWeb(s.router), "http.server", otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { return r.Method + " " + r.URL.Path }), @@ -194,3 +203,18 @@ func wsAuthQueryMiddleware(next http.Handler) http.Handler { func adaptHealthHandler(h func(http.ResponseWriter, *http.Request)) http.HandlerFunc { return h } + +// withGrpcWeb lets the gRPC server route gRPC-Web calls ahead of this router. +// +// The split happens outside the router's middleware chain on purpose. That chain +// authenticates, rewrites content types and maps errors for the REST-shaped +// handlers, none of which a gRPC-Web frame survives — and it does not need any of +// it, because the gRPC server authenticates in its own interceptors. Routing these +// past the chain rather than through it keeps one protocol from being handled by +// the other's conventions. +func (s *HTTPServer) withGrpcWeb(next http.Handler) http.Handler { + if s.config.GrpcWebRouter == nil { + return next + } + return s.config.GrpcWebRouter(next) +} diff --git a/go/core/internal/httpserver/server_grpcweb_test.go b/go/core/internal/httpserver/server_grpcweb_test.go new file mode 100644 index 000000000..9a1d900fa --- /dev/null +++ b/go/core/internal/httpserver/server_grpcweb_test.go @@ -0,0 +1,76 @@ +package httpserver + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// The rule about *which* requests are gRPC-Web, and about the /api prefix, belongs +// to the gRPC server — `grpcserver.Server.WebHandlerOr` — because more than one +// binary serves HTTP beside that server and a second copy of the rule would drift. +// What is this server's business, and all these tests pin, is that a supplied +// router is applied around its own handler, and that leaving it unset changes +// nothing. + +func TestWithGrpcWebAppliesTheSuppliedRouter(t *testing.T) { + routerCalls := 0 + innerCalls := 0 + + // Stands in for WebHandlerOr: records that it was given the router as `next`, + // and answers without delegating, the way a real gRPC-Web call would. + router := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + routerCalls++ + if next == nil { + t.Error("the router was not given the server's own handler as next") + } + w.WriteHeader(http.StatusOK) + }) + } + + s := &HTTPServer{config: ServerConfig{GrpcWebRouter: router}} + handler := s.withGrpcWeb(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { innerCalls++ })) + + handler.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest(http.MethodPost, "/api/kagent.api.v1alpha1.AgentService/ListAgents", nil), + ) + + if routerCalls != 1 { + t.Errorf("router calls = %d, want 1", routerCalls) + } + // The router decides whether the inner handler runs. This one does not delegate, + // so the router genuinely sits in front rather than beside. + if innerCalls != 0 { + t.Errorf("inner handler calls = %d, want 0 — the router should be outermost", innerCalls) + } +} + +// A router that delegates must reach the server's own handler, or ordinary API +// traffic would never be served. +func TestWithGrpcWebRouterCanDelegate(t *testing.T) { + innerCalls := 0 + passThrough := func(next http.Handler) http.Handler { return next } + + s := &HTTPServer{config: ServerConfig{GrpcWebRouter: passThrough}} + s.withGrpcWeb(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { innerCalls++ })). + ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/a2a/default/x", nil)) + + if innerCalls != 1 { + t.Errorf("inner handler calls = %d, want 1", innerCalls) + } +} + +// A server built without a router must behave exactly as it did before one existed. +func TestWithGrpcWebIsInertWhenUnset(t *testing.T) { + called := 0 + s := &HTTPServer{config: ServerConfig{}} + + s.withGrpcWeb(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called++ })). + ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/anything", nil)) + + if called != 1 { + t.Fatalf("handler calls = %d, want 1", called) + } +} diff --git a/go/core/internal/service/agenttemplate/service.go b/go/core/internal/service/agenttemplate/service.go new file mode 100644 index 000000000..864872998 --- /dev/null +++ b/go/core/internal/service/agenttemplate/service.go @@ -0,0 +1,180 @@ +// Package agenttemplate serves CRUD over the kagent.dev/v1alpha3 AgentTemplate +// CRD, the portable-behavior half of the (Harness, AgentTemplate) pair that +// AgentInstanceService.CreateAgentInstance names. +package agenttemplate + +import ( + "cmp" + "context" + "fmt" + "slices" + + "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "github.com/kagent-dev/kagent/go/core/pkg/auth" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// resourceType is the authorizer's name for this kind. It is deliberately not +// "Agent": an AgentTemplate is authored and shared like a template, and the +// AgentInstances created from it are what carry per-agent authorization. +const resourceType = "AgentTemplate" + +type Service struct { + kubeClient client.Client + authorizer auth.Authorizer +} + +func NewService(kubeClient client.Client, authorizer auth.Authorizer) *Service { + return &Service{kubeClient: kubeClient, authorizer: authorizer} +} + +func (s *Service) List(ctx context.Context, namespace string) ([]v1alpha3.AgentTemplate, error) { + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType}); err != nil { + return nil, err + } + if namespace == "" { + return nil, serviceerrors.NewInvalidArgument("namespace is required", nil) + } + + list := &v1alpha3.AgentTemplateList{} + if err := s.kubeClient.List(ctx, list, client.InNamespace(namespace)); err != nil { + return nil, serviceerrors.NewInternal("Failed to list AgentTemplates", err) + } + slices.SortFunc(list.Items, func(left, right v1alpha3.AgentTemplate) int { + return cmp.Compare(left.Name, right.Name) + }) + return list.Items, nil +} + +func (s *Service) Get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.AgentTemplate, error) { + if err := validateRef(ref); err != nil { + return nil, err + } + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return nil, err + } + return s.get(ctx, ref) +} + +func (s *Service) Create(ctx context.Context, template *v1alpha3.AgentTemplate) (*v1alpha3.AgentTemplate, error) { + if template == nil { + return nil, serviceerrors.NewInvalidArgument("AgentTemplate resource is required", nil) + } + ref := types.NamespacedName{Namespace: template.Namespace, Name: template.Name} + if err := validateNewRef(ref); err != nil { + return nil, err + } + if err := s.authorize(ctx, auth.VerbCreate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return nil, err + } + + // Status is controller-owned, so a caller that round-trips a Get into a + // Create cannot assert readiness it has not earned. + created := template.DeepCopy() + created.Status = v1alpha3.AgentTemplateStatus{} + if err := s.kubeClient.Create(ctx, created); err != nil { + if apierrors.IsAlreadyExists(err) { + return nil, serviceerrors.NewAlreadyExists("An AgentTemplate with this name already exists in the namespace", err) + } + if apierrors.IsInvalid(err) { + return nil, serviceerrors.NewInvalidArgument("Invalid AgentTemplate", err) + } + return nil, serviceerrors.NewInternal("Failed to create AgentTemplate", err) + } + return created, nil +} + +func (s *Service) Update(ctx context.Context, ref types.NamespacedName, template *v1alpha3.AgentTemplate) (*v1alpha3.AgentTemplate, error) { + if template == nil { + return nil, serviceerrors.NewInvalidArgument("AgentTemplate resource is required", nil) + } + if err := validateRef(ref); err != nil { + return nil, err + } + if err := s.authorize(ctx, auth.VerbUpdate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return nil, err + } + + // The spec is applied onto the stored object rather than the incoming one + // being written wholesale: that keeps the caller's stale resourceVersion, + // labels and controller-written status from silently overwriting the live + // object, so an update is a spec change and nothing else. + existing, err := s.get(ctx, ref) + if err != nil { + return nil, err + } + existing.Spec = *template.Spec.DeepCopy() + if err := s.kubeClient.Update(ctx, existing); err != nil { + if apierrors.IsInvalid(err) { + return nil, serviceerrors.NewInvalidArgument("Invalid AgentTemplate", err) + } + return nil, serviceerrors.NewInternal("Failed to update AgentTemplate", err) + } + return existing, nil +} + +func (s *Service) Delete(ctx context.Context, ref types.NamespacedName) error { + if err := validateRef(ref); err != nil { + return err + } + if err := s.authorize(ctx, auth.VerbDelete, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return err + } + + existing, err := s.get(ctx, ref) + if err != nil { + return err + } + if err := s.kubeClient.Delete(ctx, existing); err != nil { + return serviceerrors.NewInternal("Failed to delete AgentTemplate", err) + } + return nil +} + +func (s *Service) get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.AgentTemplate, error) { + template := &v1alpha3.AgentTemplate{} + if err := s.kubeClient.Get(ctx, ref, template); err != nil { + if apierrors.IsNotFound(err) { + return nil, serviceerrors.NewNotFound("AgentTemplate not found", err) + } + return nil, serviceerrors.NewInternal("Failed to get AgentTemplate", err) + } + return template, nil +} + +func (s *Service) authorize(ctx context.Context, verb auth.Verb, resource auth.Resource) error { + session, ok := auth.AuthSessionFrom(ctx) + if !ok || session == nil { + return serviceerrors.NewUnauthenticated("Failed to get authenticated principal", fmt.Errorf("no session found")) + } + if err := s.authorizer.Check(ctx, session.Principal(), verb, resource); err != nil { + return serviceerrors.NewPermissionDenied("Not authorized", err) + } + return nil +} + +func validateRef(ref types.NamespacedName) error { + if ref.Namespace == "" || ref.Name == "" { + return serviceerrors.NewInvalidArgument("AgentTemplate namespace and name are required", nil) + } + return nil +} + +// validateNewRef additionally rejects names the apiserver would reject, so a +// create fails with an actionable message rather than a wrapped 422. +func validateNewRef(ref types.NamespacedName) error { + if err := validateRef(ref); err != nil { + return err + } + if len(utilvalidation.IsDNS1123Subdomain(ref.Namespace)) > 0 { + return serviceerrors.NewInvalidArgument("namespace must be a valid DNS subdomain", nil) + } + if len(utilvalidation.IsDNS1123Subdomain(ref.Name)) > 0 { + return serviceerrors.NewInvalidArgument("name must be a valid DNS subdomain", nil) + } + return nil +} diff --git a/go/core/internal/service/agenttemplate/service_test.go b/go/core/internal/service/agenttemplate/service_test.go new file mode 100644 index 000000000..2caddba50 --- /dev/null +++ b/go/core/internal/service/agenttemplate/service_test.go @@ -0,0 +1,252 @@ +package agenttemplate_test + +import ( + "context" + "errors" + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha3" + authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" + "github.com/kagent-dev/kagent/go/core/internal/service/agenttemplate" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + pkgauth "github.com/kagent-dev/kagent/go/core/pkg/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +type denyAuthorizer struct{} + +func (denyAuthorizer) Check(context.Context, pkgauth.Principal, pkgauth.Verb, pkgauth.Resource) error { + return errors.New("denied") +} + +func template(namespace, name, modelConfig string) *v1alpha3.AgentTemplate { + return &v1alpha3.AgentTemplate{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, + Spec: v1alpha3.AgentTemplateSpec{ + ModelConfig: v1alpha3.AgentTemplateLocalReference{Name: modelConfig}, + }, + } +} + +func newService(t *testing.T, authorizer pkgauth.Authorizer, objects ...ctrlclient.Object) (*agenttemplate.Service, context.Context) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, v1alpha3.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + ctx := pkgauth.AuthSessionTo(t.Context(), &authimpl.SimpleSession{ + P: pkgauth.Principal{User: pkgauth.User{ID: "template-user"}}, + }) + return agenttemplate.NewService(kubeClient, authorizer), ctx +} + +func TestList(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}, + template("team", "z-last", "gpt"), + template("team", "a-first", "claude"), + template("other", "elsewhere", "gpt"), + ) + + result, err := service.List(ctx, "team") + require.NoError(t, err) + names := make([]string, 0, len(result)) + for _, item := range result { + names = append(names, item.Name) + } + assert.Equal(t, []string{"a-first", "z-last"}, names) + + _, err = service.List(ctx, "") + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) +} + +func TestCRUD(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}) + + created, err := service.Create(ctx, template("team", "researcher", "gpt")) + require.NoError(t, err) + assert.Equal(t, "gpt", created.Spec.ModelConfig.Name) + + _, err = service.Create(ctx, template("team", "researcher", "gpt")) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeAlreadyExists), err) + + ref := types.NamespacedName{Namespace: "team", Name: "researcher"} + fetched, err := service.Get(ctx, ref) + require.NoError(t, err) + assert.Equal(t, "gpt", fetched.Spec.ModelConfig.Name) + + updated, err := service.Update(ctx, ref, template("team", "researcher", "claude")) + require.NoError(t, err) + assert.Equal(t, "claude", updated.Spec.ModelConfig.Name) + + require.NoError(t, service.Delete(ctx, ref)) + _, err = service.Get(ctx, ref) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeNotFound), err) +} + +// Update writes the incoming spec onto the stored object; controller-owned +// status must survive, or every edit would blank the admitting-harness list. +func TestUpdatePreservesStatus(t *testing.T) { + existing := template("team", "researcher", "gpt") + existing.Status = v1alpha3.AgentTemplateStatus{ + Harnesses: []v1alpha3.AgentTemplateHarnessStatus{{Harness: "shared", DesiredRevision: "rev-1"}}, + } + service, ctx := newService(t, &authimpl.NoopAuthorizer{}, existing) + + updated, err := service.Update(ctx, + types.NamespacedName{Namespace: "team", Name: "researcher"}, + template("team", "researcher", "claude")) + require.NoError(t, err) + assert.Equal(t, "claude", updated.Spec.ModelConfig.Name) + require.Len(t, updated.Status.Harnesses, 1) + assert.Equal(t, "shared", updated.Status.Harnesses[0].Harness) +} + +// Create must not let a caller seed controller-owned status by round-tripping +// a Get back into a Create. +func TestCreateDropsStatus(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}) + incoming := template("team", "researcher", "gpt") + incoming.Status = v1alpha3.AgentTemplateStatus{ + Harnesses: []v1alpha3.AgentTemplateHarnessStatus{{Harness: "forged", DesiredRevision: "rev-1"}}, + } + + created, err := service.Create(ctx, incoming) + require.NoError(t, err) + assert.Empty(t, created.Status.Harnesses) +} + +func TestInvalidArgumentsAndNotFound(t *testing.T) { + missing := types.NamespacedName{Namespace: "team", Name: "absent"} + tests := []struct { + name string + call func(*agenttemplate.Service, context.Context) error + code serviceerrors.Code + }{ + { + name: "get without namespace", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Get(ctx, types.NamespacedName{Name: "researcher"}) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "get without name", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Get(ctx, types.NamespacedName{Namespace: "team"}) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "create nil resource", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Create(ctx, nil) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "create with name the apiserver would reject", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Create(ctx, template("team", "Not A Subdomain", "gpt")) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "create with namespace the apiserver would reject", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Create(ctx, template("Team", "researcher", "gpt")) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "update nil resource", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Update(ctx, missing, nil) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "get missing template", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Get(ctx, missing) + return err + }, + code: serviceerrors.CodeNotFound, + }, + { + name: "update missing template", + call: func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Update(ctx, missing, template("team", "absent", "gpt")) + return err + }, + code: serviceerrors.CodeNotFound, + }, + { + name: "delete missing template", + call: func(s *agenttemplate.Service, ctx context.Context) error { + return s.Delete(ctx, missing) + }, + code: serviceerrors.CodeNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}) + err := tt.call(service, ctx) + assert.True(t, serviceerrors.IsCode(err, tt.code), err) + }) + } +} + +func TestAuthorizationDenied(t *testing.T) { + ref := types.NamespacedName{Namespace: "team", Name: "researcher"} + tests := []struct { + name string + call func(*agenttemplate.Service, context.Context) error + }{ + {"list", func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.List(ctx, "team") + return err + }}, + {"get", func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Get(ctx, ref) + return err + }}, + {"create", func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Create(ctx, template("team", "researcher", "gpt")) + return err + }}, + {"update", func(s *agenttemplate.Service, ctx context.Context) error { + _, err := s.Update(ctx, ref, template("team", "researcher", "gpt")) + return err + }}, + {"delete", func(s *agenttemplate.Service, ctx context.Context) error { + return s.Delete(ctx, ref) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service, ctx := newService(t, denyAuthorizer{}, template("team", "researcher", "gpt")) + err := tt.call(service, ctx) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied), err) + }) + } +} + +func TestUnauthenticated(t *testing.T) { + service, _ := newService(t, &authimpl.NoopAuthorizer{}) + _, err := service.List(context.Background(), "team") + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeUnauthenticated), err) +} diff --git a/go/core/internal/service/harness/service.go b/go/core/internal/service/harness/service.go new file mode 100644 index 000000000..7c4a7b6bc --- /dev/null +++ b/go/core/internal/service/harness/service.go @@ -0,0 +1,186 @@ +// Package harness serves CRUD over the kagent.dev/v1alpha3 Harness CRD, the +// runtime and infrastructure half of the (Harness, AgentTemplate) pair that +// AgentInstanceService.CreateAgentInstance names. +// +// Harness is not AgentHarness. The agent service's GetAgentHarness / +// CreateAgentHarness / DeleteAgentHarness operate on the AgentHarness CRD — a +// single agent bound to an external ACP backend — and share nothing with this +// kind beyond a substring. A Harness is a reusable runtime that admits many +// AgentTemplates by label selector; go/core/v2/controller/collections.go pairs +// the two. Keeping them in separate packages is what stops the next reader +// from wiring one service into the other's RPCs. +package harness + +import ( + "cmp" + "context" + "fmt" + "slices" + + "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "github.com/kagent-dev/kagent/go/core/pkg/auth" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const resourceType = "Harness" + +type Service struct { + kubeClient client.Client + authorizer auth.Authorizer +} + +func NewService(kubeClient client.Client, authorizer auth.Authorizer) *Service { + return &Service{kubeClient: kubeClient, authorizer: authorizer} +} + +func (s *Service) List(ctx context.Context, namespace string) ([]v1alpha3.Harness, error) { + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType}); err != nil { + return nil, err + } + if namespace == "" { + return nil, serviceerrors.NewInvalidArgument("namespace is required", nil) + } + + list := &v1alpha3.HarnessList{} + if err := s.kubeClient.List(ctx, list, client.InNamespace(namespace)); err != nil { + return nil, serviceerrors.NewInternal("Failed to list Harnesses", err) + } + slices.SortFunc(list.Items, func(left, right v1alpha3.Harness) int { + return cmp.Compare(left.Name, right.Name) + }) + return list.Items, nil +} + +func (s *Service) Get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.Harness, error) { + if err := validateRef(ref); err != nil { + return nil, err + } + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return nil, err + } + return s.get(ctx, ref) +} + +func (s *Service) Create(ctx context.Context, incoming *v1alpha3.Harness) (*v1alpha3.Harness, error) { + if incoming == nil { + return nil, serviceerrors.NewInvalidArgument("Harness resource is required", nil) + } + ref := types.NamespacedName{Namespace: incoming.Namespace, Name: incoming.Name} + if err := validateNewRef(ref); err != nil { + return nil, err + } + if err := s.authorize(ctx, auth.VerbCreate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return nil, err + } + + // Status carries the controller's capability record, which is proven for a + // pinned adapter and image rather than declared. A caller must not be able + // to seed it by round-tripping a Get into a Create. + created := incoming.DeepCopy() + created.Status = v1alpha3.HarnessStatus{} + if err := s.kubeClient.Create(ctx, created); err != nil { + if apierrors.IsAlreadyExists(err) { + return nil, serviceerrors.NewAlreadyExists("A Harness with this name already exists in the namespace", err) + } + if apierrors.IsInvalid(err) { + return nil, serviceerrors.NewInvalidArgument("Invalid Harness", err) + } + return nil, serviceerrors.NewInternal("Failed to create Harness", err) + } + return created, nil +} + +func (s *Service) Update(ctx context.Context, ref types.NamespacedName, incoming *v1alpha3.Harness) (*v1alpha3.Harness, error) { + if incoming == nil { + return nil, serviceerrors.NewInvalidArgument("Harness resource is required", nil) + } + if err := validateRef(ref); err != nil { + return nil, err + } + if err := s.authorize(ctx, auth.VerbUpdate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return nil, err + } + + // The spec is applied onto the stored object rather than the incoming one + // being written wholesale: that keeps the caller's stale resourceVersion, + // labels and controller-written status from silently overwriting the live + // object, so an update is a spec change and nothing else. + existing, err := s.get(ctx, ref) + if err != nil { + return nil, err + } + existing.Spec = *incoming.Spec.DeepCopy() + if err := s.kubeClient.Update(ctx, existing); err != nil { + if apierrors.IsInvalid(err) { + return nil, serviceerrors.NewInvalidArgument("Invalid Harness", err) + } + return nil, serviceerrors.NewInternal("Failed to update Harness", err) + } + return existing, nil +} + +func (s *Service) Delete(ctx context.Context, ref types.NamespacedName) error { + if err := validateRef(ref); err != nil { + return err + } + if err := s.authorize(ctx, auth.VerbDelete, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { + return err + } + + existing, err := s.get(ctx, ref) + if err != nil { + return err + } + if err := s.kubeClient.Delete(ctx, existing); err != nil { + return serviceerrors.NewInternal("Failed to delete Harness", err) + } + return nil +} + +func (s *Service) get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.Harness, error) { + result := &v1alpha3.Harness{} + if err := s.kubeClient.Get(ctx, ref, result); err != nil { + if apierrors.IsNotFound(err) { + return nil, serviceerrors.NewNotFound("Harness not found", err) + } + return nil, serviceerrors.NewInternal("Failed to get Harness", err) + } + return result, nil +} + +func (s *Service) authorize(ctx context.Context, verb auth.Verb, resource auth.Resource) error { + session, ok := auth.AuthSessionFrom(ctx) + if !ok || session == nil { + return serviceerrors.NewUnauthenticated("Failed to get authenticated principal", fmt.Errorf("no session found")) + } + if err := s.authorizer.Check(ctx, session.Principal(), verb, resource); err != nil { + return serviceerrors.NewPermissionDenied("Not authorized", err) + } + return nil +} + +func validateRef(ref types.NamespacedName) error { + if ref.Namespace == "" || ref.Name == "" { + return serviceerrors.NewInvalidArgument("Harness namespace and name are required", nil) + } + return nil +} + +// validateNewRef additionally rejects names the apiserver would reject, so a +// create fails with an actionable message rather than a wrapped 422. +func validateNewRef(ref types.NamespacedName) error { + if err := validateRef(ref); err != nil { + return err + } + if len(utilvalidation.IsDNS1123Subdomain(ref.Namespace)) > 0 { + return serviceerrors.NewInvalidArgument("namespace must be a valid DNS subdomain", nil) + } + if len(utilvalidation.IsDNS1123Subdomain(ref.Name)) > 0 { + return serviceerrors.NewInvalidArgument("name must be a valid DNS subdomain", nil) + } + return nil +} diff --git a/go/core/internal/service/harness/service_test.go b/go/core/internal/service/harness/service_test.go new file mode 100644 index 000000000..e8f026c7c --- /dev/null +++ b/go/core/internal/service/harness/service_test.go @@ -0,0 +1,259 @@ +package harness_test + +import ( + "context" + "errors" + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha3" + authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" + "github.com/kagent-dev/kagent/go/core/internal/service/harness" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + pkgauth "github.com/kagent-dev/kagent/go/core/pkg/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const testImage = "example.test/runtime@sha256:0000000000000000000000000000000000000000000000000000000000000000" + +type denyAuthorizer struct{} + +func (denyAuthorizer) Check(context.Context, pkgauth.Principal, pkgauth.Verb, pkgauth.Resource) error { + return errors.New("denied") +} + +func fixture(namespace, name, workerPool string) *v1alpha3.Harness { + return &v1alpha3.Harness{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, + Spec: v1alpha3.HarnessSpec{ + Kagent: &v1alpha3.KagentHarness{}, + Workload: v1alpha3.HarnessWorkload{Image: testImage}, + Substrate: v1alpha3.HarnessSubstratePolicy{ + WorkerPoolRef: corev1.LocalObjectReference{Name: workerPool}, + SnapshotPolicy: v1alpha3.HarnessSnapshotPolicy{Location: "s3://snapshots"}, + }, + }, + } +} + +func newService(t *testing.T, authorizer pkgauth.Authorizer, objects ...ctrlclient.Object) (*harness.Service, context.Context) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, v1alpha3.AddToScheme(scheme)) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + ctx := pkgauth.AuthSessionTo(t.Context(), &authimpl.SimpleSession{ + P: pkgauth.Principal{User: pkgauth.User{ID: "harness-user"}}, + }) + return harness.NewService(kubeClient, authorizer), ctx +} + +func TestList(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}, + fixture("team", "z-last", "pool-a"), + fixture("team", "a-first", "pool-b"), + fixture("other", "elsewhere", "pool-c"), + ) + + result, err := service.List(ctx, "team") + require.NoError(t, err) + names := make([]string, 0, len(result)) + for _, item := range result { + names = append(names, item.Name) + } + assert.Equal(t, []string{"a-first", "z-last"}, names) + + _, err = service.List(ctx, "") + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) +} + +func TestCRUD(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}) + + created, err := service.Create(ctx, fixture("team", "shared", "pool-a")) + require.NoError(t, err) + assert.Equal(t, "pool-a", created.Spec.Substrate.WorkerPoolRef.Name) + + _, err = service.Create(ctx, fixture("team", "shared", "pool-a")) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeAlreadyExists), err) + + ref := types.NamespacedName{Namespace: "team", Name: "shared"} + fetched, err := service.Get(ctx, ref) + require.NoError(t, err) + assert.Equal(t, testImage, fetched.Spec.Workload.Image) + + updated, err := service.Update(ctx, ref, fixture("team", "shared", "pool-b")) + require.NoError(t, err) + assert.Equal(t, "pool-b", updated.Spec.Substrate.WorkerPoolRef.Name) + + require.NoError(t, service.Delete(ctx, ref)) + _, err = service.Get(ctx, ref) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeNotFound), err) +} + +// The capability record in status is proven by the controller for a pinned +// adapter and image, so an edit must not blank it and a create must not seed it. +func TestStatusIsControllerOwned(t *testing.T) { + existing := fixture("team", "shared", "pool-a") + existing.Status = v1alpha3.HarnessStatus{ + Capabilities: &v1alpha3.HarnessCapabilities{Version: "v1", Streaming: true}, + Conditions: []metav1.Condition{{ + Type: v1alpha3.HarnessConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: "Ready", + }}, + } + service, ctx := newService(t, &authimpl.NoopAuthorizer{}, existing) + + updated, err := service.Update(ctx, + types.NamespacedName{Namespace: "team", Name: "shared"}, + fixture("team", "shared", "pool-b")) + require.NoError(t, err) + assert.Equal(t, "pool-b", updated.Spec.Substrate.WorkerPoolRef.Name) + require.NotNil(t, updated.Status.Capabilities) + assert.Equal(t, "v1", updated.Status.Capabilities.Version) + + forged := fixture("team", "forged", "pool-a") + forged.Status = v1alpha3.HarnessStatus{ + Capabilities: &v1alpha3.HarnessCapabilities{Version: "forged", Streaming: true}, + } + created, err := service.Create(ctx, forged) + require.NoError(t, err) + assert.Nil(t, created.Status.Capabilities) +} + +func TestInvalidArgumentsAndNotFound(t *testing.T) { + missing := types.NamespacedName{Namespace: "team", Name: "absent"} + tests := []struct { + name string + call func(*harness.Service, context.Context) error + code serviceerrors.Code + }{ + { + name: "get without namespace", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Get(ctx, types.NamespacedName{Name: "shared"}) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "get without name", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Get(ctx, types.NamespacedName{Namespace: "team"}) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "create nil resource", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Create(ctx, nil) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "create with name the apiserver would reject", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Create(ctx, fixture("team", "Not A Subdomain", "pool-a")) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "create with namespace the apiserver would reject", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Create(ctx, fixture("Team", "shared", "pool-a")) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "update nil resource", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Update(ctx, missing, nil) + return err + }, + code: serviceerrors.CodeInvalidArgument, + }, + { + name: "get missing harness", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Get(ctx, missing) + return err + }, + code: serviceerrors.CodeNotFound, + }, + { + name: "update missing harness", + call: func(s *harness.Service, ctx context.Context) error { + _, err := s.Update(ctx, missing, fixture("team", "absent", "pool-a")) + return err + }, + code: serviceerrors.CodeNotFound, + }, + { + name: "delete missing harness", + call: func(s *harness.Service, ctx context.Context) error { + return s.Delete(ctx, missing) + }, + code: serviceerrors.CodeNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service, ctx := newService(t, &authimpl.NoopAuthorizer{}) + err := tt.call(service, ctx) + assert.True(t, serviceerrors.IsCode(err, tt.code), err) + }) + } +} + +func TestAuthorizationDenied(t *testing.T) { + ref := types.NamespacedName{Namespace: "team", Name: "shared"} + tests := []struct { + name string + call func(*harness.Service, context.Context) error + }{ + {"list", func(s *harness.Service, ctx context.Context) error { + _, err := s.List(ctx, "team") + return err + }}, + {"get", func(s *harness.Service, ctx context.Context) error { + _, err := s.Get(ctx, ref) + return err + }}, + {"create", func(s *harness.Service, ctx context.Context) error { + _, err := s.Create(ctx, fixture("team", "shared", "pool-a")) + return err + }}, + {"update", func(s *harness.Service, ctx context.Context) error { + _, err := s.Update(ctx, ref, fixture("team", "shared", "pool-a")) + return err + }}, + {"delete", func(s *harness.Service, ctx context.Context) error { + return s.Delete(ctx, ref) + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service, ctx := newService(t, denyAuthorizer{}, fixture("team", "shared", "pool-a")) + err := tt.call(service, ctx) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied), err) + }) + } +} + +func TestUnauthenticated(t *testing.T) { + service, _ := newService(t, &authimpl.NoopAuthorizer{}) + _, err := service.List(context.Background(), "team") + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeUnauthenticated), err) +} diff --git a/go/core/internal/service/system/service.go b/go/core/internal/service/system/service.go index 65e8773ca..1f4d770fe 100644 --- a/go/core/internal/service/system/service.go +++ b/go/core/internal/service/system/service.go @@ -16,7 +16,6 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - utilvalidation "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -30,6 +29,13 @@ type Version struct { type ATEClient interface { ListActors(context.Context, string) ([]*ateapipb.Actor, error) ListWorkers(context.Context) ([]*ateapipb.Worker, error) + // EachActorPage walks the actors a page at a time without accumulating them. + // + // Part of the interface rather than an optional cast, because the whole point + // is that the paged reads never hold the whole inventory — and an optional + // interface that silently does not engage would put that back without anything + // failing. See the substrate client's implementation for the numbers. + EachActorPage(ctx context.Context, atespace string, visit func([]*ateapipb.Actor) error) error } type Service struct { @@ -37,6 +43,9 @@ type Service struct { observedNamespaces []string authorizer auth.Authorizer ateClient ATEClient + // cache memoises the substrate reads for a fraction of a second; see + // substratecache.go for what it is for and why its answers carry their age. + cache *substrateCache } type Option func(*Service) @@ -101,7 +110,7 @@ type SubstrateWorker struct { } func NewService(options ...Option) *Service { - service := &Service{} + service := &Service{cache: newSubstrateCache()} for _, option := range options { option(service) } @@ -180,21 +189,16 @@ func (s *Service) ListNamespaces(ctx context.Context) ([]Namespace, error) { return namespaces, nil } +// GetSubstrateStatus returns the whole inventory in one value. +// +// It does not survive a large cluster — see the package comment on substrate.go +// for what replaced it and why. Kept for callers that predate the split. func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace string) (SubstrateStatus, error) { - if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: "Agent"}); err != nil { + namespaces, err := s.substrateScope(ctx, requestedNamespace) + if err != nil { return SubstrateStatus{}, err } - requestedNamespace = strings.TrimSpace(requestedNamespace) - if requestedNamespace != "" { - if validationErrors := utilvalidation.IsDNS1123Label(requestedNamespace); len(validationErrors) > 0 { - return SubstrateStatus{}, serviceerrors.NewInvalidArgument( - fmt.Sprintf("invalid namespace %q: %s", requestedNamespace, strings.Join(validationErrors, ", ")), - nil, - ) - } - } - result := SubstrateStatus{ Enabled: s.ateClient != nil, WorkerPools: []SubstrateWorkerPool{}, @@ -209,7 +213,6 @@ func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace str return SubstrateStatus{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", fmt.Errorf("kubernetes client is not configured")) } - namespaces := s.substrateNamespaces(requestedNamespace) for _, namespace := range namespaces { workerPools, actorTemplates, err := s.listSubstrateCRs(ctx, namespace) if err != nil { @@ -227,12 +230,8 @@ func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace str ctrllog.FromContext(ctx).Error(err, "list ate-api state") } - slices.SortStableFunc(result.WorkerPools, func(left, right SubstrateWorkerPool) int { - return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) - }) - slices.SortStableFunc(result.ActorTemplates, func(left, right SubstrateActorTemplate) int { - return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) - }) + sortWorkerPools(result.WorkerPools) + sortActorTemplates(result.ActorTemplates) slices.SortStableFunc(result.Actors, func(left, right SubstrateActor) int { return strings.Compare(left.ActorID, right.ActorID) }) diff --git a/go/core/internal/service/system/service_test.go b/go/core/internal/service/system/service_test.go index e22c234db..08bbd51e7 100644 --- a/go/core/internal/service/system/service_test.go +++ b/go/core/internal/service/system/service_test.go @@ -43,6 +43,25 @@ func (client *fakeATEClient) ListActors(context.Context, string) ([]*ateapipb.Ac return client.actors, nil } +// EachActorPage hands the actors over in more than one page on purpose. +// +// The production client pages, and a fake that answered in a single page would let +// a selector that only ever sees one page pass — which is precisely the bug worth +// catching, since the whole reason this method exists is not to hold them all. +func (client *fakeATEClient) EachActorPage(_ context.Context, _ string, visit func([]*ateapipb.Actor) error) error { + if client.err != nil { + return client.err + } + const pageSize = 3 + for start := 0; start < len(client.actors); start += pageSize { + end := min(start+pageSize, len(client.actors)) + if err := visit(client.actors[start:end]); err != nil { + return err + } + } + return nil +} + func (client *fakeATEClient) ListWorkers(context.Context) ([]*ateapipb.Worker, error) { return client.workers, client.err } diff --git a/go/core/internal/service/system/substrate.go b/go/core/internal/service/system/substrate.go new file mode 100644 index 000000000..7cb3a1bce --- /dev/null +++ b/go/core/internal/service/system/substrate.go @@ -0,0 +1,720 @@ +package system + +import ( + "context" + "encoding/base64" + "fmt" + "slices" + "strings" + "time" + + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "github.com/kagent-dev/kagent/go/core/pkg/auth" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/substrate" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// The substrate inventory, in the shape a caller can actually read it. +// +// GetSubstrateStatus answers with every actor and every worker in one message, +// which stopped working: a cluster reporting 103,134 actors produces a response +// gRPC refuses to send at all. What follows splits that one read into the three +// a caller needs — counts, a page of actors, a page of workers — so no single +// response grows with the size of the cluster. +// +// # What this does and does not fix +// +// It bounds what crosses the wire, not what ate-api hands the controller: the +// actor and worker lists still arrive here whole, because ListActors takes no +// page and no filter. So the cost of a read is unchanged on this side and the +// response is bounded on the other, which is the half that was failing. Pushing +// the narrowing all the way down needs ate-api to offer it. + +const ( + // What a caller gets when it does not ask, matching AgentInstanceService. + defaultSubstratePageSize = 50 + // The most one response will carry, matching AgentInstanceService. + maxSubstratePageSize = 100 +) + +/* + * The three public reads, each memoised briefly. + * + * The cache key is the whole question — every field of the request — so a different + * filter, sort, page or scope is a different answer rather than a stale one. See + * substratecache.go for why the answers carry the instant they were computed. + */ + +// GetSubstrateSummary counts the inventory server-side and returns the two small lists whole. +func (s *Service) GetSubstrateSummary(ctx context.Context, requestedNamespace string) (SubstrateSummary, error) { + // Authorized before the cache is consulted, so a cached answer can never be + // served to a caller who would have been refused the read. + if _, err := s.substrateScope(ctx, requestedNamespace); err != nil { + return SubstrateSummary{}, err + } + key := fmt.Sprintf("summary|%s", requestedNamespace) + value, computedAt, err := s.cache.get(ctx, key, func() (any, error) { + return s.computeSubstrateSummary(ctx, requestedNamespace) + }) + if err != nil { + return SubstrateSummary{}, err + } + result := value.(SubstrateSummary) + result.ComputedAt = computedAt + return result, nil +} + +// ListSubstrateActors returns one page of the actors matching filter, in the order asked for. +func (s *Service) ListSubstrateActors(ctx context.Context, request ListActorsRequest) (ListActorsResult, error) { + if _, err := s.substrateScope(ctx, request.Namespace); err != nil { + return ListActorsResult{}, err + } + key := fmt.Sprintf("actors|%s|%s|%d|%s|%s|%s", + request.Namespace, request.Filter, request.Limit, request.PageToken, + request.SortField, request.SortOrder) + value, computedAt, err := s.cache.get(ctx, key, func() (any, error) { + return s.computeSubstrateActors(ctx, request) + }) + if err != nil { + return ListActorsResult{}, err + } + result := value.(ListActorsResult) + result.ComputedAt = computedAt + return result, nil +} + +// ListSubstrateWorkers returns one page of the workers matching filter, in the order asked for. +func (s *Service) ListSubstrateWorkers(ctx context.Context, request ListWorkersRequest) (ListWorkersResult, error) { + if _, err := s.substrateScope(ctx, request.Namespace); err != nil { + return ListWorkersResult{}, err + } + key := fmt.Sprintf("workers|%s|%s|%d|%s|%s|%s", + request.Namespace, request.Filter, request.Limit, request.PageToken, + request.SortField, request.SortOrder) + value, computedAt, err := s.cache.get(ctx, key, func() (any, error) { + return s.computeSubstrateWorkers(ctx, request) + }) + if err != nil { + return ListWorkersResult{}, err + } + result := value.(ListWorkersResult) + result.ComputedAt = computedAt + return result, nil +} + +// SubstrateStatusCount is how many rows carry one status. +type SubstrateStatusCount struct { + Status string + Count int32 +} + +// SubstrateSummary is the whole inventory as counts, plus the two lists that are +// small enough to travel inline. +// +// The counts are over everything in scope and take no filter: they are what a +// caller reports as a total, and a total narrowed by a search is not one. The +// paged reads carry their own filtered total for the other half of "20 of 4,312". +type SubstrateSummary struct { + // ComputedAt is when this answer was produced, which is not necessarily now: + // the reads are memoised briefly (see substratecache.go). Reported so a caller + // can say how old the numbers are instead of implying they are live. + ComputedAt time.Time + Enabled bool + ATEAPIError string + WorkerPools []SubstrateWorkerPool + ActorTemplates []SubstrateActorTemplate + ActorCount int32 + WorkerCount int32 + RunningActorCount int32 + BusyWorkerCount int32 + ActorStatusCounts []SubstrateStatusCount +} + +// SortOrder is the direction a paged read is sorted in. +type SortOrder string + +const ( + SortAscending SortOrder = "asc" + SortDescending SortOrder = "desc" +) + +// ActorSortField names the column ListSubstrateActors orders by. +// +// Every order below ends in the actor id, which is unique. That is not tidiness: +// a page token is the sort key of the last row already sent, so a key that could +// tie would skip or repeat rows at a page boundary. +type ActorSortField string + +const ( + // ActorSortDefault groups by status and orders by id within each group. + ActorSortDefault ActorSortField = "status_then_id" + ActorSortStatus ActorSortField = "status" + ActorSortID ActorSortField = "id" + ActorSortTemplate ActorSortField = "template" + ActorSortWorker ActorSortField = "worker" +) + +// WorkerSortField names the column ListSubstrateWorkers orders by. +type WorkerSortField string + +const ( + // WorkerSortDefault groups by pool and orders by pod within each group. + WorkerSortDefault WorkerSortField = "pool_then_pod" + WorkerSortPool WorkerSortField = "pool" + WorkerSortPod WorkerSortField = "pod" + WorkerSortActor WorkerSortField = "actor" +) + +// ListActorsRequest is what one page of actors is asked for by. +type ListActorsRequest struct { + Namespace string + Filter string + Limit int32 + PageToken string + SortField ActorSortField + SortOrder SortOrder +} + +// ListWorkersRequest is the mirror of ListActorsRequest. +type ListWorkersRequest struct { + Namespace string + Filter string + Limit int32 + PageToken string + SortField WorkerSortField + SortOrder SortOrder +} + +// ListActorsResult is one page of actors and how many matched in total. +type ListActorsResult struct { + // ComputedAt is when this page was produced. See SubstrateSummary.ComputedAt. + ComputedAt time.Time + Actors []SubstrateActor + NextPageToken string + TotalSize int32 + // The order actually applied. Reported rather than assumed, so a caller can + // say how the rows are sorted instead of trusting that its request was + // honoured — an unspecified field resolves to a concrete one here. + SortField ActorSortField + SortOrder SortOrder +} + +// ListWorkersResult is one page of workers and how many matched in total. +type ListWorkersResult struct { + ComputedAt time.Time + Workers []SubstrateWorker + NextPageToken string + TotalSize int32 + SortField WorkerSortField + SortOrder SortOrder +} + +// GetSubstrateSummary counts the inventory server-side and returns the two small +// lists whole. +// +// The ate-api halves can be absent (no endpoint configured) or partial (an error +// on an otherwise successful read). Both are reported rather than raised: the +// Kubernetes-derived halves are complete either way, and failing the whole call +// would hide them. +func (s *Service) computeSubstrateSummary(ctx context.Context, requestedNamespace string) (SubstrateSummary, error) { + namespaces, err := s.substrateScope(ctx, requestedNamespace) + if err != nil { + return SubstrateSummary{}, err + } + + result := SubstrateSummary{ + Enabled: s.ateClient != nil, + WorkerPools: []SubstrateWorkerPool{}, + ActorTemplates: []SubstrateActorTemplate{}, + ActorStatusCounts: []SubstrateStatusCount{}, + } + if s.ateClient == nil { + return result, nil + } + if s.kubeClient == nil { + return SubstrateSummary{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", fmt.Errorf("kubernetes client is not configured")) + } + + for _, namespace := range namespaces { + workerPools, actorTemplates, err := s.listSubstrateCRs(ctx, namespace) + if err != nil { + return SubstrateSummary{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", err) + } + result.WorkerPools = append(result.WorkerPools, workerPools...) + result.ActorTemplates = append(result.ActorTemplates, actorTemplates...) + } + sortWorkerPools(result.WorkerPools) + sortActorTemplates(result.ActorTemplates) + + // Counted straight off the protos ate-api returned, without converting them. + // + // A count needs no struct, and building 410,110 of them to take their length is + // how the paged read next door OOM-killed the controller. The whole distribution + // is kept rather than only the running tally: knowing 12 of 4,312 are running + // says nothing about the other 4,300. + statusCounts := map[string]int32{} + inScope := namespaceFilter(namespaces) + + // Walked a page at a time and never accumulated: counting 410,110 actors costs + // a few integers this way, where holding them cost the controller its memory + // limit. + if err := s.ateClient.EachActorPage(ctx, "", func(page []*ateapipb.Actor) error { + for _, actor := range page { + if actor == nil || !inScope(actor.GetActorTemplateNamespace()) { + continue + } + result.ActorCount++ + status := substrate.ActorStatusLabel(actor.GetStatus().GetState()) + statusCounts[status]++ + if strings.EqualFold(status, "Running") { + result.RunningActorCount++ + } + } + return nil + }); err != nil { + // Partial rather than failed: the Kubernetes halves above are complete, and + // the caller is told the counts may be short instead of losing everything. + result.ATEAPIError = err.Error() + ctrllog.FromContext(ctx).Error(err, "list ate-api actors") + } + + if workersFromAPI, err := s.ateClient.ListWorkers(ctx); err != nil { + if result.ATEAPIError == "" { + result.ATEAPIError = err.Error() + } + ctrllog.FromContext(ctx).Error(err, "list ate-api workers") + } else { + for _, worker := range workersFromAPI { + if worker == nil || !inScope(worker.GetWorkerNamespace()) { + continue + } + result.WorkerCount++ + // A worker holding an actor is busy; one holding none is available. + if worker.GetStatus().GetAssignment().GetActor().GetName() != "" { + result.BusyWorkerCount++ + } + } + } + + for status, count := range statusCounts { + result.ActorStatusCounts = append(result.ActorStatusCounts, SubstrateStatusCount{Status: status, Count: count}) + } + slices.SortStableFunc(result.ActorStatusCounts, func(left, right SubstrateStatusCount) int { + return strings.Compare(left.Status, right.Status) + }) + + return result, nil +} + +// ListSubstrateActors returns one page of the actors matching filter, in the +// order asked for. +// +// Sorting is server-side for the same reason the filter is: the rows are paged, +// so ordering a page that has already been fetched reorders a hundred rows out +// of hundreds of thousands. It looks like sorting and it is not — the first row +// of the sorted cluster is almost certainly not among the hundred on screen. +func (s *Service) computeSubstrateActors(ctx context.Context, request ListActorsRequest) (ListActorsResult, error) { + namespaces, err := s.substrateScope(ctx, request.Namespace) + if err != nil { + return ListActorsResult{}, err + } + pageSize, err := substratePageSize(request.Limit) + if err != nil { + return ListActorsResult{}, err + } + after, err := decodeSubstratePageToken(request.PageToken) + if err != nil { + return ListActorsResult{}, err + } + field, order := actorSort(request.SortField, request.SortOrder) + + result := ListActorsResult{Actors: []SubstrateActor{}, SortField: field, SortOrder: order} + if s.ateClient == nil { + return result, nil + } + + /* + * Selected while streaming, so the cost of this call does not grow with the + * cluster. + * + * The obvious implementation — collect every actor, sort, take a slice — is what + * OOM-killed the controller at 410,110 actors: ate-api pages its own list, and + * accumulating those pages is hundreds of megabytes of protos before any of this + * code runs. Instead the actors are walked a page at a time and a bounded buffer + * keeps only the `pageSize` rows that belong on the page being asked for, which + * is `pageSize` rows of memory whatever the cluster is running. + * + * The filtered total is counted in the same pass, because it is the other half of + * "20 of 4,312" and counting it afterwards would mean a second walk. + */ + inScope := namespaceFilter(namespaces) + key := actorKey(field) + selector := newPageSelector(pageSize, after, order, key) + + if err := s.ateClient.EachActorPage(ctx, "", func(page []*ateapipb.Actor) error { + for _, actor := range page { + if actor == nil || !inScope(actor.GetActorTemplateNamespace()) { + continue + } + converted := actorFromProto(actor) + if !matchesFilter(request.Filter, converted.ActorID, converted.Status, converted.ActorTemplateNamespace, converted.ActorTemplateName, converted.AteomPodNamespace, converted.AteomPodName, converted.AteomPodIP) { + continue + } + selector.offer(converted) + } + return nil + }); err != nil { + // Unlike the summary, there is no complete half to salvage here: this call + // answers with actors or it answers with nothing. + return ListActorsResult{}, serviceerrors.NewInternal("Failed to list actors from ate-api", err) + } + + rows, nextToken, total := selector.page() + result.Actors = rows + result.NextPageToken = nextToken + result.TotalSize = total + return result, nil +} + +// ListSubstrateWorkers returns one page of the workers matching filter, in the +// order asked for. The mirror of ListSubstrateActors. +func (s *Service) computeSubstrateWorkers(ctx context.Context, request ListWorkersRequest) (ListWorkersResult, error) { + namespaces, err := s.substrateScope(ctx, request.Namespace) + if err != nil { + return ListWorkersResult{}, err + } + pageSize, err := substratePageSize(request.Limit) + if err != nil { + return ListWorkersResult{}, err + } + after, err := decodeSubstratePageToken(request.PageToken) + if err != nil { + return ListWorkersResult{}, err + } + field, order := workerSort(request.SortField, request.SortOrder) + + result := ListWorkersResult{Workers: []SubstrateWorker{}, SortField: field, SortOrder: order} + if s.ateClient == nil { + return result, nil + } + + // The actors are not read at all — see ListSubstrateActors for why that matters. + matching, err := s.matchingWorkers(ctx, namespaces, request.Filter) + if err != nil { + return ListWorkersResult{}, serviceerrors.NewInternal("Failed to list workers from ate-api", err) + } + + // Not streamed, unlike the actors: ListWorkers answers in one response and a + // worker count is bounded by the size of the pools, so the same selector is used + // only to keep the paging and ordering rules identical between the two. + selector := newPageSelector(pageSize, after, order, workerKey(field)) + for _, worker := range matching { + selector.offer(worker) + } + + rows, nextToken, total := selector.page() + result.Workers = rows + result.NextPageToken = nextToken + result.TotalSize = total + return result, nil +} + +/* + * The sort keys. + * + * Each is the chosen column followed by a unique tiebreaker, so that ordering is + * total: a page token is the key of the last row sent, and a key two rows could + * share would make the boundary between pages ambiguous — skipping one row or + * repeating it. + */ +func actorSort(field ActorSortField, order SortOrder) (ActorSortField, SortOrder) { + switch field { + case ActorSortStatus, ActorSortID, ActorSortTemplate, ActorSortWorker: + default: + field = ActorSortDefault + } + if order != SortDescending { + order = SortAscending + } + return field, order +} + +func actorKey(field ActorSortField) func(SubstrateActor) string { + switch field { + case ActorSortID: + return func(a SubstrateActor) string { return a.ActorID } + case ActorSortStatus: + return func(a SubstrateActor) string { return a.Status + "\x00" + a.ActorID } + case ActorSortTemplate: + return func(a SubstrateActor) string { + return a.ActorTemplateNamespace + "/" + a.ActorTemplateName + "\x00" + a.ActorID + } + case ActorSortWorker: + return func(a SubstrateActor) string { + return a.AteomPodNamespace + "/" + a.AteomPodName + "\x00" + a.ActorID + } + default: + return func(a SubstrateActor) string { return a.Status + "\x00" + a.ActorID } + } +} + +func workerSort(field WorkerSortField, order SortOrder) (WorkerSortField, SortOrder) { + switch field { + case WorkerSortPool, WorkerSortPod, WorkerSortActor: + default: + field = WorkerSortDefault + } + if order != SortDescending { + order = SortAscending + } + return field, order +} + +func workerKey(field WorkerSortField) func(SubstrateWorker) string { + pod := func(w SubstrateWorker) string { return w.WorkerNamespace + "/" + w.WorkerPod } + switch field { + case WorkerSortPod: + return pod + case WorkerSortActor: + // Idle workers sort together, and after the busy ones ascending: an empty + // string would put every available worker first, which buries the placements + // this column exists to show. + return func(w SubstrateWorker) string { + actor := w.ActorID + if actor == "" { + actor = "\uffff" + } + return actor + "\x00" + pod(w) + } + default: + return func(w SubstrateWorker) string { return w.WorkerPool + "\x00" + pod(w) } + } +} + +// matchingWorkers reads the workers and keeps only those in scope and matching +// the filter, converting as it goes. +// +// Not streamed, unlike the actors: ate-api answers ListWorkers in one response and +// a worker count is bounded by the size of the pools — eight on the cluster this +// was measured against — so there is nothing here to page around. +func (s *Service) matchingWorkers(ctx context.Context, namespaces []string, filter string) ([]SubstrateWorker, error) { + workersFromAPI, err := s.ateClient.ListWorkers(ctx) + if err != nil { + return nil, err + } + inScope := namespaceFilter(namespaces) + + var matching []SubstrateWorker + for _, worker := range workersFromAPI { + if worker == nil || !inScope(worker.GetWorkerNamespace()) { + continue + } + converted := workerFromProto(worker) + if !matchesFilter(filter, converted.WorkerNamespace, converted.WorkerPool, converted.WorkerPod, converted.ActorNamespace, converted.ActorTemplate, converted.ActorID, converted.IP) { + continue + } + matching = append(matching, converted) + } + return matching, nil +} + +// namespaceFilter reports whether a row's namespace is in scope. +// +// A row with no namespace is always in scope: ate-api leaves it empty on records +// it cannot attribute, and dropping those would quietly shorten the inventory. +// This is the same rule listATEState applies, extracted so the paged reads cannot +// drift from the unpaged one. +func namespaceFilter(namespaces []string) func(string) bool { + if len(namespaces) == 1 && namespaces[0] == "" { + return func(string) bool { return true } + } + allowed := make(map[string]struct{}, len(namespaces)) + for _, namespace := range namespaces { + if namespace != "" { + allowed[namespace] = struct{}{} + } + } + return func(namespace string) bool { + namespace = strings.TrimSpace(namespace) + if namespace == "" { + return true + } + _, ok := allowed[namespace] + return ok + } +} + +// substrateScope authorizes the caller and resolves which namespaces to read. +// +// Shared by all three substrate reads so that one of them cannot quietly become +// more permissive than the others — the authorization and the namespace +// validation are the same check on every path. +func (s *Service) substrateScope(ctx context.Context, requestedNamespace string) ([]string, error) { + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: "Agent"}); err != nil { + return nil, err + } + requestedNamespace = strings.TrimSpace(requestedNamespace) + if requestedNamespace != "" { + if problems := utilvalidation.IsDNS1123Label(requestedNamespace); len(problems) > 0 { + return nil, serviceerrors.NewInvalidArgument( + fmt.Sprintf("invalid namespace %q: %s", requestedNamespace, strings.Join(problems, ", ")), + nil, + ) + } + } + return s.substrateNamespaces(requestedNamespace), nil +} + +// substratePageSize applies the same bounds AgentInstanceService uses. +// +// Zero means "no preference" and takes the default; anything outside the range +// is refused rather than clamped, so a caller asking for 5,000 is told its +// request was not honoured instead of quietly receiving 100. +func substratePageSize(limit int32) (int, error) { + if limit == 0 { + return defaultSubstratePageSize, nil + } + if limit < 0 || limit > maxSubstratePageSize { + return 0, serviceerrors.NewInvalidArgument(fmt.Sprintf("page limit must be between 1 and %d", maxSubstratePageSize), nil) + } + return int(limit), nil +} + +// The page token is the sort key of the last row already sent. +// +// A key rather than an offset, so that rows appearing or disappearing between +// two reads shift the page's contents instead of causing a row to be skipped or +// repeated — which on an inventory that changes every second it is polled is the +// difference between a stable list and one that flickers. +func encodeSubstratePageToken(key string) string { + return base64.RawURLEncoding.EncodeToString([]byte(key)) +} + +func decodeSubstratePageToken(token string) (string, error) { + if token == "" { + return "", nil + } + value, err := base64.RawURLEncoding.DecodeString(token) + if err != nil { + return "", serviceerrors.NewInvalidArgument("page token is invalid", err) + } + return string(value), nil +} + +/* + * The rows belonging on one page, chosen without holding the rest. + * + * Paging is by sort key — the key of the last row already sent — so the page being + * asked for is "the `limit` rows that come after `after` in the chosen order". That + * can be decided from a stream: keep the `limit` rows nearest the front and discard + * anything that cannot make the page. + * + * Compaction rather than a heap. The buffer is allowed to grow to twice the limit + * and is then sorted and truncated, which is the same amortised work as a heap for + * these sizes and considerably easier to be sure is right — and being sure matters, + * because the failure mode of a wrong selector is a page that silently skips rows. + */ +type pageSelector[T any] struct { + limit int + after string + order SortOrder + key func(T) string + rows []T + // Every row that matched, page or not. The caller reports it as the total, and + // it is the reason this is counted here rather than by a second walk. + total int32 +} + +func newPageSelector[T any](limit int, after string, order SortOrder, key func(T) string) *pageSelector[T] { + return &pageSelector[T]{limit: limit, after: after, order: order, key: key} +} + +// before reports whether left comes before right in the selected order. +func (s *pageSelector[T]) before(left, right string) bool { + if s.order == SortDescending { + return left > right + } + return left < right +} + +func (s *pageSelector[T]) offer(row T) { + // Every matching row counts towards the total, page or not — it is a total, not + // a remainder. + s.total++ + // Rows at or before the token have already been sent. + if s.after != "" && !s.before(s.after, s.key(row)) { + return + } + s.rows = append(s.rows, row) + if len(s.rows) >= 2*s.limit { + s.compact() + } +} + +func (s *pageSelector[T]) compact() { + slices.SortStableFunc(s.rows, func(left, right T) int { + leftKey, rightKey := s.key(left), s.key(right) + if leftKey == rightKey { + return 0 + } + if s.before(leftKey, rightKey) { + return -1 + } + return 1 + }) + if len(s.rows) > s.limit { + s.rows = s.rows[:s.limit] + } +} + +// page returns the page, the token to ask for the next one, and the filtered total. +// +// The token is empty on the last page rather than on a page that merely happens to +// be full, so a caller never fetches an empty page to discover it has finished. +func (s *pageSelector[T]) page() ([]T, string, int32) { + s.compact() + if len(s.rows) == 0 { + return []T{}, "", s.total + } + // A full page means there may be more: the selector discarded everything past + // the limit, so it cannot know whether anything was there. Asking again is the + // only way to find out, and an empty answer is what ends the walk. + token := "" + if len(s.rows) == s.limit { + token = encodeSubstratePageToken(s.key(s.rows[len(s.rows)-1])) + } + return s.rows, token, s.total +} + +// matchesFilter reports whether any of the row's displayed fields contains the +// term, case-insensitively. An empty term matches every row. +// +// Matched against the fields the caller displays rather than against every field +// on the record: a search that hits on something not on screen reads as a list +// filtering itself at random. +func matchesFilter(filter string, fields ...string) bool { + needle := strings.ToLower(strings.TrimSpace(filter)) + if needle == "" { + return true + } + for _, field := range fields { + if strings.Contains(strings.ToLower(field), needle) { + return true + } + } + return false +} + +func sortWorkerPools(pools []SubstrateWorkerPool) { + slices.SortStableFunc(pools, func(left, right SubstrateWorkerPool) int { + return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) + }) +} + +func sortActorTemplates(templates []SubstrateActorTemplate) { + slices.SortStableFunc(templates, func(left, right SubstrateActorTemplate) int { + return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) + }) +} diff --git a/go/core/internal/service/system/substrate_test.go b/go/core/internal/service/system/substrate_test.go new file mode 100644 index 000000000..925bc58f0 --- /dev/null +++ b/go/core/internal/service/system/substrate_test.go @@ -0,0 +1,491 @@ +package system_test + +import ( + "context" + "fmt" + "slices" + "testing" + + atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "github.com/kagent-dev/kagent/go/core/internal/service/system" + pkgAuth "github.com/kagent-dev/kagent/go/core/pkg/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// substrateScheme is the scheme the substrate CRs are registered against, shared +// by every test below. +func substrateScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, atev1alpha1.AddToScheme(scheme)) + return scheme +} + +func substrateContext(t *testing.T) context.Context { + t.Helper() + return pkgAuth.AuthSessionTo(t.Context(), &authimpl.SimpleSession{P: pkgAuth.Principal{User: pkgAuth.User{ID: "user"}}}) +} + +// actorsNamed builds n actors in one namespace, alternating status so that the +// grouping the service sorts by is actually exercised rather than assumed. +func actorsNamed(namespace string, n int) []*ateapipb.Actor { + actors := make([]*ateapipb.Actor, 0, n) + for i := range n { + state := ateapipb.ActorState_ACTOR_STATE_RUNNING + if i%2 == 1 { + state = ateapipb.ActorState_ACTOR_STATE_SUSPENDED + } + actors = append(actors, &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Name: fmt.Sprintf("actor-%03d", i)}, + Status: &ateapipb.ActorStatus{State: state}, + ActorTemplateNamespace: namespace, + ActorTemplateName: "template", + }) + } + return actors +} + +func workersNamed(namespace string, n int, busy int) []*ateapipb.Worker { + workers := make([]*ateapipb.Worker, 0, n) + for i := range n { + worker := &ateapipb.Worker{ + Metadata: &ateapipb.ResourceMetadata{Version: int64(i)}, + WorkerNamespace: namespace, + WorkerPool: "pool", + WorkerPod: fmt.Sprintf("worker-%03d", i), + Status: &ateapipb.WorkerStatus{}, + } + if i < busy { + worker.Status.Assignment = &ateapipb.ActorAssignment{ + ActorTemplate: &ateapipb.KubeNamespacedObjectRef{Namespace: namespace, Name: "template"}, + Actor: &ateapipb.ObjectRef{Name: fmt.Sprintf("actor-%03d", i)}, + } + } + workers = append(workers, worker) + } + return workers +} + +// TestGetSubstrateSummary is the guard on the tiles: these counts are the only +// honest total a caller has, because every other read is now a page. +func TestGetSubstrateSummary(t *testing.T) { + ctx := substrateContext(t) + + t.Run("counts everything in scope and carries the small lists inline", func(t *testing.T) { + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).WithObjects( + &atev1alpha1.WorkerPool{ + ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "pool"}, + Spec: atev1alpha1.WorkerPoolSpec{Replicas: 8, AteomImage: "ateom:test"}, + }, + &atev1alpha1.ActorTemplate{ + ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "template"}, + Status: atev1alpha1.ActorTemplateStatus{Phase: atev1alpha1.PhaseReady}, + }, + ).Build() + ateClient := &fakeATEClient{ + actors: actorsNamed("team", 10), + workers: workersNamed("team", 8, 3), + } + service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, ateClient)) + + result, err := service.GetSubstrateSummary(ctx, "team") + require.NoError(t, err) + + assert.True(t, result.Enabled) + assert.Empty(t, result.ATEAPIError) + require.Len(t, result.WorkerPools, 1) + assert.Equal(t, int32(8), result.WorkerPools[0].Replicas) + require.Len(t, result.ActorTemplates, 1) + + assert.Equal(t, int32(10), result.ActorCount) + assert.Equal(t, int32(5), result.RunningActorCount, "half the actors are Running") + assert.Equal(t, int32(8), result.WorkerCount) + assert.Equal(t, int32(3), result.BusyWorkerCount, "a worker is busy when an actor is placed on it") + + // The whole distribution, not only the running tally: a caller that knows 5 + // of 10 are running still cannot say what the other 5 are doing. + assert.Equal(t, []system.SubstrateStatusCount{ + {Status: "Running", Count: 5}, + {Status: "Suspended", Count: 5}, + }, result.ActorStatusCounts) + }) + + t.Run("reports a partial ate-api read rather than failing", func(t *testing.T) { + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).WithObjects( + &atev1alpha1.WorkerPool{ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "pool"}}, + ).Build() + ateClient := &fakeATEClient{err: assert.AnError} + service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, ateClient)) + + result, err := service.GetSubstrateSummary(ctx, "team") + + // The Kubernetes half is complete, so failing the call would hide data that + // arrived intact. + require.NoError(t, err) + assert.NotEmpty(t, result.ATEAPIError) + assert.Len(t, result.WorkerPools, 1) + assert.Equal(t, int32(0), result.ActorCount) + }) + + t.Run("disabled does not read Kubernetes", func(t *testing.T) { + service := system.NewService(system.WithInventory(nil, nil, &authimpl.NoopAuthorizer{}, nil)) + result, err := service.GetSubstrateSummary(ctx, "team") + require.NoError(t, err) + assert.False(t, result.Enabled) + assert.Empty(t, result.WorkerPools) + }) + + t.Run("validates and authorizes exactly as GetSubstrateStatus does", func(t *testing.T) { + service := system.NewService(system.WithInventory(nil, nil, &authimpl.NoopAuthorizer{}, nil)) + _, err := service.GetSubstrateSummary(ctx, "INVALID_NAMESPACE") + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) + + service = system.NewService(system.WithInventory(nil, nil, systemDenyAuthorizer{}, nil)) + _, err = service.GetSubstrateSummary(ctx, "") + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied), err) + }) +} + +func TestListSubstrateActors(t *testing.T) { + ctx := substrateContext(t) + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() + + newService := func(actors []*ateapipb.Actor) *system.Service { + return system.NewService(system.WithInventory( + kubeClient, nil, &authimpl.NoopAuthorizer{}, + &fakeATEClient{actors: actors}, + )) + } + + t.Run("pages through every actor exactly once", func(t *testing.T) { + service := newService(actorsNamed("team", 25)) + + seen := map[string]int{} + pageToken := "" + pages := 0 + for { + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: pageToken}) + require.NoError(t, err) + // The total is of everything matching, not of this page — which is what + // lets a caller say "10 of 25" instead of implying the page is the lot. + assert.Equal(t, int32(25), result.TotalSize) + for _, actor := range result.Actors { + seen[actor.ActorID]++ + } + pages++ + require.Less(t, pages, 10, "paging did not terminate") + if result.NextPageToken == "" { + // An empty token is the last page, so a caller never fetches an empty + // one to discover it has finished. + assert.LessOrEqual(t, len(result.Actors), 10) + break + } + pageToken = result.NextPageToken + } + + assert.Equal(t, 3, pages, "25 actors at 10 a page") + assert.Len(t, seen, 25, "every actor appeared") + for id, count := range seen { + assert.Equal(t, 1, count, "%s appeared more than once", id) + } + }) + + t.Run("filters server-side across the whole list, not one page", func(t *testing.T) { + service := newService(actorsNamed("team", 25)) + + // actor-019 sorts well past the first page, so a client-side filter over a + // fetched page would report no matches for it. That is the failure this + // exists to prevent. + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "actor-019", Limit: 10, PageToken: ""}) + require.NoError(t, err) + require.Len(t, result.Actors, 1) + assert.Equal(t, "actor-019", result.Actors[0].ActorID) + assert.Equal(t, int32(1), result.TotalSize) + assert.Empty(t, result.NextPageToken) + }) + + t.Run("matches case-insensitively on status too", func(t *testing.T) { + service := newService(actorsNamed("team", 10)) + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "SUSPEND", Limit: 100, PageToken: ""}) + require.NoError(t, err) + assert.Equal(t, int32(5), result.TotalSize) + }) + + t.Run("groups by status so a page is stable between reads", func(t *testing.T) { + service := newService(actorsNamed("team", 10)) + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 100, PageToken: ""}) + require.NoError(t, err) + require.Len(t, result.Actors, 10) + + // Sorted by status then id: every Running actor precedes every Suspended one. + for i := range 5 { + assert.Equal(t, "Running", result.Actors[i].Status) + } + for i := 5; i < 10; i++ { + assert.Equal(t, "Suspended", result.Actors[i].Status) + } + }) + + t.Run("refuses a page size it cannot honour rather than clamping", func(t *testing.T) { + service := newService(actorsNamed("team", 5)) + + _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 5000, PageToken: ""}) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) + + _, err = service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: -1, PageToken: ""}) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) + + // Zero is "no preference" and takes the default. + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 0, PageToken: ""}) + require.NoError(t, err) + assert.Len(t, result.Actors, 5) + }) + + t.Run("rejects a page token that is not one", func(t *testing.T) { + service := newService(actorsNamed("team", 5)) + _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: "not base64!!"}) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) + }) + + t.Run("answers empty when ate-api is not configured", func(t *testing.T) { + service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, nil)) + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) + require.NoError(t, err) + assert.Empty(t, result.Actors) + assert.Equal(t, int32(0), result.TotalSize) + }) + + t.Run("fails the call when ate-api does", func(t *testing.T) { + // Unlike the summary there is no complete half to salvage: this call answers + // with actors or it answers with nothing. + service := system.NewService(system.WithInventory( + kubeClient, nil, &authimpl.NoopAuthorizer{}, &fakeATEClient{err: assert.AnError}, + )) + _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) + require.Error(t, err) + }) + + t.Run("validates and authorizes", func(t *testing.T) { + service := newService(nil) + _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "INVALID_NAMESPACE", Filter: "", Limit: 10, PageToken: ""}) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) + + denied := system.NewService(system.WithInventory(kubeClient, nil, systemDenyAuthorizer{}, &fakeATEClient{})) + _, err = denied.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) + assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied), err) + }) +} + +func TestListSubstrateWorkers(t *testing.T) { + ctx := substrateContext(t) + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() + + newService := func(workers []*ateapipb.Worker) *system.Service { + return system.NewService(system.WithInventory( + kubeClient, nil, &authimpl.NoopAuthorizer{}, + &fakeATEClient{workers: workers}, + )) + } + + t.Run("pages through every worker exactly once", func(t *testing.T) { + service := newService(workersNamed("team", 12, 4)) + + seen := map[string]int{} + pageToken := "" + for pages := 0; ; pages++ { + require.Less(t, pages, 10, "paging did not terminate") + result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{Namespace: "team", Filter: "", Limit: 5, PageToken: pageToken}) + require.NoError(t, err) + assert.Equal(t, int32(12), result.TotalSize) + for _, worker := range result.Workers { + seen[worker.WorkerPod]++ + } + if result.NextPageToken == "" { + break + } + pageToken = result.NextPageToken + } + + assert.Len(t, seen, 12) + for pod, count := range seen { + assert.Equal(t, 1, count, "%s appeared more than once", pod) + } + }) + + t.Run("filters on the placed actor", func(t *testing.T) { + service := newService(workersNamed("team", 12, 4)) + result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{Namespace: "team", Filter: "actor-002", Limit: 100, PageToken: ""}) + require.NoError(t, err) + require.Len(t, result.Workers, 1) + assert.Equal(t, "worker-002", result.Workers[0].WorkerPod) + }) + + t.Run("answers empty when ate-api is not configured", func(t *testing.T) { + service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, nil)) + result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) + require.NoError(t, err) + assert.Empty(t, result.Workers) + }) +} + +// TestListSubstrateActorsSorting is the guard on server-side ordering. +// +// The property that matters is not "the rows came back sorted" — it is that +// **paging through a sorted result yields every row exactly once**. A selector +// whose direction and whose page token disagree drops rows at a page boundary, +// and it does so silently: each page looks correctly ordered on its own. +func TestListSubstrateActorsSorting(t *testing.T) { + ctx := substrateContext(t) + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() + service := system.NewService(system.WithInventory( + kubeClient, nil, &authimpl.NoopAuthorizer{}, + &fakeATEClient{actors: actorsNamed("team", 25)}, + )) + + // Every order the API offers, in both directions. + fields := []system.ActorSortField{ + system.ActorSortDefault, + system.ActorSortStatus, + system.ActorSortID, + system.ActorSortTemplate, + system.ActorSortWorker, + } + orders := []system.SortOrder{system.SortAscending, system.SortDescending} + + for _, field := range fields { + for _, order := range orders { + t.Run(string(field)+"/"+string(order), func(t *testing.T) { + seen := map[string]int{} + var ordered []string + pageToken := "" + + for pages := 0; ; pages++ { + require.Less(t, pages, 12, "paging did not terminate") + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{ + Namespace: "team", + Limit: 7, + PageToken: pageToken, + SortField: field, + SortOrder: order, + }) + require.NoError(t, err) + + // The order applied is reported, not assumed — a caller says how its + // rows are sorted rather than trusting the request was honoured. + assert.Equal(t, field, result.SortField) + assert.Equal(t, order, result.SortOrder) + assert.Equal(t, int32(25), result.TotalSize) + + for _, actor := range result.Actors { + seen[actor.ActorID]++ + ordered = append(ordered, actor.ActorID) + } + if result.NextPageToken == "" { + break + } + pageToken = result.NextPageToken + } + + // Every row, exactly once, across the whole walk. + assert.Len(t, seen, 25, "paging lost or repeated rows") + for id, count := range seen { + assert.Equal(t, 1, count, "%s appeared more than once", id) + } + + // And the concatenated pages are themselves in order: a page that + // sorted only within itself would satisfy the count above. + sorted := append([]string(nil), ordered...) + slices.Sort(sorted) + if order == system.SortDescending { + slices.Reverse(sorted) + } + if field == system.ActorSortID { + // Only the id sort is a total order on the id alone; the others tie + // on their column and break it with the id, so the ids themselves + // are not monotonic. + assert.Equal(t, sorted, ordered, "pages were not in the requested order") + } + }) + } + } +} + +// TestListSubstrateActorsSortDirectionIsHonoured checks the two directions +// actually differ — a selector that ignored the order would pass every +// completeness assertion above. +func TestListSubstrateActorsSortDirectionIsHonoured(t *testing.T) { + ctx := substrateContext(t) + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() + service := system.NewService(system.WithInventory( + kubeClient, nil, &authimpl.NoopAuthorizer{}, + &fakeATEClient{actors: actorsNamed("team", 25)}, + )) + + first := func(order system.SortOrder) string { + result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{ + Namespace: "team", + Limit: 1, + SortField: system.ActorSortID, + SortOrder: order, + }) + require.NoError(t, err) + require.Len(t, result.Actors, 1) + return result.Actors[0].ActorID + } + + assert.Equal(t, "actor-000", first(system.SortAscending)) + assert.Equal(t, "actor-024", first(system.SortDescending)) +} + +// TestListSubstrateWorkersSorting is the same property for the worker list, +// which pages through the same selector. +func TestListSubstrateWorkersSorting(t *testing.T) { + ctx := substrateContext(t) + kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() + service := system.NewService(system.WithInventory( + kubeClient, nil, &authimpl.NoopAuthorizer{}, + &fakeATEClient{workers: workersNamed("team", 12, 4)}, + )) + + for _, field := range []system.WorkerSortField{ + system.WorkerSortDefault, + system.WorkerSortPool, + system.WorkerSortPod, + system.WorkerSortActor, + } { + for _, order := range []system.SortOrder{system.SortAscending, system.SortDescending} { + seen := map[string]int{} + pageToken := "" + for pages := 0; ; pages++ { + require.Less(t, pages, 12, "paging did not terminate") + result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{ + Namespace: "team", + Limit: 5, + PageToken: pageToken, + SortField: field, + SortOrder: order, + }) + require.NoError(t, err) + assert.Equal(t, field, result.SortField) + assert.Equal(t, int32(12), result.TotalSize) + for _, worker := range result.Workers { + seen[worker.WorkerPod]++ + } + if result.NextPageToken == "" { + break + } + pageToken = result.NextPageToken + } + assert.Len(t, seen, 12, "%s/%s lost or repeated rows", field, order) + } + } +} diff --git a/go/core/internal/service/system/substratecache.go b/go/core/internal/service/system/substratecache.go new file mode 100644 index 000000000..ed5f95f36 --- /dev/null +++ b/go/core/internal/service/system/substratecache.go @@ -0,0 +1,149 @@ +package system + +import ( + "context" + "sync" + "time" + + "golang.org/x/sync/singleflight" +) + +/* + * A short-lived cache in front of the substrate reads, and why it reports its own + * age. + * + * # The cost this exists for + * + * Every one of the three substrate reads walks ate-api's whole actor list, because + * ate-api offers no filter, no ordering and no server-side count — only pagination. + * On a deployment holding 410,110 actors that measured at ~1.6s per call, and the + * substrate page makes three of them on load and again on every poll tick. + * + * Asking ate-api for larger pages does not help: the page size was raised to its + * maximum of 1000 and the timing did not move, so the cost is ate-api's own scan + * rather than the number of round trips. There is nothing to optimise on this side + * of that call; the only lever left is to make the same call less often. + * + * # Why the age is part of the answer + * + * Because the page above this offers polling, and a cache is exactly how polling + * becomes a lie: the reader turns it on, the requests go out, the responses come + * back instantly, and the numbers never change. This codebase has already shipped + * that once — a poll control that reported it was re-reading and was not — and the + * fix then was to measure rather than to trust. + * + * So every cached answer carries the instant it was computed. A caller can say "as + * of 0.4s ago" instead of implying "now", and a reader watching a stalled cluster + * can tell the difference between nothing changing and nothing being read. The TTL + * is deliberately shorter than the page's default poll interval, so an ordinary + * poll misses the cache and genuinely re-reads; what the cache absorbs is the burst + * of identical requests a single page load makes. + */ + +// substrateCacheTTL is how long a computed answer may be reused. +// +// Below the substrate page's default one-second poll and at its half-second floor, +// so polling at any offered rate still reaches ate-api. What this collapses is the +// three-or-more identical requests one page load makes — including React rendering +// a component twice in development. +const substrateCacheTTL = 400 * time.Millisecond + +// substrateCacheEntries caps how many distinct answers are held. +// +// Each entry is one page of rows or one set of counts, so the cap bounds memory at +// something small and fixed. Distinct entries come from distinct questions — a +// different filter, sort or page — and a reader cannot generate many of those +// quickly. Oldest-out when full, which for a TTL this short is nearly always an +// entry that had expired anyway. +const substrateCacheEntries = 64 + +// cachedAnswer is a computed result and the instant it was computed at. +type cachedAnswer struct { + value any + computedAt time.Time +} + +// substrateCache memoises the substrate reads for substrateCacheTTL. +// +// The singleflight group is the other half of the point: without it, the three +// reads a page load fires concurrently would each start their own walk before any +// of them had a result to cache. +type substrateCache struct { + mutex sync.Mutex + entries map[string]cachedAnswer + group singleflight.Group + // now is injectable so the tests can move time without sleeping. + now func() time.Time +} + +func newSubstrateCache() *substrateCache { + return &substrateCache{entries: map[string]cachedAnswer{}, now: time.Now} +} + +// get returns the answer for key, computing it only when there is no fresh one. +// +// Returns the value and the instant it was computed, which is not necessarily now — +// that difference is the whole reason this returns two things. +func (c *substrateCache) get(ctx context.Context, key string, compute func() (any, error)) (any, time.Time, error) { + if c == nil { + value, err := compute() + return value, time.Now(), err + } + + if answer, ok := c.fresh(key); ok { + return answer.value, answer.computedAt, nil + } + + // Shared: concurrent callers asking the same question wait for one walk rather + // than starting one each. + result, err, _ := c.group.Do(key, func() (any, error) { + // Re-checked inside the flight: a caller that queued behind another one may + // find the answer already stored by the time it runs. + if answer, ok := c.fresh(key); ok { + return answer, nil + } + value, err := compute() + if err != nil { + return cachedAnswer{}, err + } + answer := cachedAnswer{value: value, computedAt: c.now()} + c.store(key, answer) + return answer, nil + }) + if err != nil { + return nil, time.Time{}, err + } + if ctx.Err() != nil { + return nil, time.Time{}, ctx.Err() + } + answer := result.(cachedAnswer) + return answer.value, answer.computedAt, nil +} + +func (c *substrateCache) fresh(key string) (cachedAnswer, bool) { + c.mutex.Lock() + defer c.mutex.Unlock() + answer, ok := c.entries[key] + if !ok || c.now().Sub(answer.computedAt) > substrateCacheTTL { + return cachedAnswer{}, false + } + return answer, true +} + +func (c *substrateCache) store(key string, answer cachedAnswer) { + c.mutex.Lock() + defer c.mutex.Unlock() + if len(c.entries) >= substrateCacheEntries { + // Drop the oldest rather than clearing everything: clearing would throw away + // the entry the current burst of requests is about to ask for again. + var oldestKey string + var oldest time.Time + for candidate, entry := range c.entries { + if oldestKey == "" || entry.computedAt.Before(oldest) { + oldestKey, oldest = candidate, entry.computedAt + } + } + delete(c.entries, oldestKey) + } + c.entries[key] = answer +} diff --git a/go/core/internal/service/system/substratecache_test.go b/go/core/internal/service/system/substratecache_test.go new file mode 100644 index 000000000..604f53a47 --- /dev/null +++ b/go/core/internal/service/system/substratecache_test.go @@ -0,0 +1,133 @@ +package system + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The cache exists to make a ~1.6s walk happen less often. These pin the two +// properties that make it safe to do that: a stale answer is never presented as a +// fresh one, and concurrent identical questions cost one walk rather than many. +func TestSubstrateCache(t *testing.T) { + t.Run("recomputes once the entry has aged out", func(t *testing.T) { + cache := newSubstrateCache() + clock := time.Unix(1_800_000_000, 0) + cache.now = func() time.Time { return clock } + + calls := 0 + compute := func() (any, error) { + calls++ + return calls, nil + } + + first, firstAt, err := cache.get(t.Context(), "k", compute) + require.NoError(t, err) + assert.Equal(t, 1, first) + assert.Equal(t, clock, firstAt) + + // Inside the window: the same answer, and the same age — which is the point. + // A cache that reported `now` here would make a stale number look live. + clock = clock.Add(substrateCacheTTL / 2) + second, secondAt, err := cache.get(t.Context(), "k", compute) + require.NoError(t, err) + assert.Equal(t, 1, second, "should have been served from the cache") + assert.Equal(t, firstAt, secondAt, "a cached answer must report when it was computed") + assert.Equal(t, 1, calls) + + // Past the window: computed again, and the age moves with it. + clock = clock.Add(substrateCacheTTL) + third, thirdAt, err := cache.get(t.Context(), "k", compute) + require.NoError(t, err) + assert.Equal(t, 2, third) + assert.True(t, thirdAt.After(firstAt)) + assert.Equal(t, 2, calls) + }) + + t.Run("a different question is a different answer, not a stale one", func(t *testing.T) { + cache := newSubstrateCache() + calls := 0 + compute := func() (any, error) { + calls++ + return calls, nil + } + + _, _, err := cache.get(t.Context(), "actors|team||100||status|asc", compute) + require.NoError(t, err) + // Same read, different sort — a cache keyed too loosely would answer this + // with the previous order's rows. + _, _, err = cache.get(t.Context(), "actors|team||100||status|desc", compute) + require.NoError(t, err) + assert.Equal(t, 2, calls) + }) + + t.Run("concurrent identical requests share one walk", func(t *testing.T) { + cache := newSubstrateCache() + var mutex sync.Mutex + calls := 0 + release := make(chan struct{}) + + compute := func() (any, error) { + mutex.Lock() + calls++ + mutex.Unlock() + <-release + return "value", nil + } + + const callers = 8 + var waiting sync.WaitGroup + waiting.Add(callers) + for range callers { + go func() { + defer waiting.Done() + value, _, err := cache.get(context.Background(), "k", compute) + assert.NoError(t, err) + assert.Equal(t, "value", value) + }() + } + + // Let them all queue behind the one in flight, then finish it. + time.Sleep(50 * time.Millisecond) + close(release) + waiting.Wait() + + mutex.Lock() + defer mutex.Unlock() + assert.Equal(t, 1, calls, "the walk should have happened once for all callers") + }) + + t.Run("a failure is not cached", func(t *testing.T) { + cache := newSubstrateCache() + calls := 0 + compute := func() (any, error) { + calls++ + return nil, errors.New("ate-api is down") + } + + _, _, err := cache.get(t.Context(), "k", compute) + require.Error(t, err) + _, _, err = cache.get(t.Context(), "k", compute) + require.Error(t, err) + // Caching the failure would keep a recovered backend looking broken for as + // long as the entry lived. + assert.Equal(t, 2, calls) + }) + + t.Run("holds a bounded number of entries", func(t *testing.T) { + cache := newSubstrateCache() + for index := range substrateCacheEntries * 3 { + key := string(rune('a'+index%26)) + string(rune('0'+index/26)) + _, _, err := cache.get(t.Context(), key, func() (any, error) { return index, nil }) + require.NoError(t, err) + } + cache.mutex.Lock() + defer cache.mutex.Unlock() + assert.LessOrEqual(t, len(cache.entries), substrateCacheEntries) + }) +} diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index 084f8dfc7..a7223d678 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -42,7 +42,9 @@ import ( "github.com/kagent-dev/kagent/go/core/internal/grpcserver" "github.com/kagent-dev/kagent/go/core/internal/httpserver" agentservice "github.com/kagent-dev/kagent/go/core/internal/service/agent" + agenttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/agenttemplate" feedbackservice "github.com/kagent-dev/kagent/go/core/internal/service/feedback" + harnessservice "github.com/kagent-dev/kagent/go/core/internal/service/harness" memoryservice "github.com/kagent-dev/kagent/go/core/internal/service/memory" modelservice "github.com/kagent-dev/kagent/go/core/internal/service/model" prompttemplateservice "github.com/kagent-dev/kagent/go/core/internal/service/prompttemplate" @@ -51,6 +53,7 @@ import ( taskservice "github.com/kagent-dev/kagent/go/core/internal/service/task" toolservice "github.com/kagent-dev/kagent/go/core/internal/service/tool" common "github.com/kagent-dev/kagent/go/core/internal/utils" + a2agateway "github.com/kagent-dev/kagent/go/core/v2/a2agateway" "github.com/kagent-dev/kagent/go/core/v2/agentinstance" v2controller "github.com/kagent-dev/kagent/go/core/v2/controller" @@ -534,6 +537,19 @@ func Start(getExtensionConfig GetExtensionConfig, extraSources []migrations.Sour } agentInstanceService := agentinstance.NewService(dbClient, extensionCfg.Authorizer, instanceWorkflow) + atenetRouterURL := cfg.Substrate.AtenetRouterURL + if atenetRouterURL == "" { + atenetRouterURL = substrate.DefaultAtenetRouterURL + } + // Dials an instance's runtime through the atenet router, which is how the A2A + // gateway reaches a private actor: the instance's authority is not routable + // directly. + a2aGatewayDialer, err := a2agateway.NewRuntimeDialer(atenetRouterURL, extensionCfg.Authenticator) + if err != nil { + setupLog.Error(err, "unable to create A2A runtime dialer") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if metricsCertWatcher != nil { setupLog.Info("Adding metrics certificate watcher to manager") @@ -593,21 +609,22 @@ func Start(getExtensionConfig GetExtensionConfig, extraSources []migrations.Sour memoryService := memoryservice.NewService(dbClient) sessionService := sessionservice.NewService(dbClient) taskService := taskservice.NewService(dbClient) + agentTemplateService := agenttemplateservice.NewService(mgr.GetClient(), extensionCfg.Authorizer) + harnessService := harnessservice.NewService(mgr.GetClient(), extensionCfg.Authorizer) - httpServer, err := httpserver.NewHTTPServer(httpserver.ServerConfig{ - Router: router, - BindAddr: cfg.HttpServerAddr, - KubeClient: mgr.GetClient(), - DbClient: dbClient, - Authenticator: extensionCfg.Authenticator, - }) - if err != nil { - setupLog.Error(err, "unable to create HTTP server") - os.Exit(1) - } - if err := mgr.Add(httpServer); err != nil { - setupLog.Error(err, "unable to set up HTTP server") - os.Exit(1) + // A2A over gRPC, routed to an AgentInstance: the gateway reads the + // `x-kagent-agent-instance-{namespace,id}` metadata and dials that instance's + // own `a2a_authority` through the atenet router. + // + // An extension may supply its own; absent one this controller serves the + // gateway itself, so a browser that can list and create agents over gRPC-Web + // also has somewhere to send a message. The workflow is what lets the gateway + // suspend an instance when a turn completes — the same one the lifecycle RPCs + // use, rather than a second over the same client. + a2aHandler := extensionCfg.A2AHandler + if a2aHandler == nil { + a2aHandler = a2agateway.New(dbClient, extensionCfg.Authorizer, a2aGatewayDialer, + instanceWorkflow, cfg.A2ABaseUrl) } grpcServer, err := grpcserver.New(grpcserver.Config{ @@ -622,6 +639,8 @@ func Start(getExtensionConfig GetExtensionConfig, extraSources []migrations.Sour AgentService: agentService, ModelService: modelConfigService, ToolService: toolService, + AgentTemplateService: agentTemplateService, + HarnessService: harnessService, PromptTemplateService: promptTemplateService, SystemService: systemService, FeedbackService: feedbackService, @@ -629,7 +648,7 @@ func Start(getExtensionConfig GetExtensionConfig, extraSources []migrations.Sour SessionService: sessionService, TaskService: taskService, AgentInstanceService: agentInstanceService, - A2AHandler: extensionCfg.A2AHandler, + A2AHandler: a2aHandler, }) if err != nil { setupLog.Error(err, "unable to create gRPC server") @@ -640,6 +659,26 @@ func Start(getExtensionConfig GetExtensionConfig, extraSources []migrations.Sour os.Exit(1) } + httpServer, err := httpserver.NewHTTPServer(httpserver.ServerConfig{ + Router: router, + BindAddr: cfg.HttpServerAddr, + KubeClient: mgr.GetClient(), + DbClient: dbClient, + Authenticator: extensionCfg.Authenticator, + // Lets a browser reach the gRPC services over the same origin the app is + // served from; see grpcserver.WebHandler. Built after the gRPC server + // because it is that server's own rule about which requests are its. + GrpcWebRouter: grpcServer.WebHandlerOr, + }) + if err != nil { + setupLog.Error(err, "unable to create HTTP server") + os.Exit(1) + } + if err := mgr.Add(httpServer); err != nil { + setupLog.Error(err, "unable to set up HTTP server") + os.Exit(1) + } + // DB TTL cleanup (memory + sessions) runs only on the leader to avoid duplicate deletes. // Currently configured to run every 24 hours. if err := mgr.Add(httpserver.NewDbCleanupRunnable(dbClient, 24*time.Hour, cfg.Database.SessionRetentionDays)); err != nil { diff --git a/go/core/pkg/auth/share.go b/go/core/pkg/auth/share.go index 391491e77..d8baf924c 100644 --- a/go/core/pkg/auth/share.go +++ b/go/core/pkg/auth/share.go @@ -5,9 +5,26 @@ import "context" // ShareContext holds the context derived from a validated X-Share-Token header. type ShareContext struct { Token string // the raw share token - SessionID string // session this token grants access to + SessionID string // session this token grants access to, when it is a session share UserID string // owner's user ID — used for DB lookups ReadOnly bool // when true, only read operations are allowed + + // AgentInstanceID is the instance this token grants access to, when it is an + // AgentInstance share. + // + // Exactly one of SessionID and AgentInstanceID is set. They are two different + // kinds of share over two different resources — a session belongs to the older + // chat path, an instance is the conversation itself — and a single field would + // have the A2A gateway matching an id that named a session. + AgentInstanceID string +} + +// IsForAgentInstance reports whether this share grants access to the named instance. +// +// Asked rather than assumed: a session share reaching the A2A gateway must not be +// treated as authority over an instance that happens to share its id space. +func (s *ShareContext) IsForAgentInstance(instanceID string) bool { + return s != nil && s.AgentInstanceID != "" && s.AgentInstanceID == instanceID } type shareContextKeyType struct{} diff --git a/go/core/pkg/migrations/core/000017_agent_instance_name.down.sql b/go/core/pkg/migrations/core/000017_agent_instance_name.down.sql new file mode 100644 index 000000000..a0b66d34b --- /dev/null +++ b/go/core/pkg/migrations/core/000017_agent_instance_name.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE agent_instance + DROP COLUMN IF EXISTS name; diff --git a/go/core/pkg/migrations/core/000017_agent_instance_name.up.sql b/go/core/pkg/migrations/core/000017_agent_instance_name.up.sql new file mode 100644 index 000000000..0786dd47a --- /dev/null +++ b/go/core/pkg/migrations/core/000017_agent_instance_name.up.sql @@ -0,0 +1,6 @@ +-- A reader-supplied display name for the conversation an AgentInstance is. +-- Deliberately not unique: unlike a Kubernetes name this is a label for a human, +-- and two conversations with the same agent may reasonably carry the same title. +-- The default keeps the column additive — every existing row reads as unnamed. +ALTER TABLE agent_instance + ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT ''; diff --git a/go/core/pkg/sandboxbackend/substrate/lifecycle_shared.go b/go/core/pkg/sandboxbackend/substrate/lifecycle_shared.go index 931a4dd67..fcb4b692a 100644 --- a/go/core/pkg/sandboxbackend/substrate/lifecycle_shared.go +++ b/go/core/pkg/sandboxbackend/substrate/lifecycle_shared.go @@ -228,6 +228,28 @@ func sanitizeActorTemplateEnvVar(e corev1.EnvVar) *atev1alpha1.EnvVar { return &atev1alpha1.EnvVar{Name: e.Name, Value: e.Value} } +// secretValue reads a key from a Secret that may not have come from the API server. +// +// `Data` is the only field a Secret read from the cluster has populated, but +// `StringData` is write-only: the API server folds it into `Data` on create, so a +// Secret built in memory and never applied has `StringData` set and `Data` empty. +// The sandbox path passes exactly such a Secret — the translator builds the agent's +// config Secret and hands it straight to the actor-template builder — so reading only +// `Data` finds nothing and every sandbox agent fails to reconcile with "secret does +// not contain key config.json" about a Secret whose content is right there. +// +// Preferring `Data` keeps the cluster-read path byte-identical; the fallback only +// matters for the not-yet-applied case. +func secretValue(secret *corev1.Secret, key string) ([]byte, bool) { + if value, ok := secret.Data[key]; ok { + return value, true + } + if value, ok := secret.StringData[key]; ok { + return []byte(value), true + } + return nil, false +} + func resolvePodEnv(ctx context.Context, kube client.Reader, namespace string, env []corev1.EnvVar, localSecret *corev1.Secret) ([]corev1.EnvVar, error) { resolved := append([]corev1.EnvVar(nil), env...) for i, variable := range resolved { @@ -245,7 +267,7 @@ func resolvePodEnv(ctx context.Context, kube client.Reader, namespace string, en } return nil, err } - value, ok := secret.Data[ref.Key] + value, ok := secretValue(secret, ref.Key) if !ok { if ref.Optional != nil && *ref.Optional { resolved[i].ValueFrom = nil diff --git a/go/core/pkg/sandboxbackend/substrate/lifecycle_shared_secret_test.go b/go/core/pkg/sandboxbackend/substrate/lifecycle_shared_secret_test.go new file mode 100644 index 000000000..b03c6ad47 --- /dev/null +++ b/go/core/pkg/sandboxbackend/substrate/lifecycle_shared_secret_test.go @@ -0,0 +1,66 @@ +package substrate + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +// A Secret built in memory has StringData set and Data empty, because StringData is +// write-only and the API server folds it into Data on create. The sandbox path passes +// exactly such a Secret, so reading only Data found nothing and every sandbox agent +// failed to reconcile with "does not contain key config.json" about content that was +// present. This pins both halves. +func TestSecretValue(t *testing.T) { + tests := []struct { + name string + secret *corev1.Secret + key string + want string + wantOK bool + }{ + { + name: "a Secret read from the cluster has Data", + secret: &corev1.Secret{Data: map[string][]byte{"config.json": []byte("{\"from\":\"data\"}")}}, + key: "config.json", + want: "{\"from\":\"data\"}", + wantOK: true, + }, + { + name: "a Secret built in memory has only StringData", + secret: &corev1.Secret{StringData: map[string]string{"config.json": "{\"from\":\"stringdata\"}"}}, + key: "config.json", + want: "{\"from\":\"stringdata\"}", + wantOK: true, + }, + { + // Data is what the API server produced, so it wins where both are set. + name: "Data wins when both carry the key", + secret: &corev1.Secret{ + Data: map[string][]byte{"config.json": []byte("data")}, + StringData: map[string]string{"config.json": "stringdata"}, + }, + key: "config.json", + want: "data", + wantOK: true, + }, + { + name: "a key in neither is still absent", + secret: &corev1.Secret{StringData: map[string]string{"other": "x"}}, + key: "config.json", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := secretValue(tt.secret, tt.key) + if ok != tt.wantOK { + t.Fatalf("ok = %v, want %v", ok, tt.wantOK) + } + if ok && string(got) != tt.want { + t.Errorf("value = %q, want %q", string(got), tt.want) + } + }) + } +} diff --git a/go/core/pkg/sandboxbackend/substrate/list.go b/go/core/pkg/sandboxbackend/substrate/list.go index b01265752..ddbbec31a 100644 --- a/go/core/pkg/sandboxbackend/substrate/list.go +++ b/go/core/pkg/sandboxbackend/substrate/list.go @@ -6,6 +6,13 @@ import ( "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) +// actorPageSize is what ate-api is asked for per page. +// +// Its maximum: "values above 1000 are coerced to 1000". Left unset the server picks a +// much smaller default, and on a deployment holding 410,110 actors that is thousands of +// round trips for one walk — which measured at ~1.6s per read of the inventory. +const actorPageSize = 1000 + // ListActors returns all actors in the given atespace (empty atespace = all atespaces, // including substrate's reserved golden atespace). The list API is paginated — pages are // followed until the token drains, since a single page may miss actors. @@ -20,6 +27,7 @@ func (c *Client) ListActors(ctx context.Context, atespace string) ([]*ateapipb.A for { resp, err := c.ControlClient.ListActors(ctx, &ateapipb.ListActorsRequest{ Atespace: atespace, + PageSize: actorPageSize, PageToken: pageToken, }) if err != nil { @@ -33,6 +41,44 @@ func (c *Client) ListActors(ctx context.Context, atespace string) ([]*ateapipb.A } } +// EachActorPage calls visit with each page of actors as it arrives, instead of +// accumulating them. +// +// ListActors above collects every page into one slice, which is fine for a small +// cluster and fatal for a large one: a deployment reporting 410,110 actors put +// several hundred megabytes of protos in the controller and OOM-killed it. A +// caller that only needs to count, filter or take one page never has to hold them +// all, and this is how it avoids doing so. +// +// visit must not retain the slice it is given — the next page reuses nothing, but +// the actors themselves are only guaranteed to live as long as the call. Returning +// an error from visit stops the walk and is returned as-is. +func (c *Client) EachActorPage(ctx context.Context, atespace string, visit func([]*ateapipb.Actor) error) error { + if c == nil { + return nil + } + ctx, cancel := c.callCtx(ctx) + defer cancel() + pageToken := "" + for { + resp, err := c.ControlClient.ListActors(ctx, &ateapipb.ListActorsRequest{ + Atespace: atespace, + PageSize: actorPageSize, + PageToken: pageToken, + }) + if err != nil { + return err + } + if err := visit(resp.GetActors()); err != nil { + return err + } + pageToken = resp.GetNextPageToken() + if pageToken == "" { + return nil + } + } +} + // ListWorkers returns all workers reflected in ate-api. func (c *Client) ListWorkers(ctx context.Context) ([]*ateapipb.Worker, error) { if c == nil { diff --git a/go/core/v2/a2agateway/gateway.go b/go/core/v2/a2agateway/gateway.go index 341dd015f..f98697f16 100644 --- a/go/core/v2/a2agateway/gateway.go +++ b/go/core/v2/a2agateway/gateway.go @@ -48,6 +48,7 @@ type instanceStore interface { CreateAgentInstanceTask(context.Context, string, []byte, *a2atype.Task) (*a2atype.Task, bool, error) GetActiveAgentInstanceTask(context.Context, string) (*a2atype.Task, error) InterruptActiveAgentInstanceTask(context.Context, string, string) (bool, error) + AbandonActiveAgentInstanceTask(context.Context, string, string) (bool, error) StoreAgentInstanceTaskEvent(context.Context, string, *a2atype.Task, a2atype.Event, *dbpkg.AgentInstanceTaskSnapshot) error GetAgentInstanceTask(context.Context, string, string) (*a2atype.Task, error) ListAgentInstanceTasks(context.Context, string, string, a2atype.TaskState, *time.Time, int) ([]*a2atype.Task, int, error) @@ -125,6 +126,33 @@ func newGateway(store instanceStore, authorizer auth.Authorizer, dialer runtimeD } func (g *Gateway) instance(ctx context.Context, verb auth.Verb) (*apiv1alpha1.AgentInstance, error) { + instance, err := g.storedInstance(ctx, verb) + if err != nil { + return nil, err + } + if instance.GetState() != apiv1alpha1.AgentInstanceState_AGENT_INSTANCE_STATE_READY { + return nil, a2atype.NewError(a2atype.ErrUnsupportedOperation, fmt.Sprintf("AgentInstance is %s", instance.GetState())) + } + return instance, nil +} + +/* + * Resolves the routed instance, whatever state it is in. + * + * Authorization is unchanged — an instance is still read as its creator, and a share + * token still only widens reach to the instance it names. What is dropped is the + * readiness requirement, because it was never this function's to impose: a task list + * and a task come out of the store, and the store does not care whether the instance + * currently holds a worker. + * + * Requiring READY for those reads made a suspended conversation unreadable, which is a + * real problem now that conversations give their workers back at the end of every turn: + * opening one to re-read what was said reported "AgentInstance is + * AGENT_INSTANCE_STATE_SUSPENDED" as if the record had been lost. The alternative — + * resuming on open — would claim a worker every time somebody glanced at a transcript, + * which is exactly what suspending them was meant to stop. + */ +func (g *Gateway) storedInstance(ctx context.Context, verb auth.Verb) (*apiv1alpha1.AgentInstance, error) { namespace, id, err := route(ctx) if err != nil { return nil, a2atype.NewError(a2atype.ErrInvalidRequest, err.Error()) @@ -134,10 +162,27 @@ func (g *Gateway) instance(ctx context.Context, verb auth.Verb) (*apiv1alpha1.Ag return nil, a2atype.NewError(a2atype.ErrUnauthenticated, "authentication is required") } principal := session.Principal() - if err := g.authorizer.Check(ctx, principal, verb, auth.Resource{Type: "AgentInstance", Name: namespace + "/" + id}); err != nil { + + /* + * A share token is authority over one instance, and only that one. + * + * The visitor is still authenticated as themselves — a share widens what an + * account may reach, it does not replace authentication — so the ordinary + * authorization check is skipped only when the token names *this* instance, and + * the record is then read as its owner. Reading it as the visitor would find + * nothing, because an instance is scoped to its creator. + * + * The read-only half is enforced in the interceptor, which refuses a + * write-access RPC for a read-only share before this is reached. + */ + creator := principal.User.ID + share, hasShare := auth.ShareContextFrom(ctx) + if hasShare && share.IsForAgentInstance(id) { + creator = share.UserID + } else if err := g.authorizer.Check(ctx, principal, verb, auth.Resource{Type: "AgentInstance", Name: namespace + "/" + id}); err != nil { return nil, a2atype.NewError(a2atype.ErrUnauthorized, "not authorized") } - instance, err := g.store.GetAgentInstance(ctx, namespace, id, principal.User.ID) + instance, err := g.store.GetAgentInstance(ctx, namespace, id, creator) if errors.Is(err, dbpkg.ErrNotFound) { return nil, a2atype.NewError(a2atype.ErrUnauthorized, "not authorized") } @@ -145,9 +190,6 @@ func (g *Gateway) instance(ctx context.Context, verb auth.Verb) (*apiv1alpha1.Ag ctrllog.FromContext(ctx).Error(err, "failed to load AgentInstance", "namespace", namespace, "id", id) return nil, a2atype.NewError(a2atype.ErrInternalError, "failed to load AgentInstance") } - if instance.GetState() != apiv1alpha1.AgentInstanceState_AGENT_INSTANCE_STATE_READY { - return nil, a2atype.NewError(a2atype.ErrUnsupportedOperation, fmt.Sprintf("AgentInstance is %s", instance.GetState())) - } return instance, nil } @@ -168,7 +210,7 @@ func route(ctx context.Context) (namespace, id string, err error) { } func (g *Gateway) GetTask(ctx context.Context, req *a2atype.GetTaskRequest) (*a2atype.Task, error) { - instance, err := g.instance(ctx, auth.VerbGet) + instance, err := g.storedInstance(ctx, auth.VerbGet) if err != nil { return nil, err } @@ -187,7 +229,7 @@ func (g *Gateway) GetTask(ctx context.Context, req *a2atype.GetTaskRequest) (*a2 } func (g *Gateway) ListTasks(ctx context.Context, req *a2atype.ListTasksRequest) (*a2atype.ListTasksResponse, error) { - instance, err := g.instance(ctx, auth.VerbGet) + instance, err := g.storedInstance(ctx, auth.VerbGet) if err != nil { return nil, err } @@ -250,7 +292,17 @@ func (g *Gateway) CancelTask(ctx context.Context, req *a2atype.CancelTaskRequest defer client.Destroy() canceled, err := client.CancelTask(ctx, req) if err != nil { - return nil, err + // A cancel the reader asked for has to free the conversation even when the + // runtime cannot help — it may have no record of the task, or be + // unreachable. Without this an instance whose turn is parked or stranded + // stays unable to answer with no way out, which is the defect this path + // exists to escape. The store only acts while the task is still the active + // one, so a turn that has already finished is untouched. + local, localErr := g.cancelTaskLocally(ctx, instance.GetId(), req.ID) + if localErr != nil || local == nil { + return nil, err + } + return local, nil } if err := validateTaskInfo(canceled, task); err != nil { return nil, a2atype.NewError(a2atype.ErrInternalError, err.Error()) @@ -383,10 +435,17 @@ func (g *Gateway) GetExtendedAgentCard(ctx context.Context, _ *a2atype.GetExtend } // The compiled card provides immutable template metadata. Public transport, - // capabilities, security, and signatures belong to the gateway instead of - // the private runtime that produced that card. + // security, and signatures belong to the gateway instead of the private + // runtime that produced that card. card.SupportedInterfaces = []*a2atype.AgentInterface{a2atype.NewAgentInterface(g.gatewayURL, a2atype.TransportProtocolGRPC)} - card.Capabilities = a2atype.AgentCapabilities{Streaming: true, ExtendedAgentCard: true} + // Extensions are the exception, and replacing the whole capabilities struct + // used to drop them. They describe what the runtime behind this gateway can + // negotiate — human-in-the-loop among them — which is not the gateway's to + // erase. A client discovers HITL by reading this card, so wiping it made + // answering an agent's question undiscoverable while the card still rendered + // perfectly. + extensions := card.Capabilities.Extensions + card.Capabilities = a2atype.AgentCapabilities{Streaming: true, ExtendedAgentCard: true, Extensions: extensions} card.SecurityRequirements = nil card.SecuritySchemes = nil card.Signatures = nil @@ -483,6 +542,13 @@ func (g *Gateway) reconcileActiveTask(ctx context.Context, instance *apiv1alpha1 if err != nil { return err } + // A parked turn needs no runtime round trip: its state already says the + // runtime stopped and is waiting on a human. Nothing here may clear it — the + // question is still answerable, and only the reader can decide to give it up, + // which they do with CancelTask. + if dbpkg.TaskParkedAwaitingUser(active.Status.State) { + return errParkedTaskHoldsSlot + } client, err := g.dialer.Dial(ctx, instance) if err != nil { ctrllog.FromContext(ctx).Error(err, "failed to reconcile active AgentInstance task", "task", active.ID) @@ -495,6 +561,16 @@ func (g *Gateway) reconcileActiveTask(ctx context.Context, instance *apiv1alpha1 for event, eventErr := range client.SubscribeToTask(ctx, &a2atype.SubscribeToTaskRequest{ID: active.ID}) { if errors.Is(eventErr, a2atype.ErrTaskNotFound) { latest, err := client.GetTask(ctx, &a2atype.GetTaskRequest{ID: active.ID}) + // A runtime that has no record of the task at all is ambiguous: the task + // may never have been dispatched, in which case the dispatch is still + // coming and interrupting it would race. Age is the only discriminator — + // past the dispatch grace period no dispatch can still be in flight, so + // the slot is stale rather than contended, and without this an instance + // stranded by a lost runtime record could never answer again. A task with + // no status timestamp has an unknown age and stays untouched. + if errors.Is(err, a2atype.ErrTaskNotFound) && staleBeyondDispatch(active) { + return g.interruptTask(ctx, instance.GetId(), active.ID) + } if err != nil || latest == nil { return dbpkg.ErrAgentInstanceTaskConflict } @@ -600,6 +676,28 @@ func taskForEvent(task *a2atype.Task, event a2atype.Event) (*a2atype.Task, error if err != nil { return nil, a2atype.NewError(a2atype.ErrInternalError, fmt.Sprintf("apply runtime task event: %v", err)) } + /* + * The runtime may send a whole task, and it does not always remember as much as + * the store does. + * + * `ApplyUpdate` takes the runtime's version where one is given, which is right for + * status and artifacts and wrong for history: a runtime that has been quiesced and + * resumed can answer with a task carrying no history at all, and persisting that + * replaces a transcript with an empty one. That is not a display problem — the + * messages are gone from the record, and the conversation opens blank. + * + * Seen doing exactly that: a conversation parked on a question, answered after the + * runtime had been suspended, came back as an eighty-byte task while its six events + * sat untouched in the store beside it. + * + * So history only ever grows here. A runtime that genuinely has more is believed; + * one that has less is not allowed to forget on the store's behalf. + */ + if len(updated.History) < len(task.History) { + kept := *updated + kept.History = task.History + return &kept, nil + } return updated, nil } @@ -646,10 +744,46 @@ func (g *Gateway) failAttempt(ctx context.Context, attempt *preparedSend) { } } +// errParkedTaskHoldsSlot means the instance's active task is waiting on a human, +// not executing. It is reported rather than cleared: the pending question is +// valid, and the reader may still want to answer it. Discarding it silently to +// make room for an unrelated message would throw away the thing the agent is +// waiting for. +var errParkedTaskHoldsSlot = errors.New("AgentInstance task is waiting for a reply") + +// dispatchGracePeriod bounds how long a task the runtime has never heard of may +// still be mid-dispatch. Well above any real dispatch, so a live one is never +// interrupted, and short enough that a reader is not locked out of their own +// conversation for long. +const dispatchGracePeriod = 5 * time.Minute + +func staleBeyondDispatch(task *a2atype.Task) bool { + if task.Status.Timestamp == nil { + return false + } + return time.Since(*task.Status.Timestamp) > dispatchGracePeriod +} + +func (g *Gateway) cancelTaskLocally(ctx context.Context, instanceID string, taskID a2atype.TaskID) (*a2atype.Task, error) { + canceled, err := g.store.AbandonActiveAgentInstanceTask(ctx, instanceID, string(taskID)) + if err != nil || !canceled { + return nil, err + } + ctrllog.FromContext(ctx).Info("recorded AgentInstance task cancellation without the runtime", "instance", instanceID, "task", taskID) + return g.store.GetAgentInstanceTask(ctx, instanceID, string(taskID)) +} + func (g *Gateway) storeError(ctx context.Context, err error) error { if errors.Is(err, dbpkg.ErrIdempotencyConflict) { return a2atype.NewError(a2atype.ErrInvalidRequest, "message ID was already used with a different request") } + if errors.Is(err, errParkedTaskHoldsSlot) { + // Naming the way out matters: saying only that a task was active is why a + // conversation waiting on an unanswered question read as a broken agent + // rather than as one waiting for the reader. + return a2atype.NewError(a2atype.ErrUnsupportedOperation, + "the agent is waiting for a reply to its last message; answer it, or cancel that task to start a new one") + } if errors.Is(err, dbpkg.ErrAgentInstanceTaskConflict) { return a2atype.NewError(a2atype.ErrUnsupportedOperation, "AgentInstance already has an active task") } diff --git a/go/core/v2/a2agateway/gateway_test.go b/go/core/v2/a2agateway/gateway_test.go index fa5bab03f..8464fcfb1 100644 --- a/go/core/v2/a2agateway/gateway_test.go +++ b/go/core/v2/a2agateway/gateway_test.go @@ -48,6 +48,11 @@ type gatewayTestStore struct { active *a2atype.Task interruptResult bool interrupted bool + abandonResult bool + abandoned bool + claimed *a2atype.Task + restored *a2atype.Task + createdTasks int stored []a2atype.Event snapshot *dbpkg.AgentInstanceTaskSnapshot onStore func() @@ -91,6 +96,7 @@ func (s *gatewayTestStore) CreateAgentInstanceTask(_ context.Context, _ string, } s.task = task s.active = task + s.createdTasks++ s.stored = append(s.stored, task.History[0]) return task, true, nil } @@ -111,8 +117,48 @@ func (s *gatewayTestStore) InterruptActiveAgentInstanceTask(_ context.Context, _ return true, nil } -func (s *gatewayTestStore) GetAgentInstanceTask(context.Context, string, string) (*a2atype.Task, error) { - return s.task, s.taskErr +func (s *gatewayTestStore) AbandonActiveAgentInstanceTask(_ context.Context, _ string, taskID string) (bool, error) { + if !s.abandonResult || s.active == nil || string(s.active.ID) != taskID { + return false, nil + } + canceled := *s.active + canceled.Status = a2atype.TaskStatus{State: a2atype.TaskStateCanceled} + s.task = &canceled + s.active = nil + s.abandoned = true + return true, nil +} + +func (s *gatewayTestStore) ClaimParkedAgentInstanceTask(_ context.Context, _ string, taskID string) (*a2atype.Task, bool, error) { + if s.active == nil { + return nil, false, dbpkg.ErrNotFound + } + if string(s.active.ID) != taskID || !dbpkg.TaskParkedAwaitingUser(s.active.Status.State) { + return nil, false, nil + } + parked := *s.active + working := *s.active + working.Status = a2atype.TaskStatus{State: a2atype.TaskStateWorking} + s.active = &working + s.claimed = &parked + return &parked, true, nil +} + +func (s *gatewayTestStore) RestoreParkedAgentInstanceTask(_ context.Context, _ string, task *a2atype.Task) error { + restored := *task + s.active = &restored + s.restored = &restored + return nil +} + +func (s *gatewayTestStore) GetAgentInstanceTask(_ context.Context, _ string, taskID string) (*a2atype.Task, error) { + if s.taskErr != nil { + return nil, s.taskErr + } + if s.task == nil || string(s.task.ID) != taskID { + return nil, dbpkg.ErrNotFound + } + return s.task, nil } func (s *gatewayTestStore) ListAgentInstanceTasks(context.Context, string, string, a2atype.TaskState, *time.Time, int) ([]*a2atype.Task, int, error) { @@ -165,6 +211,17 @@ type gatewayTestRuntime struct { subscribeEvent a2atype.Event subscribeErr error privateTask *a2atype.Task + subscribeCalls int + cancelErr error + sendCalls int + sentTaskID a2atype.TaskID +} + +func (r *gatewayTestRuntime) CancelTask(context.Context, a2aclient.ServiceParams, *a2atype.CancelTaskRequest) (*a2atype.Task, error) { + if r.cancelErr != nil { + return nil, r.cancelErr + } + return r.task, nil } func (r *gatewayTestRuntime) GetTask(context.Context, a2aclient.ServiceParams, *a2atype.GetTaskRequest) (*a2atype.Task, error) { @@ -177,6 +234,7 @@ func (r *gatewayTestRuntime) GetTask(context.Context, a2aclient.ServiceParams, * } func (r *gatewayTestRuntime) SubscribeToTask(context.Context, a2aclient.ServiceParams, *a2atype.SubscribeToTaskRequest) iter.Seq2[a2atype.Event, error] { + r.subscribeCalls++ return func(yield func(a2atype.Event, error) bool) { if r.subscribeEvent != nil || r.subscribeErr != nil { yield(r.subscribeEvent, r.subscribeErr) @@ -187,6 +245,8 @@ func (r *gatewayTestRuntime) SubscribeToTask(context.Context, a2aclient.ServiceP func (r *gatewayTestRuntime) SendMessage(_ context.Context, _ a2aclient.ServiceParams, req *a2atype.SendMessageRequest) (a2atype.SendMessageResult, error) { r.sent = true r.privateTask, _ = apia2a.TakeStoredTask(req.Message) + r.sendCalls++ + r.sentTaskID = req.Message.TaskID return &a2atype.Task{ID: req.Message.TaskID, ContextID: req.Message.ContextID, Status: a2atype.TaskStatus{State: a2atype.TaskStateCompleted}}, nil } @@ -489,6 +549,88 @@ func TestGatewayReadsTasksWithoutDialingRuntime(t *testing.T) { } } +/* + * A suspended conversation is still readable, and sending to it is still refused. + * + * Both halves matter and they used to be one rule. Every RPC resolved the instance + * through a helper that insisted on READY, which is right for anything needing the + * worker and wrong for a task list — that comes out of the store, which does not care + * whether a worker is attached. + * + * It became a real fault once conversations started giving their workers back at the + * end of every turn: opening one to re-read what was said reported "AgentInstance is + * AGENT_INSTANCE_STATE_SUSPENDED" as though the transcript had been lost. Resuming on + * open would have claimed a worker every time somebody glanced at one, which is the + * thing suspending them exists to avoid. + */ +func TestGatewayReadsTasksWhileSuspended(t *testing.T) { + instance := gatewayTestInstance() + instance.State = apiv1alpha1.AgentInstanceState_AGENT_INSTANCE_STATE_SUSPENDED + task := &a2atype.Task{ID: gatewayTestID, ContextID: gatewayTestID} + store := &gatewayTestStore{instance: instance, task: task, tasks: []*a2atype.Task{task}, total: 1} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{}, &gatewayTestWorkflow{}, gatewayTestURL) + + if _, err := gateway.ListTasks(gatewayTestContext(), &a2atype.ListTasksRequest{}); err != nil { + t.Fatalf("ListTasks() on a suspended instance = %v, want the stored transcript", err) + } + if _, err := gateway.GetTask(gatewayTestContext(), &a2atype.GetTaskRequest{ID: task.ID}); err != nil { + t.Fatalf("GetTask() on a suspended instance = %v, want the stored task", err) + } + + // The other half: what needs the worker is still refused, naming the state. Without + // this the change would read as "suspended no longer means anything". + if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()); err == nil { + t.Fatal("SendMessage() to a suspended instance succeeded, want a refusal naming the state") + } +} + +/* + * A runtime that has forgotten the conversation must not erase it. + * + * `ApplyUpdate` takes the runtime's task where one is sent, which is right for status + * and artifacts and wrong for history. A runtime that has been quiesced and resumed can + * answer with a task carrying no history at all — and persisting that replaces the + * transcript with an empty one, so the conversation opens blank on the next read while + * its events sit untouched in the store beside it. + * + * That is what a conversation parked on a question did after being answered: an + * eighty-byte task in place of everything that had been said. + */ +func TestGatewayKeepsHistoryARuntimeHasForgotten(t *testing.T) { + stored := &a2atype.Task{ + ID: gatewayTestID, ContextID: gatewayTestID, + History: []*a2atype.Message{{ID: "one"}, {ID: "two"}, {ID: "three"}}, + } + forgetful := &a2atype.Task{ + ID: gatewayTestID, ContextID: gatewayTestID, + Status: a2atype.TaskStatus{State: a2atype.TaskStateFailed}, + } + + updated, err := taskForEvent(stored, forgetful) + if err != nil { + t.Fatal(err) + } + if len(updated.History) != len(stored.History) { + t.Fatalf("history = %d messages, want the stored %d kept", len(updated.History), len(stored.History)) + } + // The rest of the runtime's answer is still believed — this keeps history, it does + // not ignore the update. + if updated.Status.State != a2atype.TaskStateFailed { + t.Fatalf("state = %s, want the runtime's %s", updated.Status.State, a2atype.TaskStateFailed) + } + + // And a runtime with more to say is believed about that too, or an agent could + // never add to a transcript at all. + richer := &a2atype.Task{ + ID: gatewayTestID, ContextID: gatewayTestID, + History: []*a2atype.Message{{ID: "one"}, {ID: "two"}, {ID: "three"}, {ID: "four"}}, + } + grown, err := taskForEvent(stored, richer) + if err != nil || len(grown.History) != 4 { + t.Fatalf("history = %d messages (%v), want the runtime's 4", len(grown.History), err) + } +} + func TestGatewayBuildsAgentCardFromPinnedRevision(t *testing.T) { store := &gatewayTestStore{ instance: gatewayTestInstance(), @@ -497,7 +639,7 @@ func TestGatewayBuildsAgentCardFromPinnedRevision(t *testing.T) { AgentCard: []byte(`{ "name":"assistant","description":"pinned description","version":"v1", "supportedInterfaces":[{"url":"http://127.0.0.1:80","protocolBinding":"GRPC","protocolVersion":"1.0"}], - "capabilities":{"pushNotifications":true},"skills":[], + "capabilities":{"pushNotifications":true,"extensions":[{"uri":"https://kagent.dev/extensions/hitl/v1","required":false}]},"skills":[], "defaultInputModes":["text"],"defaultOutputModes":["text"] }`), }, @@ -520,6 +662,13 @@ func TestGatewayBuildsAgentCardFromPinnedRevision(t *testing.T) { if !card.Capabilities.Streaming || !card.Capabilities.ExtendedAgentCard || card.Capabilities.PushNotifications { t.Fatalf("gateway capabilities = %#v", card.Capabilities) } + // Transport and streaming are the gateway's to state, but extensions describe + // what the runtime can negotiate. Replacing the whole struct used to drop them, + // which left a client no way to discover that an agent's question is answerable + // while the card still looked complete. + if len(card.Capabilities.Extensions) != 1 || card.Capabilities.Extensions[0].URI != "https://kagent.dev/extensions/hitl/v1" { + t.Fatalf("runtime extensions = %#v, want the runtime's own preserved", card.Capabilities.Extensions) + } if authorizer.verb != auth.VerbGet || dialer.instance != nil { t.Fatalf("authorization verb = %q, runtime dialed = %v", authorizer.verb, dialer.instance != nil) } @@ -803,3 +952,349 @@ func TestGatewayRejectsConflictingMessageIDWithoutDialing(t *testing.T) { t.Fatal("conflicting message dialed the private runtime") } } + +// A denying authorizer, so "the share is what let this through" is provable rather +// than merely consistent with the result. +type gatewayDenyAuthorizer struct{ called bool } + +func (a *gatewayDenyAuthorizer) Check(context.Context, auth.Principal, auth.Verb, auth.Resource) error { + a.called = true + return errors.New("denied") +} + +/* + * Share links over an AgentInstance. + * + * The instance *is* the conversation, so sharing one is sharing what was said. Two + * things have to hold, and neither is implied by the other: + * + * - the share is authority over its own instance, and the record is read as the + * *owner* — an instance is scoped to its creator, so reading it as the visitor + * finds nothing and the link would 404; + * - the share is authority over nothing else, so a token for one instance cannot + * open another. + */ +func TestGatewayHonoursAgentInstanceShare(t *testing.T) { + instance := gatewayTestInstance() + store := &gatewayTestStore{instance: instance} + authorizer := &gatewayDenyAuthorizer{} + runtime := &gatewayTestRuntime{} + gateway := New(store, authorizer, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + ctx := auth.AuthSessionTo(t.Context(), gatewayTestSession{}) + ctx = auth.ShareContextTo(ctx, &auth.ShareContext{ + Token: "share", + UserID: "the-owner", + AgentInstanceID: instance.GetId(), + ReadOnly: true, + }) + ctx = metadata.NewIncomingContext(ctx, metadata.Pairs( + AgentInstanceNamespaceHeader, instance.GetNamespace(), + AgentInstanceIDHeader, instance.GetId(), + )) + + if _, err := gateway.ListTasks(ctx, &a2atype.ListTasksRequest{}); err != nil { + t.Fatalf("ListTasks() with a share for this instance = %v", err) + } + // Read as the owner: the visitor is somebody else, and an instance is scoped to + // its creator. + if store.userID != "the-owner" { + t.Fatalf("instance read as %q, want the share's owner", store.userID) + } + if authorizer.called { + t.Fatal("the ordinary authorization check should be skipped for a matching share") + } +} + +func TestGatewayRefusesAShareForADifferentInstance(t *testing.T) { + instance := gatewayTestInstance() + store := &gatewayTestStore{instance: instance} + authorizer := &gatewayDenyAuthorizer{} + gateway := New(store, authorizer, &gatewayTestDialer{}, &gatewayTestWorkflow{}, gatewayTestURL) + + ctx := auth.AuthSessionTo(t.Context(), gatewayTestSession{}) + // A perfectly valid share — for something else. + ctx = auth.ShareContextTo(ctx, &auth.ShareContext{ + Token: "share", + UserID: "the-owner", + AgentInstanceID: "00000000-0000-0000-0000-000000000000", + }) + ctx = metadata.NewIncomingContext(ctx, metadata.Pairs( + AgentInstanceNamespaceHeader, instance.GetNamespace(), + AgentInstanceIDHeader, instance.GetId(), + )) + + if _, err := gateway.ListTasks(ctx, &a2atype.ListTasksRequest{}); err == nil { + t.Fatal("a share for another instance opened this one") + } + if !authorizer.called { + t.Fatal("a non-matching share must fall through to the ordinary check") + } +} + +// A *session* share must not read as authority over an instance, however its id +// happens to be spelled. The two are separate fields for exactly this reason. +func TestGatewayIgnoresASessionShare(t *testing.T) { + instance := gatewayTestInstance() + authorizer := &gatewayDenyAuthorizer{} + gateway := New(&gatewayTestStore{instance: instance}, authorizer, &gatewayTestDialer{}, &gatewayTestWorkflow{}, gatewayTestURL) + + ctx := auth.AuthSessionTo(t.Context(), gatewayTestSession{}) + ctx = auth.ShareContextTo(ctx, &auth.ShareContext{ + Token: "share", + UserID: "the-owner", + SessionID: instance.GetId(), + }) + ctx = metadata.NewIncomingContext(ctx, metadata.Pairs( + AgentInstanceNamespaceHeader, instance.GetNamespace(), + AgentInstanceIDHeader, instance.GetId(), + )) + + if _, err := gateway.ListTasks(ctx, &a2atype.ListTasksRequest{}); err == nil { + t.Fatal("a session share opened an AgentInstance") + } + if !authorizer.called { + t.Fatal("a session share must fall through to the ordinary check") + } +} + +// TestGatewayRefusesButPreservesAParkedTurn is the reproduced defect and the +// decision about it. A turn that ends INPUT_REQUIRED holds the instance's single +// active-task slot, so every later send was refused as "already has an active +// task" — which reads as a broken agent. But that turn is a *valid pending +// question* (`ask_user` is a long-running call), so the send must be refused with +// a reason the reader can act on, and the question must survive: only the reader +// may give it up. +func TestGatewayRefusesButPreservesAParkedTurn(t *testing.T) { + for _, test := range []struct { + name string + state a2atype.TaskState + wantSent bool + wantQueries int + }{ + {name: "input required", state: a2atype.TaskStateInputRequired, wantSent: false, wantQueries: 0}, + {name: "auth required", state: a2atype.TaskStateAuthRequired, wantSent: false, wantQueries: 0}, + // A turn the runtime is still executing keeps the slot too, but for the + // other reason: an execution really is in flight. + {name: "working is still live", state: a2atype.TaskStateWorking, wantSent: false, wantQueries: 1}, + {name: "submitted is still live", state: a2atype.TaskStateSubmitted, wantSent: false, wantQueries: 1}, + } { + t.Run(test.name, func(t *testing.T) { + active := &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: test.state}} + runtime := &gatewayTestRuntime{subscribeEvent: active} + store := &gatewayTestStore{instance: gatewayTestInstance(), active: active, abandonResult: true, interruptResult: true} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()) + if (err == nil) != test.wantSent { + t.Fatalf("SendMessage() error = %v, want sent %t", err, test.wantSent) + } + // The pending question must still be there afterwards. + if store.abandoned || store.interrupted || store.active != active { + t.Fatalf("the parked turn was discarded: abandoned=%v interrupted=%v active=%#v", store.abandoned, store.interrupted, store.active) + } + // A parked turn is diagnosed without asking the runtime anything: its + // state already says no execution is in flight. Counting dials cannot + // show this, so count the reconcile's own round trip. + if runtime.subscribeCalls != test.wantQueries || runtime.getTaskCalls != 0 { + t.Fatalf("runtime queries: subscribe = %d (want %d), GetTask = %d (want 0)", runtime.subscribeCalls, test.wantQueries, runtime.getTaskCalls) + } + if dbpkg.TaskParkedAwaitingUser(test.state) && !strings.Contains(err.Error(), "waiting for a reply") { + // The old wording named only the symptom, so a conversation waiting on + // the reader was indistinguishable from a wedged one. + t.Fatalf("refusal for a parked turn = %q, want it to say what the agent is waiting for", err) + } + }) + } +} + +// TestGatewayCancelTaskFreesAConversationTheRuntimeCannotHelpWith pins the +// deliberate recovery. Cancel is the reader choosing to give up a pending +// question, and it has to work even when the runtime has no record of the task — +// otherwise a parked or stranded turn leaves the conversation unable to answer +// with no way out. +func TestGatewayCancelTaskFreesAConversationTheRuntimeCannotHelpWith(t *testing.T) { + for _, test := range []struct { + name string + abandonResult bool + wantErr bool + wantAbandoned bool + }{ + {name: "the active turn is canceled locally", abandonResult: true, wantErr: false, wantAbandoned: true}, + // Nothing to cancel means the runtime's own error is the honest answer. + {name: "a turn that already finished is left to the runtime error", abandonResult: false, wantErr: true, wantAbandoned: false}, + } { + t.Run(test.name, func(t *testing.T) { + parked := &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired}} + runtime := &gatewayTestRuntime{cancelErr: a2atype.ErrTaskNotFound} + store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked, abandonResult: test.abandonResult} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + _, err := gateway.CancelTask(gatewayTestContext(), &a2atype.CancelTaskRequest{ID: parked.ID}) + if (err != nil) != test.wantErr { + t.Fatalf("CancelTask() error = %v, want error %t", err, test.wantErr) + } + if store.abandoned != test.wantAbandoned { + t.Fatalf("abandoned = %v, want %t", store.abandoned, test.wantAbandoned) + } + }) + } +} + +// TestGatewaySendAfterCancellingAParkedTurnSucceeds is the whole recovery, end to +// end: refused while the question stands, accepted once the reader cancels it. +func TestGatewaySendAfterCancellingAParkedTurnSucceeds(t *testing.T) { + parked := &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired}} + runtime := &gatewayTestRuntime{cancelErr: a2atype.ErrTaskNotFound} + store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked, abandonResult: true} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()); err == nil { + t.Fatal("SendMessage() was accepted while a question was pending") + } + if _, err := gateway.CancelTask(gatewayTestContext(), &a2atype.CancelTaskRequest{ID: parked.ID}); err != nil { + t.Fatal(err) + } + if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()); err != nil { + t.Fatalf("SendMessage() after cancelling the parked turn = %v", err) + } +} + +// TestGatewayReapsAStaleSlotOnlyOnceDispatchCannotBeInFlight pins both halves of +// the age gate. A task the runtime has never heard of may simply not have been +// dispatched yet, so interrupting a fresh one races the dispatch; an old one +// cannot still be arriving, and leaving it would make the instance permanently +// unable to answer. +func TestGatewayReapsAStaleSlotOnlyOnceDispatchCannotBeInFlight(t *testing.T) { + stale := time.Now().Add(-dispatchGracePeriod - time.Minute) + fresh := time.Now() + for _, test := range []struct { + name string + timestamp *time.Time + wantInterrupted bool + }{ + {name: "older than the grace period is reaped", timestamp: &stale, wantInterrupted: true}, + {name: "within the grace period is left alone", timestamp: &fresh, wantInterrupted: false}, + {name: "an unknown age is left alone", timestamp: nil, wantInterrupted: false}, + } { + t.Run(test.name, func(t *testing.T) { + active := &a2atype.Task{ + ID: "active", ContextID: gatewayTestID, + Status: a2atype.TaskStatus{State: a2atype.TaskStateWorking, Timestamp: test.timestamp}, + } + runtime := &gatewayTestRuntime{taskErr: a2atype.ErrTaskNotFound, subscribeErr: a2atype.ErrTaskNotFound} + store := &gatewayTestStore{instance: gatewayTestInstance(), active: active, interruptResult: true} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()) + if (err == nil) != test.wantInterrupted { + t.Fatalf("SendMessage() error = %v, want reaped %t", err, test.wantInterrupted) + } + if store.interrupted != test.wantInterrupted { + t.Fatalf("interrupted = %v, want %t", store.interrupted, test.wantInterrupted) + } + }) + } +} + +func gatewayTestParkedTask() *a2atype.Task { + return &a2atype.Task{ + ID: "parked", ContextID: gatewayTestID, + Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired}, + } +} + +func gatewayTestReply(taskID a2atype.TaskID) *a2atype.SendMessageRequest { + message := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart("Medium")) + message.TaskID = taskID + return &a2atype.SendMessageRequest{Message: message} +} + +func TestGatewayRefusesAReplyThatCannotBeDelivered(t *testing.T) { + for _, test := range []struct { + name string + // known is the task the store can find by id, which is what separates an + // unknown task from one that exists and is simply past answering. + known *a2atype.Task + active *a2atype.Task + taskID a2atype.TaskID + wantNotFound bool + }{ + { + // The replay guard: a duplicate reply finds the turn already moved on. + // Reporting that as "task not found" — which it used to — is a lie about a + // task sitting in the reader's own transcript. + name: "a turn already working is no longer waiting", + known: &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateWorking}}, + active: &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateWorking}}, + taskID: "parked", + wantNotFound: false, + }, + { + name: "a reply naming a task that does not exist", + active: gatewayTestParkedTask(), + taskID: "no-such-task", + wantNotFound: true, + }, + { + name: "a reply with no turn to answer at all", + active: nil, + taskID: "parked", + wantNotFound: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + runtime := &gatewayTestRuntime{} + store := &gatewayTestStore{instance: gatewayTestInstance(), active: test.active, task: test.known} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(test.taskID)) + if err == nil { + t.Fatal("SendMessage() accepted a reply it could not deliver") + } + if errors.Is(err, a2atype.ErrTaskNotFound) != test.wantNotFound { + t.Fatalf("refusal = %v, want task-not-found %t", err, test.wantNotFound) + } + if runtime.sent || store.createdTasks != 0 { + t.Fatalf("undeliverable reply: reached runtime = %v, tasks reserved = %d", runtime.sent, store.createdTasks) + } + }) + } +} + +// TestGatewayRepliedTwiceDeliversOnce is the replay guard measured rather than +// reasoned about: the same answer sent twice must reach the runtime once. +func TestGatewayRepliedTwiceDeliversOnce(t *testing.T) { + parked := gatewayTestParkedTask() + runtime := &gatewayTestRuntime{} + store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) + + if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(parked.ID)); err != nil { + t.Fatal(err) + } + if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(parked.ID)); err == nil { + t.Fatal("the same reply was accepted twice") + } + if runtime.sendCalls != 1 { + t.Fatalf("runtime received %d sends, want exactly 1", runtime.sendCalls) + } +} + +// TestGatewayRestoresTheQuestionWhenAReplyCannotBeDelivered keeps a transport +// failure from turning an answerable question into a dead turn. +func TestGatewayRestoresTheQuestionWhenAReplyCannotBeDelivered(t *testing.T) { + parked := gatewayTestParkedTask() + store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked} + gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{err: errors.New("runtime unavailable")}, &gatewayTestWorkflow{}, gatewayTestURL) + + if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(parked.ID)); err == nil { + t.Fatal("SendMessage() reported success with no runtime") + } + // The last thing written must put the question back where a reader can answer + // it. Failing to deliver an answer does not make the question unanswerable, and + // leaving the claimed task behind would stop the conversation for good. + if store.task == nil || !dbpkg.TaskParkedAwaitingUser(store.task.Status.State) { + t.Fatalf("task left behind = %#v, want the question back awaiting the reader", store.task) + } +} diff --git a/go/core/v2/agentinstance/grpc.go b/go/core/v2/agentinstance/grpc.go index 3e768a001..70ab915da 100644 --- a/go/core/v2/agentinstance/grpc.go +++ b/go/core/v2/agentinstance/grpc.go @@ -19,7 +19,7 @@ func RegisterGRPC(registrar grpc.ServiceRegistrar, service *Service) { } func (s *grpcServer) CreateAgentInstance(ctx context.Context, request *apiv1alpha1.CreateAgentInstanceRequest) (*apiv1alpha1.CreateAgentInstanceResponse, error) { - instance, err := s.service.Create(ctx, request.GetNamespace(), request.GetHarness(), request.GetAgentTemplate(), request.GetRequestId()) + instance, err := s.service.Create(ctx, request.GetNamespace(), request.GetHarness(), request.GetAgentTemplate(), request.GetRequestId(), request.GetName()) if err != nil { return nil, err } @@ -37,6 +37,7 @@ func (s *grpcServer) GetAgentInstance(ctx context.Context, request *apiv1alpha1. func (s *grpcServer) ListAgentInstances(ctx context.Context, request *apiv1alpha1.ListAgentInstancesRequest) (*apiv1alpha1.ListAgentInstancesResponse, error) { result, err := s.service.List(ctx, ListRequest{ Namespace: request.GetNamespace(), MatchLabels: request.GetMatchLabels(), AllCreators: request.GetAllCreators(), + AgentTemplate: request.GetAgentTemplate(), Harness: request.GetHarness(), PageSize: int(request.GetPage().GetLimit()), PageToken: request.GetPage().GetPageToken(), }) if err != nil { @@ -48,6 +49,14 @@ func (s *grpcServer) ListAgentInstances(ctx context.Context, request *apiv1alpha }, nil } +func (s *grpcServer) RenameAgentInstance(ctx context.Context, request *apiv1alpha1.RenameAgentInstanceRequest) (*apiv1alpha1.RenameAgentInstanceResponse, error) { + instance, err := s.service.Rename(ctx, request.GetNamespace(), request.GetAgentInstanceId(), request.GetName()) + if err != nil { + return nil, err + } + return &apiv1alpha1.RenameAgentInstanceResponse{AgentInstance: instance}, nil +} + func (s *grpcServer) SuspendAgentInstance(ctx context.Context, request *apiv1alpha1.SuspendAgentInstanceRequest) (*apiv1alpha1.SuspendAgentInstanceResponse, error) { instance, err := s.service.Suspend(ctx, request.GetNamespace(), request.GetAgentInstanceId()) if err != nil { diff --git a/go/core/v2/agentinstance/service.go b/go/core/v2/agentinstance/service.go index db4a36341..7c43c63c9 100644 --- a/go/core/v2/agentinstance/service.go +++ b/go/core/v2/agentinstance/service.go @@ -8,27 +8,39 @@ import ( "errors" "fmt" "strings" + "unicode" + "unicode/utf8" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/google/uuid" dbpkg "github.com/kagent-dev/kagent/go/api/database" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" "github.com/kagent-dev/kagent/go/core/pkg/auth" utilvalidation "k8s.io/apimachinery/pkg/util/validation" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) const ( defaultPageSize = 50 maxPageSize = 100 + // maxNameLength bounds the conversation's display name. It is counted in + // runes rather than bytes so a non-ASCII title is not cut to a third of the + // length an ASCII one gets, and it is generous enough to hold a title derived + // from a first message while still fitting a list column. + maxNameLength = 200 ) type store interface { CreateAgentInstance(context.Context, *apiv1alpha1.AgentInstance, string) (*apiv1alpha1.AgentInstance, bool, error) GetAgentInstance(context.Context, string, string, string) (*apiv1alpha1.AgentInstance, error) - ListAgentInstances(context.Context, string, string, bool, map[string]string, string, int) ([]*apiv1alpha1.AgentInstance, error) + ListAgentInstances(context.Context, dbpkg.AgentInstanceQuery) ([]*apiv1alpha1.AgentInstance, error) + RenameAgentInstance(context.Context, string, string, string, string) (*apiv1alpha1.AgentInstance, error) CreateAgentInstanceShare(context.Context, dbpkg.AgentInstanceShare) (*dbpkg.AgentInstanceShare, error) ListAgentInstanceShares(context.Context, string, string, string, string, int) ([]dbpkg.AgentInstanceShare, error) DeleteAgentInstanceShare(context.Context, string, string, string) error + GetActiveAgentInstanceTask(context.Context, string) (*a2a.Task, error) + InterruptActiveAgentInstanceTask(context.Context, string, string) (bool, error) } type instanceWorkflow interface { @@ -42,8 +54,12 @@ type ListRequest struct { Namespace string MatchLabels map[string]string AllCreators bool - PageSize int - PageToken string + // AgentTemplate and Harness narrow the page to one agent's conversations. + // Either may be given alone. + AgentTemplate string + Harness string + PageSize int + PageToken string } type ListResult struct { @@ -66,16 +82,22 @@ func NewService(store store, authorizer auth.Authorizer, workflow instanceWorkfl return &Service{store: store, authorizer: authorizer, workflow: workflow} } -func (s *Service) Create(ctx context.Context, namespace, harness, template, requestID string) (*apiv1alpha1.AgentInstance, error) { +// Create reserves and converges a new conversation. name is optional; an empty +// name leaves the conversation identified by its id, which is how every instance +// created before names existed behaves. +func (s *Service) Create(ctx context.Context, namespace, harness, template, requestID, name string) (*apiv1alpha1.AgentInstance, error) { if err := validateCreate(namespace, harness, template, requestID); err != nil { return nil, err } + if err := validateName(name); err != nil { + return nil, err + } creator, err := s.authorize(ctx, auth.VerbCreate, namespace+"/"+template) if err != nil { return nil, err } instance, _, err := s.store.CreateAgentInstance(ctx, &apiv1alpha1.AgentInstance{ - Id: uuid.NewString(), Namespace: namespace, Creator: creator, + Id: uuid.NewString(), Namespace: namespace, Creator: creator, Name: name, Harness: &apiv1alpha1.ResourceReference{Namespace: namespace, Name: harness}, AgentTemplate: &apiv1alpha1.ResourceReference{Namespace: namespace, Name: template}, }, requestID) @@ -113,10 +135,40 @@ func (s *Service) Get(ctx context.Context, namespace, id string) (*apiv1alpha1.A return instance, nil } +// Rename sets the conversation's display name. Unlike every other read on this +// service this is a write, and it authorizes as one: a reader who may list and +// open a conversation must not be able to retitle it. +func (s *Service) Rename(ctx context.Context, namespace, id, name string) (*apiv1alpha1.AgentInstance, error) { + if err := validateIdentity(namespace, id); err != nil { + return nil, err + } + if err := validateName(name); err != nil { + return nil, err + } + creator, err := s.authorize(ctx, auth.VerbUpdate, namespace+"/"+id) + if err != nil { + return nil, err + } + instance, err := s.store.RenameAgentInstance(ctx, namespace, id, creator, name) + if errors.Is(err, dbpkg.ErrNotFound) { + return nil, serviceerrors.NewNotFound("AgentInstance not found", err) + } + if err != nil { + return nil, serviceerrors.NewInternal("Failed to rename AgentInstance", err) + } + return instance, nil +} + func (s *Service) List(ctx context.Context, request ListRequest) (ListResult, error) { if err := validateNamespace(request.Namespace); err != nil { return ListResult{}, err } + if err := validateOptionalName("agent_template", request.AgentTemplate); err != nil { + return ListResult{}, err + } + if err := validateOptionalName("harness", request.Harness); err != nil { + return ListResult{}, err + } userID, err := s.authorize(ctx, auth.VerbGet, request.Namespace) if err != nil { return ListResult{}, err @@ -137,7 +189,12 @@ func (s *Service) List(ctx context.Context, request ListRequest) (ListResult, er if err != nil { return ListResult{}, serviceerrors.NewInvalidArgument("page token is invalid", err) } - instances, err := s.store.ListAgentInstances(ctx, request.Namespace, userID, request.AllCreators, request.MatchLabels, afterID, pageSize+1) + instances, err := s.store.ListAgentInstances(ctx, dbpkg.AgentInstanceQuery{ + Namespace: request.Namespace, UserID: userID, AllUsers: request.AllCreators, + MatchLabels: request.MatchLabels, + AgentTemplate: request.AgentTemplate, Harness: request.Harness, + AfterID: afterID, Limit: pageSize + 1, + }) if err != nil { return ListResult{}, serviceerrors.NewInternal("Failed to list AgentInstances", err) } @@ -196,9 +253,41 @@ func (s *Service) Suspend(ctx context.Context, namespace, id string) (*apiv1alph if err != nil { return nil, serviceerrors.NewUnavailable("Failed to suspend AgentInstance", err) } + s.reapActiveTask(ctx, instance.GetId()) return instance, nil } +// reapActiveTask records that the instance's in-flight turn ended, because +// suspending stops the runtime executing it. Without this the turn stays +// non-terminal and holds the instance's single active-task slot, and the +// instance is left unable to answer until something else notices. +// +// A turn parked awaiting the reader is deliberately left alone. Suspending is a +// pause, not an abandonment: the agent's question is still valid and still +// answerable after a resume, so failing it here would destroy the very thing the +// conversation is waiting for — and would do so invisibly, since a suspend says +// nothing about tasks. +// +// A failure here is logged rather than returned: the suspend itself succeeded, +// and reporting it as failed would invite a retry of an operation that already +// happened. +func (s *Service) reapActiveTask(ctx context.Context, instanceID string) { + active, err := s.store.GetActiveAgentInstanceTask(ctx, instanceID) + if errors.Is(err, dbpkg.ErrNotFound) { + return + } + if err != nil { + ctrllog.FromContext(ctx).Error(err, "failed to read active task while suspending AgentInstance", "instance", instanceID) + return + } + if dbpkg.TaskParkedAwaitingUser(active.Status.State) { + return + } + if _, err := s.store.InterruptActiveAgentInstanceTask(ctx, instanceID, string(active.ID)); err != nil { + ctrllog.FromContext(ctx).Error(err, "failed to interrupt active task while suspending AgentInstance", "instance", instanceID, "task", active.ID) + } +} + func (s *Service) Resume(ctx context.Context, namespace, id string) (*apiv1alpha1.AgentInstance, error) { if err := validateIdentity(namespace, id); err != nil { return nil, err @@ -303,7 +392,33 @@ func (s *Service) RevokeShare(ctx context.Context, namespace, shareID string) er return nil } +/* + * Resolves who an AgentInstance call is made as, honouring a share over that instance. + * + * The same rule the A2A gateway already applies, and it has to be the same: a share + * token is authority over one instance, the visitor stays authenticated as themselves, + * and the record is then read as the share's owner — because an instance is scoped to + * its creator and reading it as the visitor finds nothing at all. + * + * Without this, everything a shared conversation offers beyond reading and sending was + * refused: the visitor could talk to the agent through the gateway, which understands + * shares, and could not suspend or resume it through this service, which did not. The + * shared page ended up offering a live conversation with no way to give its worker + * back — on a pool that is the reason suspending exists. + * + * Read-only shares are not a concern here and deliberately not re-checked: the + * interceptor refuses any non-read RPC for one before this is reached, which is where + * that rule belongs and where it is tested. + */ func (s *Service) authorize(ctx context.Context, verb auth.Verb, name string) (string, error) { + if share, ok := auth.ShareContextFrom(ctx); ok { + if _, id, found := strings.Cut(name, "/"); found && share.IsForAgentInstance(id) { + if _, ok := auth.AuthSessionFrom(ctx); !ok { + return "", serviceerrors.NewUnauthenticated("Failed to get authenticated principal", nil) + } + return share.UserID, nil + } + } return s.authorizeType(ctx, verb, "AgentInstance", name) } @@ -335,6 +450,44 @@ func validateCreate(namespace, harness, template, requestID string) error { return nil } +// validateName bounds a conversation's display name. An empty name is valid and +// means unnamed. Control characters are refused because they render as an +// invisible break in a table cell or silently truncate a header, and surrounding +// whitespace is refused rather than trimmed: quietly rewriting what someone +// typed reads on screen as a rename that did not take. +func validateName(name string) error { + if name == "" { + return nil + } + if strings.TrimSpace(name) != name { + return serviceerrors.NewInvalidArgument("name must not have leading or trailing whitespace", nil) + } + if utf8.RuneCountInString(name) > maxNameLength { + return serviceerrors.NewInvalidArgument(fmt.Sprintf("name must be at most %d characters", maxNameLength), nil) + } + if !utf8.ValidString(name) { + return serviceerrors.NewInvalidArgument("name must be valid UTF-8", nil) + } + for _, character := range name { + if unicode.IsControl(character) { + return serviceerrors.NewInvalidArgument("name must not contain control characters", nil) + } + } + return nil +} + +// validateOptionalName checks a filter that names a Kubernetes object, where +// absent means "do not filter". +func validateOptionalName(field, value string) error { + if value == "" { + return nil + } + if problems := utilvalidation.IsDNS1123Subdomain(value); len(problems) > 0 { + return serviceerrors.NewInvalidArgument(field+" is invalid: "+strings.Join(problems, "; "), nil) + } + return nil +} + func validateIdentity(namespace, id string) error { if err := validateNamespace(namespace); err != nil { return err diff --git a/go/core/v2/agentinstance/service_test.go b/go/core/v2/agentinstance/service_test.go index 4c7045c3b..cfc480a06 100644 --- a/go/core/v2/agentinstance/service_test.go +++ b/go/core/v2/agentinstance/service_test.go @@ -5,8 +5,10 @@ import ( "context" "crypto/sha256" "errors" + "strings" "testing" + a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/google/uuid" dbpkg "github.com/kagent-dev/kagent/go/api/database" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" @@ -31,14 +33,18 @@ type serviceTestStore struct { requestID string createErr error instances []*apiv1alpha1.AgentInstance - listUserID string - listAllUsers bool - listAfterID string - listLimit int + listQuery dbpkg.AgentInstanceQuery share dbpkg.AgentInstanceShare shares []dbpkg.AgentInstanceShare shareAfterID string shareLimit int + renamed *apiv1alpha1.AgentInstance + renameName string + renameUserID string + renameErr error + getCreator string + activeTask *a2a.Task + interrupted string } func (s *serviceTestStore) CreateAgentInstance(_ context.Context, instance *apiv1alpha1.AgentInstance, requestID string) (*apiv1alpha1.AgentInstance, bool, error) { @@ -51,15 +57,37 @@ func (s *serviceTestStore) CreateAgentInstance(_ context.Context, instance *apiv return instance, true, nil } -func (s *serviceTestStore) GetAgentInstance(context.Context, string, string, string) (*apiv1alpha1.AgentInstance, error) { +func (s *serviceTestStore) GetAgentInstance(_ context.Context, _, _, creator string) (*apiv1alpha1.AgentInstance, error) { + s.getCreator = creator return &apiv1alpha1.AgentInstance{State: apiv1alpha1.AgentInstanceState_AGENT_INSTANCE_STATE_READY}, nil } -func (s *serviceTestStore) ListAgentInstances(_ context.Context, _ string, userID string, allUsers bool, _ map[string]string, afterID string, limit int) ([]*apiv1alpha1.AgentInstance, error) { - s.listUserID, s.listAllUsers, s.listAfterID, s.listLimit = userID, allUsers, afterID, limit +func (s *serviceTestStore) ListAgentInstances(_ context.Context, query dbpkg.AgentInstanceQuery) ([]*apiv1alpha1.AgentInstance, error) { + s.listQuery = query return s.instances, nil } +func (s *serviceTestStore) RenameAgentInstance(_ context.Context, _, id, userID, name string) (*apiv1alpha1.AgentInstance, error) { + if s.renameErr != nil { + return nil, s.renameErr + } + s.renameName, s.renameUserID = name, userID + s.renamed = &apiv1alpha1.AgentInstance{Id: id, Name: name} + return s.renamed, nil +} + +func (s *serviceTestStore) GetActiveAgentInstanceTask(context.Context, string) (*a2a.Task, error) { + if s.activeTask == nil { + return nil, dbpkg.ErrNotFound + } + return s.activeTask, nil +} + +func (s *serviceTestStore) InterruptActiveAgentInstanceTask(_ context.Context, _, taskID string) (bool, error) { + s.interrupted = taskID + return true, nil +} + func (s *serviceTestStore) CreateAgentInstanceShare(_ context.Context, share dbpkg.AgentInstanceShare) (*dbpkg.AgentInstanceShare, error) { s.share = share return &s.share, nil @@ -100,7 +128,7 @@ func TestServiceCreateUsesAuthenticatedOwnerAndGeneratedUUID(t *testing.T) { store := &serviceTestStore{} service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{}) - instance, err := service.Create(serviceTestContext("alice"), "team-a", "kagent", "assistant", "request-1") + instance, err := service.Create(serviceTestContext("alice"), "team-a", "kagent", "assistant", "request-1", "") if err != nil { t.Fatal(err) } @@ -124,7 +152,7 @@ func TestServiceCreateMapsStoreErrors(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { service := NewService(&serviceTestStore{createErr: test.err}, serviceTestAuthorizer{}, serviceTestWorkflow{}) - _, err := service.Create(serviceTestContext("alice"), "team-a", "kagent", "assistant", "request-1") + _, err := service.Create(serviceTestContext("alice"), "team-a", "kagent", "assistant", "request-1", "") if !serviceerrors.IsCode(err, test.code) { t.Fatalf("Create() error = %v, want code %s", err, test.code) } @@ -146,7 +174,7 @@ func TestServiceCreateRejectsInvalidOrUnauthorizedRequests(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { service := NewService(&serviceTestStore{}, test.authorizer, serviceTestWorkflow{}) - _, err := service.Create(test.ctx, test.namespace, "kagent", "assistant", "request-1") + _, err := service.Create(test.ctx, test.namespace, "kagent", "assistant", "request-1", "") if !serviceerrors.IsCode(err, test.code) { t.Fatalf("Create() error = %v, want code %s", err, test.code) } @@ -186,8 +214,8 @@ func TestServiceListPaginatesByInstanceID(t *testing.T) { if err != nil { t.Fatal(err) } - if len(result.Instances) != 2 || store.listUserID != "alice" || store.listAllUsers || store.listLimit != 3 { - t.Fatalf("List() = %+v, user ID = %q, all users = %t, limit = %d", result, store.listUserID, store.listAllUsers, store.listLimit) + if len(result.Instances) != 2 || store.listQuery.UserID != "alice" || store.listQuery.AllUsers || store.listQuery.Limit != 3 { + t.Fatalf("List() = %+v, query = %+v", result, store.listQuery) } afterID, err := decodePageToken(result.NextPageToken) if err != nil || afterID != ids[1] { @@ -196,8 +224,8 @@ func TestServiceListPaginatesByInstanceID(t *testing.T) { if _, err := service.List(serviceTestContext("alice"), ListRequest{Namespace: "team-a", AllCreators: true}); err != nil { t.Fatal(err) } - if store.listUserID != "alice" || !store.listAllUsers { - t.Fatalf("operator list user ID = %q, all users = %t", store.listUserID, store.listAllUsers) + if store.listQuery.UserID != "alice" || !store.listQuery.AllUsers { + t.Fatalf("operator list query = %+v", store.listQuery) } } @@ -240,3 +268,312 @@ func TestServiceListSharesPaginatesInStore(t *testing.T) { t.Fatalf("next page token = %q (%v), want %q", afterID, err, ids[2]) } } + +func TestServiceCreateCarriesTheNameAndLeavesAnOmittedOneEmpty(t *testing.T) { + for _, test := range []struct { + name string + given string + want string + }{ + {name: "named", given: "Debugging the ingress", want: "Debugging the ingress"}, + // An omitted name must stay empty rather than being filled in with the id: + // the whole change is additive, and a caller that never mentions a name has + // to behave exactly as it did before the field existed. + {name: "omitted stays empty", given: "", want: ""}, + } { + t.Run(test.name, func(t *testing.T) { + store := &serviceTestStore{} + service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{}) + instance, err := service.Create(serviceTestContext("alice"), "team-a", "kagent", "assistant", "request-1", test.given) + if err != nil { + t.Fatal(err) + } + if store.createInput.GetName() != test.want || instance.GetName() != test.want { + t.Fatalf("stored name = %q, returned name = %q, want %q", store.createInput.GetName(), instance.GetName(), test.want) + } + if instance.GetName() == instance.GetId() && test.want == "" { + t.Fatal("an unnamed instance was given its id as a name") + } + }) + } +} + +func TestServiceRejectsInvalidNames(t *testing.T) { + for _, test := range []struct { + name string + given string + wantErr bool + }{ + {name: "empty is unnamed", given: "", wantErr: false}, + {name: "ordinary title", given: "Why is the pod pending?", wantErr: false}, + {name: "punctuation and emoji", given: "deploy 🚀 v2 — take 3", wantErr: false}, + {name: "at the length limit", given: strings.Repeat("a", maxNameLength), wantErr: false}, + {name: "runes not bytes at the limit", given: strings.Repeat("é", maxNameLength), wantErr: false}, + {name: "over the length limit", given: strings.Repeat("a", maxNameLength+1), wantErr: true}, + {name: "newline", given: "first line\nsecond line", wantErr: true}, + {name: "carriage return", given: "title\r", wantErr: true}, + {name: "tab", given: "a\tb", wantErr: true}, + {name: "leading whitespace", given: " title", wantErr: true}, + {name: "trailing whitespace", given: "title ", wantErr: true}, + } { + t.Run(test.name, func(t *testing.T) { + service := NewService(&serviceTestStore{}, serviceTestAuthorizer{}, serviceTestWorkflow{}) + ctx := serviceTestContext("alice") + createErr := service.createError(ctx, test.given) + renameErr := service.renameError(ctx, test.given) + if (createErr != nil) != test.wantErr || (renameErr != nil) != test.wantErr { + t.Fatalf("create error = %v, rename error = %v, want error %t", createErr, renameErr, test.wantErr) + } + if test.wantErr && !serviceerrors.IsCode(createErr, serviceerrors.CodeInvalidArgument) { + t.Fatalf("create error = %v, want code %s", createErr, serviceerrors.CodeInvalidArgument) + } + }) + } +} + +// createError and renameError keep the validation table above honest: both entry +// points must apply the same rules, or a name refused on create is accepted on +// rename and reaches the database anyway. +func (s *Service) createError(ctx context.Context, name string) error { + _, err := s.Create(ctx, "team-a", "kagent", "assistant", "request-1", name) + return err +} + +func (s *Service) renameError(ctx context.Context, name string) error { + _, err := s.Rename(ctx, "team-a", "11111111-1111-4111-8111-111111111111", name) + return err +} + +func TestServiceRenameRequiresWriteAuthorizationAndScopesToTheOwner(t *testing.T) { + instanceID := "11111111-1111-4111-8111-111111111111" + + t.Run("refused without authorization", func(t *testing.T) { + store := &serviceTestStore{} + service := NewService(store, serviceTestAuthorizer{err: errors.New("denied")}, serviceTestWorkflow{}) + _, err := service.Rename(serviceTestContext("alice"), "team-a", instanceID, "New title") + if !serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied) { + t.Fatalf("Rename() error = %v, want code %s", err, serviceerrors.CodePermissionDenied) + } + if store.renamed != nil { + t.Fatal("Rename() reached the store despite being unauthorized") + } + }) + + t.Run("authorizes as an update, not a read", func(t *testing.T) { + authorizer := &recordingAuthorizer{} + store := &serviceTestStore{} + service := NewService(store, authorizer, serviceTestWorkflow{}) + instance, err := service.Rename(serviceTestContext("alice"), "team-a", instanceID, "New title") + if err != nil { + t.Fatal(err) + } + if authorizer.verb != auth.VerbUpdate { + t.Fatalf("authorized verb = %q, want %q", authorizer.verb, auth.VerbUpdate) + } + if store.renameUserID != "alice" || store.renameName != "New title" || instance.GetName() != "New title" { + t.Fatalf("rename owner = %q, name = %q, returned = %+v", store.renameUserID, store.renameName, instance) + } + }) + + t.Run("clearing the name is allowed", func(t *testing.T) { + store := &serviceTestStore{} + service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{}) + instance, err := service.Rename(serviceTestContext("alice"), "team-a", instanceID, "") + if err != nil || instance.GetName() != "" { + t.Fatalf("Rename(\"\") = %+v, error %v", instance, err) + } + }) + + t.Run("a missing instance is not found", func(t *testing.T) { + store := &serviceTestStore{renameErr: dbpkg.ErrNotFound} + service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{}) + _, err := service.Rename(serviceTestContext("alice"), "team-a", instanceID, "New title") + if !serviceerrors.IsCode(err, serviceerrors.CodeNotFound) { + t.Fatalf("Rename() error = %v, want code %s", err, serviceerrors.CodeNotFound) + } + }) +} + +/* + * A share over an instance is authority to act on it, and the record is read as its owner. + * + * The same rule the A2A gateway applies, which is the point: the visitor could already + * talk to a shared conversation through the gateway, because that understands shares, + * and could not suspend or resume it through this service, because this did not. So a + * shared conversation offered a live agent with no way to give its worker back. + * + * An instance is scoped to its creator, so reading it as the visitor finds nothing — + * which is why this asserts the creator the store is asked for, not merely that the + * call succeeded. A call that authorized correctly and then looked the record up under + * the wrong user would fail as "not found", which is the confusing half of this bug. + * + * Read-only shares are refused before reaching here, by the interceptor, and are tested + * where that rule lives. + */ +func TestServiceSuspendAcceptsAShareOverThatInstance(t *testing.T) { + instanceID := "11111111-1111-4111-8111-111111111111" + shared := auth.ShareContextTo(serviceTestContext("visitor"), &auth.ShareContext{ + AgentInstanceID: instanceID, + UserID: "owner", + }) + + t.Run("acts as the share's owner, not the visitor", func(t *testing.T) { + store := &serviceTestStore{} + // The authorizer refuses everything: a share that still needed its approval + // would pass this test for the wrong reason. + service := NewService(store, serviceTestAuthorizer{err: errors.New("denied")}, serviceTestWorkflow{}) + if _, err := service.Suspend(shared, "team-a", instanceID); err != nil { + t.Fatalf("Suspend() with a share over this instance = %v, want it accepted", err) + } + if store.getCreator != "owner" { + t.Fatalf("record read as %q, want the share's owner", store.getCreator) + } + }) + + t.Run("a share over a different instance is no authority here", func(t *testing.T) { + elsewhere := auth.ShareContextTo(serviceTestContext("visitor"), &auth.ShareContext{ + AgentInstanceID: "22222222-2222-4222-8222-222222222222", + UserID: "owner", + }) + store := &serviceTestStore{} + service := NewService(store, serviceTestAuthorizer{err: errors.New("denied")}, serviceTestWorkflow{}) + if _, err := service.Suspend(elsewhere, "team-a", instanceID); !serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied) { + t.Fatalf("Suspend() with a share over another instance = %v, want it refused", err) + } + }) + + t.Run("a session share is not an instance share", func(t *testing.T) { + // Two different kinds of share over two different resources. Treating one as + // the other is exactly what `IsForAgentInstance` exists to prevent. + session := auth.ShareContextTo(serviceTestContext("visitor"), &auth.ShareContext{ + SessionID: instanceID, + UserID: "owner", + }) + store := &serviceTestStore{} + service := NewService(store, serviceTestAuthorizer{err: errors.New("denied")}, serviceTestWorkflow{}) + if _, err := service.Suspend(session, "team-a", instanceID); !serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied) { + t.Fatalf("Suspend() with a session share = %v, want it refused", err) + } + }) +} + +type recordingAuthorizer struct { + verb auth.Verb + resource auth.Resource +} + +func (a *recordingAuthorizer) Check(_ context.Context, _ auth.Principal, verb auth.Verb, resource auth.Resource) error { + a.verb, a.resource = verb, resource + return nil +} + +func TestServiceListPassesTheAgentPairThroughToTheStore(t *testing.T) { + for _, test := range []struct { + name string + request ListRequest + wantErr bool + wantPair [2]string + }{ + { + name: "both halves of the pair", + request: ListRequest{Namespace: "team-a", AgentTemplate: "assistant", Harness: "kagent"}, + wantPair: [2]string{"assistant", "kagent"}, + }, + { + name: "template alone", + request: ListRequest{Namespace: "team-a", AgentTemplate: "assistant"}, + wantPair: [2]string{"assistant", ""}, + }, + { + name: "neither, which lists everything", + request: ListRequest{Namespace: "team-a"}, + wantPair: [2]string{"", ""}, + }, + { + name: "an invalid template name is refused rather than matching nothing", + request: ListRequest{Namespace: "team-a", AgentTemplate: "NOT A NAME"}, + wantErr: true, + }, + { + name: "an invalid harness name is refused", + request: ListRequest{Namespace: "team-a", Harness: "NOT A NAME"}, + wantErr: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + store := &serviceTestStore{} + service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{}) + _, err := service.List(serviceTestContext("alice"), test.request) + if test.wantErr { + if !serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument) { + t.Fatalf("List() error = %v, want code %s", err, serviceerrors.CodeInvalidArgument) + } + return + } + if err != nil { + t.Fatal(err) + } + got := [2]string{store.listQuery.AgentTemplate, store.listQuery.Harness} + if got != test.wantPair { + t.Fatalf("store query pair = %v, want %v", got, test.wantPair) + } + }) + } +} + +// TestServiceSuspendReapsTheActiveTurn pins the half of the stranded-task fix that +// stops the strand forming. Suspending stops the runtime, so an in-flight turn is +// over; leaving it non-terminal holds the instance's one active-task slot and +// every later send is refused with "AgentInstance already has an active task". +func TestServiceSuspendReapsTheActiveTurn(t *testing.T) { + for _, test := range []struct { + name string + active *a2a.Task + workflowErr error + wantInterrupted string + }{ + { + name: "an in-flight turn is interrupted", + active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateWorking}}, + wantInterrupted: "task-1", + }, + { + name: "no active turn is left alone", + active: nil, + wantInterrupted: "", + }, + { + // A suspend that did not happen must not close a turn that is still running. + name: "a failed suspend interrupts nothing", + active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateWorking}}, + workflowErr: errors.New("substrate unavailable"), + wantInterrupted: "", + }, + { + // Suspending is a pause, not an abandonment. A question the agent asked is + // still valid and still answerable after a resume, so failing it here would + // destroy the thing the conversation is waiting for — invisibly, since a + // suspend says nothing about tasks. + name: "a turn waiting on the reader survives a suspend", + active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateInputRequired}}, + wantInterrupted: "", + }, + { + name: "a turn waiting on authorization survives a suspend", + active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateAuthRequired}}, + wantInterrupted: "", + }, + } { + t.Run(test.name, func(t *testing.T) { + store := &serviceTestStore{activeTask: test.active} + service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{err: test.workflowErr}) + _, err := service.Suspend(serviceTestContext("alice"), "team-a", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab") + if (err != nil) != (test.workflowErr != nil) { + t.Fatalf("Suspend() error = %v", err) + } + if store.interrupted != test.wantInterrupted { + t.Fatalf("interrupted task = %q, want %q", store.interrupted, test.wantInterrupted) + } + }) + } +} diff --git a/go/core/v2/translator/compiler_test.go b/go/core/v2/translator/compiler_test.go index 6c9faa34a..771bff0ca 100644 --- a/go/core/v2/translator/compiler_test.go +++ b/go/core/v2/translator/compiler_test.go @@ -73,7 +73,11 @@ func TestCompileAgentTemplatePinsAgentPluginSources(t *testing.T) { if err := json.Unmarshal(spec.ConfigJSON, &config); err != nil { t.Fatal(err) } - if config.SessionDBURL != "sqlite:////data/sessions.db" { + // The driver is part of the assertion, not incidental. The Python runtime opens + // this URL with an asyncio engine and refuses a bare `sqlite:` one, so dropping + // the driver leaves an actor that never serves /readyz — which surfaces as a + // harness stuck in ResumeGoldenActor rather than as anything naming this line. + if config.SessionDBURL != "sqlite+aiosqlite:////data/sessions.db" { t.Fatalf("session DB URL = %q", config.SessionDBURL) } plugins := config.AgentPlugins diff --git a/go/core/v2/translator/kagent/agentcard_test.go b/go/core/v2/translator/kagent/agentcard_test.go new file mode 100644 index 000000000..69162a24b --- /dev/null +++ b/go/core/v2/translator/kagent/agentcard_test.go @@ -0,0 +1,45 @@ +package kagent + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestAgentTemplateCardDeclaresHumanInTheLoop pins discoverability. The compiled +// card is a snapshot stored with the revision, not the runtime's live card, so a +// capability the runtime has is invisible unless this states it. Dropping it +// breaks nothing observable at the API — a reply still works for a client that +// knows to ask — which is exactly why it needs a test: the failure is a client +// that cannot tell an answerable question from an unanswerable one. +func TestAgentTemplateCardDeclaresHumanInTheLoop(t *testing.T) { + card := agentTemplateCard(&v1alpha3.AgentTemplate{ + ObjectMeta: metav1.ObjectMeta{Name: "pizza-agent", Namespace: "team-a"}, + }) + + if !card.Capabilities.Streaming { + t.Fatalf("capabilities = %#v, want streaming", card.Capabilities) + } + var found bool + for _, extension := range card.Capabilities.Extensions { + if extension.URI == hitlExtensionURI { + found = true + if extension.Required { + t.Fatal("the HITL extension must be optional; requiring it would refuse clients that cannot answer questions") + } + } + } + if !found { + t.Fatalf("extensions = %#v, want %s declared", card.Capabilities.Extensions, hitlExtensionURI) + } + // The card must stay free of cluster-specific addresses; the gateway supplies + // the public interface. + if len(card.SupportedInterfaces) != 1 || card.SupportedInterfaces[0].URL != "http://127.0.0.1:80" { + t.Fatalf("supported interfaces = %#v", card.SupportedInterfaces) + } + // The name is normalised for ADK, which rejects hyphens. + if card.Name != "pizza_agent" { + t.Fatalf("card name = %q, want the ADK-safe form", card.Name) + } +} diff --git a/go/core/v2/translator/kagent/compiler.go b/go/core/v2/translator/kagent/compiler.go index 122eb6633..5be6d5890 100644 --- a/go/core/v2/translator/kagent/compiler.go +++ b/go/core/v2/translator/kagent/compiler.go @@ -52,7 +52,14 @@ func (c *Compiler) Compile(ctx context.Context, input *v2translator.HarnessInput } template, harness := input.Root.Template, input.Harness cfg := compiled.config - cfg.SessionDBURL = "sqlite:////data/sessions.db" + // The async driver is named, because the Python runtime cannot infer it and the Go + // one does not need it. `DatabaseSessionService` builds an asyncio engine and + // refuses a bare `sqlite:` URL with "the asyncio extension requires an async + // driver" — so a kagent-adk actor never opened its readiness port, and the harness + // sat in ResumeGoldenActor until the golden actor timed out. The Go ADK accepts + // `sqlite+` and strips the driver (see adk/pkg/session.sqlitePathFromURL), + // so one URL serves both. + cfg.SessionDBURL = "sqlite+aiosqlite:////data/sessions.db" configJSON, err := json.Marshal(cfg) if err != nil { @@ -386,6 +393,12 @@ func (c *Compiler) resolveValueRef(ctx context.Context, namespace string, ref v1 } // agentTemplateCard describes the runtime-local A2A server. Substrate routes +// hitlExtensionURI is the human-in-the-loop A2A extension the kagent runtime +// negotiates. Spelled here rather than imported from the ADK package so the +// controller does not depend on the runtime's module for one constant; the two +// must agree, and `agentcard.go` is the definition. +const hitlExtensionURI = "https://kagent.dev/extensions/hitl/v1" + // public traffic to this loopback interface; the card must not advertise a // cluster-specific external address. func agentTemplateCard(template *v1alpha3.AgentTemplate) *a2atype.AgentCard { @@ -398,7 +411,19 @@ func agentTemplateCard(template *v1alpha3.AgentTemplate) *a2atype.AgentCard { ProtocolBinding: a2atype.TransportProtocolGRPC, ProtocolVersion: a2atype.Version, }}, - Capabilities: a2atype.AgentCapabilities{Streaming: true}, + // This compiler builds cards for the kagent runtime specifically, whose A2A + // layer always negotiates human-in-the-loop (see adk/pkg/a2a/agentcard.go, + // which appends this extension unconditionally). Declaring it here is what + // makes an agent's question discoverably answerable: a client reads the card + // to learn it may request the extension and render the choices. Other + // harnesses compile their own cards and make no such claim. + Capabilities: a2atype.AgentCapabilities{ + Streaming: true, + Extensions: []a2atype.AgentExtension{{ + URI: hitlExtensionURI, + Description: "Human in the loop for tool approval, ask user, and nested subagents", + }}, + }, Skills: []a2atype.AgentSkill{}, DefaultInputModes: []string{"text"}, DefaultOutputModes: []string{"text"}, diff --git a/go/go.mod b/go/go.mod index 140ff406d..a7561a1da 100644 --- a/go/go.mod +++ b/go/go.mod @@ -5,8 +5,6 @@ go 1.27.0 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 buf.build/go/protovalidate v1.3.0 - // core dependencies - dario.cat/mergo v1.0.2 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 github.com/a2aproject/a2a-go/v2 v2.5.0 @@ -32,9 +30,9 @@ require ( // api dependencies github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/hashicorp/go-multierror v1.1.1 + github.com/improbable-eng/grpc-web v0.15.0 github.com/jackc/pgx/v5 v5.10.0 github.com/jedib0t/go-pretty/v6 v6.8.3 github.com/kagent-dev/kmcp v0.3.0 @@ -66,7 +64,6 @@ require ( go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/log v0.20.0 go.opentelemetry.io/otel/trace v1.44.0 - go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.0 golang.org/x/sync v0.22.0 golang.org/x/text v0.41.0 @@ -95,6 +92,8 @@ require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect + // core dependencies + dario.cat/mergo v1.0.2 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect dev.gaijin.team/go/golib v0.6.0 // indirect github.com/4meepo/tagalign v1.4.3 // indirect @@ -186,6 +185,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/docker/cli v29.7.2+incompatible // indirect @@ -262,6 +262,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect github.com/googleapis/gax-go/v2 v2.23.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect @@ -369,6 +370,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/cors v1.7.0 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect @@ -475,6 +477,7 @@ require ( modernc.org/sqlite v1.57.0 // indirect mvdan.cc/gofumpt v0.9.2 // indirect mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect + nhooyr.io/websocket v1.8.6 // indirect rsc.io/omap v1.2.0 // indirect rsc.io/ordered v1.1.1 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 // indirect diff --git a/go/go.sum b/go/go.sum index a3aca428b..12f7e73e7 100644 --- a/go/go.sum +++ b/go/go.sum @@ -10,6 +10,8 @@ cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/auth v0.23.1 h1:1tPpBPG02lQHmoiAvs9egyCASqXP0xgobptjZzov/Jg= @@ -28,6 +30,7 @@ dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKf dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= @@ -58,14 +61,17 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/5vCrMONS+g4u4LRHNgOXVSh3O43J2CnI= github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go-linter v1.2.0 h1:zbm174up3hTKjp0wKZVnTzRiG7tSF5XZF0FJG/MuCBI= github.com/ClickHouse/clickhouse-go-linter v1.2.0/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 h1:bN1gA3of5bXtbnLsRPrwfmbbe7A5UWFlcTHseujLnpc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0/go.mod h1:Yj5vHEz/aAepZGliRJsA6uvHAVAQyEwajq9ORCHPxzM= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -80,6 +86,9 @@ github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgy github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/a2aproject/a2a-go/v2 v2.5.0 h1:ZdcFoxv+nZTUV0i2ue5hES76YCANFPG9vjqd7vK8yWM= github.com/a2aproject/a2a-go/v2 v2.5.0/go.mod h1:NcRp/ZHxgMzDj12/BteIC2gOjljuEBKaGRfEdJ2lNSI= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= @@ -88,6 +97,7 @@ github.com/abiosoft/ishell/v2 v2.0.2 h1:5qVfGiQISaYM8TkbBl7RFO6MddABoXpATrsFbVI+ github.com/abiosoft/ishell/v2 v2.0.2/go.mod h1:E4oTCXfo6QjoCart0QYa5m9w4S+deXs/P/9jA77A9Bs= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= @@ -96,6 +106,11 @@ github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsr github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= @@ -110,14 +125,23 @@ github.com/anthropics/anthropic-sdk-go v1.66.0 h1:/CKwgscn0Pe1q4U8aFInSOt/v06JeM github.com/anthropics/anthropic-sdk-go v1.66.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI= github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM= github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM= github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v1.43.6 h1:RrmFcqCBxkJuf7g1axVo5krB4jM/AO8r5e5oujrgdoQ= github.com/aws/aws-sdk-go-v2 v1.43.6/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 h1:LAfOuhAH331fmOjTQpAaOlH+Ftn7RzSDJ2VFwjdMMy4= @@ -162,8 +186,11 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -188,14 +215,23 @@ github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= @@ -228,12 +264,21 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -242,9 +287,14 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= @@ -263,6 +313,9 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -281,16 +334,26 @@ github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUE github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260731231718-6c0b035a1609 h1:tDW58+K0hyud0xABAhuF1buayeMdzDln0MIp/slckuk= github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260731231718-6c0b035a1609/go.mod h1:pdjA+146jsWRsJ0M1jgfRrvJq1HPWeyvaT/Va+nV2OY= github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260812071801-353463cc7248 h1:+HVeI0tXxznym8nR2TFJ1S0rxbzER4aevVcMA/Bl/i0= github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260812071801-353463cc7248/go.mod h1:tjY7cZvIhlQAKRpob4GSOkZYAqtoTPtgWZXGhOTWbhQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -301,6 +364,7 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= @@ -312,22 +376,40 @@ github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47A github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q= github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/glebarez/go-sqlite v1.23.0 h1:FyhIq4jqmgphQAUlY79zPldYGwISEZikaDfhiGWkkaI= github.com/glebarez/go-sqlite v1.23.0/go.mod h1:IIYrOH3L0rHY3jb4IXOHoWdklNajSGUN2eJcvK8WrnI= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -372,8 +454,20 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGL github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4= github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q= github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= @@ -401,20 +495,48 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A= @@ -439,12 +561,19 @@ github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2b github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.31.0 h1:H0bhpFTqOvmHrBGrWKp7ZlhBm5Hh8PYUEXnwxT1LL7A= github.com/google/cel-go v0.31.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -459,20 +588,28 @@ github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+ github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU= github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= @@ -487,30 +624,59 @@ github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduw github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= @@ -533,8 +699,19 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/kagent-dev/kmcp v0.3.0 h1:CuF8LN6JbPoy75saRzwI5OIcSVZUgiwB0BeWtq+rAO4= @@ -549,14 +726,26 @@ github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhE github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= @@ -579,6 +768,9 @@ github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYU github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= @@ -596,10 +788,13 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0= github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0= @@ -618,27 +813,40 @@ github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -670,6 +878,8 @@ github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -687,8 +897,19 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= @@ -697,19 +918,34 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/ollama/ollama v0.32.15 h1:lnCycypBjS9SoMNeM6FivlYeDRn7mP/zfLG0uJXwmZ4= github.com/ollama/ollama v0.32.15/go.mod h1:Kekx/+OtFZHmqbkVH/QUUDVcMQS+1pg1dcz4Qy7TGn4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo/v2 v2.31.0 h1:GtuJos5DFUV9EerYJo8RhYxosYNGvOdDE5haKq6Grfs= github.com/onsi/ginkgo/v2 v2.31.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.42.0 h1:CJby8u36xb7v34W78F8WKvqTQP7PCMIPB78IVDB73l4= github.com/onsi/gomega v1.42.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/openai/openai-go/v3 v3.52.0 h1:VDSjIvI5Sr2/AzGJI6219sM2Il+zBWuopvluMy6KdjE= github.com/openai/openai-go/v3 v3.52.0/go.mod h1:Vy3y2/I2H/MbqvJGXEK8VbN5+avZV6zxux4I3eBdvaA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -717,35 +953,67 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pgvector/pgvector-go v0.4.1 h1:Oaj0mC0Ky8KaTweNHHpLwyFlN6a0nUFoo1vgSFTEhPI= github.com/pgvector/pgvector-go v0.4.1/go.mod h1:4fSXyjl1TYAIdByAql6JazKWRr2s7J0g4hcRY5cBFCk= github.com/pgvector/pgvector-go/pgx v0.4.1 h1:4ASHKHkHKon+x3TlKCVoH0znhFWTWRvAF4z3vnY6bKc= github.com/pgvector/pgvector-go/pgx v0.4.1/go.mod h1:uGpIdPvyX/FxvOljT15nW/NY166HSDCC05JIcJBAzK0= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca h1:ujRGEVWJEoaxQ+8+HMl8YEpGaDAgohgZxJ5S+d2TTFQ= github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU= github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= @@ -760,6 +1028,7 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4l github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -768,8 +1037,13 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= @@ -777,10 +1051,12 @@ github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/L github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU= github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU= github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -789,6 +1065,7 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/securego/gosec/v2 v2.26.1 h1:gdkttGhQFVehqRJ8grKH4DrpqM/QlPKNHBnl8QgcEC4= github.com/securego/gosec/v2 v2.26.1/go.mod h1:57UW4p0uoP3kxoTkhoo3axLdVAi+OWrLg/Ax/kdqtPE= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= @@ -801,20 +1078,31 @@ github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3 github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs= github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY= github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= @@ -829,11 +1117,16 @@ github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -874,14 +1167,24 @@ github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRU github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= github.com/uudashr/iface v1.4.2 h1:06Vq5RKVYThBsj0Bnw4oasMjD1r+7CE/bcKOA8dVSvg= @@ -892,6 +1195,7 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -921,6 +1225,11 @@ go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR5 go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.69.0 h1:saQoWg5845Q8TojpqeVStS7zGwVZ6bc5W2PJavTPiBM= @@ -975,12 +1284,20 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= @@ -990,18 +1307,38 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1010,19 +1347,41 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1032,12 +1391,32 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1067,10 +1446,25 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= @@ -1094,33 +1488,85 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/adk/v2 v2.2.0 h1:QFgzAoH3iWrumg5YEam1buCIt5R1yRV+L7NqVB1Cm9o= google.golang.org/adk/v2 v2.2.0/go.mod h1:7omVW7/SXduhAFjHzgpDgn2qIXvS5JPoyPnLIu5XE8M= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.293.0 h1:p9XIWOf63U4OgYx120ZwVU8+vl4XTPmWfgVPnmOAS9w= google.golang.org/api v0.293.0/go.mod h1:6n5tjEB1gzwniZTepZ0g5u+wM7Bof5GeULCx/zh8ZE0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genai v1.69.0 h1:quP3Rbiz0Mn+zPfXsWHQQwOx8IfO2MnQehZUbrJ/jPo= google.golang.org/genai v1.69.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 h1:YJjbgu+dkp5kUJLfpMyCLfBIWZb/FcJyuLeo1gVBOuo= google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:RRHjglSYABVCWpQ7USCpdfhcd9t4PkajvVwyynZizTc= google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= @@ -1129,6 +1575,10 @@ gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= istio.io/api v1.31.0-alpha.1.0.20260819121012-5803fb6accf7 h1:OWLDeAAvyCOlfY/OxvsPgnaGfWAKmmQ9oBNdDdAIIwQ= @@ -1191,6 +1641,8 @@ mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= +nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= @@ -1215,5 +1667,7 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q= sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/helm/kagent/files/nginx.conf b/helm/kagent/files/nginx.conf index 1af906b3e..c92d8d50b 100644 --- a/helm/kagent/files/nginx.conf +++ b/helm/kagent/files/nginx.conf @@ -6,6 +6,31 @@ events { worker_connections 1024; } +{{- /* + Identity headers an upstream auth proxy (oauth2-proxy and friends) sets and a + backend may trust. The UI used to be a Next server that forwarded a strict + allowlist — Authorization plus ui.additionalForwardedHeaders — and dropped + everything else, so a client-supplied identity header could never reach the + controller. nginx forwards every request header by default, so these are + cleared explicitly unless the operator opted them in, keeping that guarantee + for the headers where spoofing actually matters. +*/}} +{{- $identityHeaders := list + "x-auth-request-user" + "x-auth-request-email" + "x-auth-request-groups" + "x-auth-request-preferred-username" + "x-auth-request-access-token" + "x-auth-request-redirect" + "x-forwarded-user" + "x-forwarded-email" + "x-forwarded-groups" + "x-forwarded-preferred-username" }} +{{- $forwarded := dict }} +{{- range .Values.ui.additionalForwardedHeaders }} + {{- $_ := set $forwarded (lower (trim .)) true }} +{{- end }} + http { client_body_temp_path /tmp/nginx/client_temp; proxy_temp_path /tmp/nginx/proxy_temp; @@ -13,15 +38,14 @@ http { uwsgi_temp_path /tmp/nginx/uwsgi_temp; scgi_temp_path /tmp/nginx/scgi_temp; + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /dev/stdout; log_format main '[$time_local] $remote_addr - $remote_user - $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for'; log_format upstreamlog '[$time_local] $remote_addr - $remote_user - $server_name $host to: $upstream_addr: $request $status upstream_response_time $upstream_response_time msec $msec request_time $request_time'; - upstream kagent_ui { - server 127.0.0.1:8001; - } - upstream kagent_backend { server {{ include "kagent.controllerServiceAuthority" . }}; } @@ -38,41 +62,45 @@ http { {{- end }} server_name localhost; - location /a2a/ { - proxy_pass http://kagent_ui/a2a/; - proxy_http_version 1.1; - proxy_set_header Connection ''; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Origin $scheme://$host; - proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }}; - proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }}; - proxy_buffering off; + root /usr/share/nginx/html; + index index.html; + + location /health { + return 200 'OK'; } - location / { - proxy_pass http://kagent_ui; + {{- /* + Rendered by init.sh from the pod's env on every start, so Helm values + reach the browser instead of being frozen into the bundle at image + build time. Never cached: a stale copy would pin the client to the + previous release's configuration. + */}} + location = /env-config.js { + alias /tmp/kagent/env-config.js; + default_type application/javascript; + add_header Cache-Control "no-store" always; + } + + location /a2a/ { + proxy_pass http://kagent_backend/a2a/; proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; + proxy_set_header Connection ''; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Origin $scheme://$host; - proxy_cache_bypass $http_upgrade; - # Increased timeouts for streaming endpoints + {{- range $header := $identityHeaders }} + {{- if not (hasKey $forwarded $header) }} + proxy_set_header {{ $header }} ""; + {{- end }} + {{- end }} proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }}; proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }}; + # Chat is server-sent events; buffering would hold tokens back. proxy_buffering off; } - location /health { - return 200 'OK'; - } - location /api/ { proxy_pass http://kagent_backend/api/; proxy_http_version 1.1; @@ -83,10 +111,45 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $server_name; + {{- range $header := $identityHeaders }} + {{- if not (hasKey $forwarded $header) }} + proxy_set_header {{ $header }} ""; + {{- end }} + {{- end }} proxy_cache_bypass $http_upgrade; proxy_read_timeout {{ .Values.ui.nginx.proxyReadTimeout }}; proxy_send_timeout {{ .Values.ui.nginx.proxySendTimeout }}; proxy_buffering off; } + + {{- /* + The mock service worker is deleted from the bundle at image build, so + this path has no file behind it — but the SPA fallback below would + answer it with index.html and a 200, which is a misleading thing to + hand a registration attempt. Refuse it outright. As an exact-match + location this also wins over the fallback if a future build ever ships + the worker again, so the mock backend cannot be reached in production + even then. + */}} + location = /mockServiceWorker.js { + return 404; + } + + # Vite fingerprints these filenames, so a given URL's bytes never change. + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable" always; + } + + # The bundle entrypoint must never be cached, or a client keeps loading + # the previous release's asset URLs after an upgrade. + location = /index.html { + add_header Cache-Control "no-store" always; + } + + # SPA fallback: routing is client-side, so a deep link such as /agents + # is not a file on disk and must still return the app shell. + location / { + try_files $uri $uri/ /index.html; + } } } diff --git a/helm/kagent/files/supervisord.conf b/helm/kagent/files/supervisord.conf deleted file mode 100644 index f3720acfa..000000000 --- a/helm/kagent/files/supervisord.conf +++ /dev/null @@ -1,41 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/dev/stdout -logfile_maxbytes=0 -loglevel=debug - -[program:nginx] -command=/usr/sbin/nginx -g "daemon off;" -autostart=true -autorestart=true -startretries=5 -numprocs=1 -startsecs=0 -priority=20 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -stdout_events_enabled=true -stderr_events_enabled=true - -[program:nextjs] -command=node /app/ui/server.js -directory=/app/ui -{{- if .Values.ipv6.enabled }} -environment=PORT=8001,HOSTNAME="::",NODE_ENV=production,PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -{{- else }} -environment=PORT=8001,HOSTNAME="0.0.0.0",NODE_ENV=production,PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -{{- end }} -autostart=true -autorestart=true -startretries=5 -numprocs=1 -startsecs=0 -priority=10 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -stdout_events_enabled=true -stderr_events_enabled=true diff --git a/helm/kagent/templates/_helpers.tpl b/helm/kagent/templates/_helpers.tpl index f6a6d9f73..ce184a2f8 100644 --- a/helm/kagent/templates/_helpers.tpl +++ b/helm/kagent/templates/_helpers.tpl @@ -273,21 +273,6 @@ Controller Service host:port for nginx upstream (no scheme). {{- printf "%s-controller.%s.svc:%d" (include "kagent.fullname" .) (include "kagent.namespace" .) (.Values.controller.service.ports.port | int) -}} {{- end -}} -{{/* -In-cluster HTTP base for the Next.js A2A and other protocol-native routes (includes /api). -The kagent application API uses kagent.controllerInternalGrpcBase instead. -*/}} -{{- define "kagent.controllerInternalHttpApiBase" -}} -{{- printf "http://%s/api" (include "kagent.controllerServiceAuthority" .) -}} -{{- end -}} - -{{/* -In-cluster native gRPC base URL for Next.js server-side calls. -*/}} -{{- define "kagent.controllerInternalGrpcBase" -}} -{{- printf "http://%s-controller.%s.svc:%d" (include "kagent.fullname" .) (include "kagent.namespace" .) (.Values.controller.service.ports.grpc | int) -}} -{{- end -}} - {{/* imagePullSecrets from global values (for subchart usage). Reads .Values.global.imagePullSecrets set by the parent chart. diff --git a/helm/kagent/templates/ui-deployment.yaml b/helm/kagent/templates/ui-deployment.yaml index ab973e0db..ef04ecd34 100644 --- a/helm/kagent/templates/ui-deployment.yaml +++ b/helm/kagent/templates/ui-deployment.yaml @@ -38,9 +38,6 @@ spec: {{- end }} serviceAccountName: {{ include "kagent.fullname" . }}-ui volumes: - - name: nextjs-cache - emptyDir: - sizeLimit: {{ .Values.ui.volumes.nextjsCache }} - name: tmp emptyDir: sizeLimit: {{ .Values.ui.volumes.tmp }} @@ -71,28 +68,20 @@ spec: {{- end }} image: "{{ .Values.ui.image.registry | default .Values.registry }}/{{ .Values.ui.image.repository }}:{{ coalesce .Values.tag .Values.ui.image.tag .Chart.Version }}" imagePullPolicy: {{ .Values.ui.image.pullPolicy | default .Values.imagePullPolicy }} + {{- /* The UI is a static bundle; these are read by init.sh, which + renders them into the config.json the browser fetches at + startup. Nothing here is consumed by a server process. */}} env: - - name: NEXT_PUBLIC_BACKEND_URL - value: {{ .Values.ui.publicBackendUrl | quote }} - - name: BACKEND_INTERNAL_URL - value: {{ .Values.ui.backendInternalUrl | default (include "kagent.controllerInternalHttpApiBase" .) | quote }} - - name: BACKEND_GRPC_URL - value: {{ .Values.ui.backendGrpcUrl | default (include "kagent.controllerInternalGrpcBase" .) | quote }} + - name: KAGENT_API_BASE_URL + value: {{ .Values.ui.publicBackendUrl | default "/api" | quote }} {{- if .Values.ui.auth }} - name: SSO_REDIRECT_PATH value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} - # Client components (e.g. AuthContext) can only read NEXT_PUBLIC_* vars at runtime. - - name: NEXT_PUBLIC_SSO_REDIRECT_PATH - value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }} {{- end }} {{- if .Values.ui.streamTimeoutSeconds }} - name: KAGENT_STREAM_TIMEOUT_MS value: {{ mul (int .Values.ui.streamTimeoutSeconds) 1000 | quote }} {{- end }} - {{- with .Values.ui.additionalForwardedHeaders }} - - name: KAGENT_ADDITIONAL_FORWARDED_HEADERS - value: {{ join "," . | quote }} - {{- end }} {{- with .Values.ui.env }} {{- toYaml . | nindent 12 }} {{- end }} @@ -101,18 +90,12 @@ spec: containerPort: {{ .Values.ui.service.ports.targetPort }} protocol: TCP volumeMounts: - - name: nextjs-cache - mountPath: /app/ui/.next/cache - name: tmp mountPath: /tmp - name: ui-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf readOnly: true - - name: ui-config - mountPath: /etc/supervisor/conf.d/supervisord.conf - subPath: supervisord.conf - readOnly: true resources: {{- toYaml .Values.ui.resources | nindent 12 }} {{- if .Values.ui.startupProbe }} diff --git a/helm/kagent/templates/ui-nginx-configmap.yaml b/helm/kagent/templates/ui-nginx-configmap.yaml index 0c41d04c2..ebe06071c 100644 --- a/helm/kagent/templates/ui-nginx-configmap.yaml +++ b/helm/kagent/templates/ui-nginx-configmap.yaml @@ -8,5 +8,3 @@ metadata: data: nginx.conf: | {{- tpl (.Files.Get "files/nginx.conf") . | nindent 4 }} - supervisord.conf: | - {{- tpl (.Files.Get "files/supervisord.conf") . | nindent 4 }} diff --git a/helm/kagent/tests/security-context_test.yaml b/helm/kagent/tests/security-context_test.yaml index 05b5238dd..fc654afdf 100644 --- a/helm/kagent/tests/security-context_test.yaml +++ b/helm/kagent/tests/security-context_test.yaml @@ -123,16 +123,6 @@ tests: - isNull: path: spec.template.spec.containers[0].securityContext.seccompProfile - - it: should have nextjs-cache volume for UI - template: ui-deployment.yaml - asserts: - - contains: - path: spec.template.spec.volumes - content: - name: nextjs-cache - emptyDir: - sizeLimit: 100Mi - - it: should have tmp volume for UI template: ui-deployment.yaml asserts: @@ -143,15 +133,6 @@ tests: emptyDir: sizeLimit: 50Mi - - it: should have nextjs-cache volume mount for UI - template: ui-deployment.yaml - asserts: - - contains: - path: spec.template.spec.containers[0].volumeMounts - content: - name: nextjs-cache - mountPath: /app/ui/.next/cache - - it: should have tmp volume mount for UI template: ui-deployment.yaml asserts: diff --git a/helm/kagent/tests/ui-deployment_test.yaml b/helm/kagent/tests/ui-deployment_test.yaml index 6ea2e9939..c129bfbb1 100644 --- a/helm/kagent/tests/ui-deployment_test.yaml +++ b/helm/kagent/tests/ui-deployment_test.yaml @@ -76,15 +76,6 @@ tests: path: spec.template.spec.containers[0].ports[0].containerPort value: 8080 - - it: should set the internal gRPC backend URL - template: ui-deployment.yaml - asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: BACKEND_GRPC_URL - value: "http://RELEASE-NAME-controller.NAMESPACE.svc:8084" - - it: should set global annotations on deployment template: ui-deployment.yaml set: @@ -207,7 +198,7 @@ tests: - notExists: path: spec.template.spec.containers[0].readinessProbe.httpGet - - it: should mount unified ui-config ConfigMap for nginx and supervisord + - it: should mount the ui-config ConfigMap for nginx template: ui-deployment.yaml asserts: - contains: @@ -223,7 +214,11 @@ tests: mountPath: /etc/nginx/nginx.conf subPath: nginx.conf readOnly: true - - contains: + + - it: should not mount a supervisord config now that nginx runs as PID 1 + template: ui-deployment.yaml + asserts: + - notContains: path: spec.template.spec.containers[0].volumeMounts content: name: ui-config @@ -307,40 +302,99 @@ tests: - notExists: path: spec.template.spec.topologySpreadConstraints - - it: should use same-origin public API path and internal controller URL for nginx proxying + - it: should pass runtime browser config to init.sh as env template: ui-deployment.yaml asserts: - contains: path: spec.template.spec.containers[0].env content: - name: NEXT_PUBLIC_BACKEND_URL + name: KAGENT_API_BASE_URL value: "/api" - contains: path: spec.template.spec.containers[0].env content: - name: BACKEND_INTERNAL_URL - value: "http://RELEASE-NAME-controller.NAMESPACE.svc:8083/api" + name: SSO_REDIRECT_PATH + value: "/oauth2/start" + - contains: + path: spec.template.spec.containers[0].env + content: + name: KAGENT_STREAM_TIMEOUT_MS + value: "1800000" - - it: should allow overriding ui.backendInternalUrl + - it: should carry an overridden publicBackendUrl through to the browser config template: ui-deployment.yaml set: ui: - backendInternalUrl: "http://custom-controller:9090/api" + publicBackendUrl: "https://api.example.com/api" asserts: - contains: path: spec.template.spec.containers[0].env content: - name: BACKEND_INTERNAL_URL - value: "http://custom-controller:9090/api" + name: KAGENT_API_BASE_URL + value: "https://api.example.com/api" + + - it: should fall back to /api when publicBackendUrl is blanked + template: ui-deployment.yaml + set: + ui: + publicBackendUrl: "" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: KAGENT_API_BASE_URL + value: "/api" - - it: should allow overriding ui.publicBackendUrl + - it: should reflect overridden auth and stream settings template: ui-deployment.yaml set: ui: - publicBackendUrl: "https://example.com/api" + auth: + ssoRedirectPath: /custom/start + streamTimeoutSeconds: 90 asserts: - contains: + path: spec.template.spec.containers[0].env + content: + name: SSO_REDIRECT_PATH + value: "/custom/start" + - contains: + path: spec.template.spec.containers[0].env + content: + name: KAGENT_STREAM_TIMEOUT_MS + value: "90000" + + - it: should not set env vars that only the removed node server consumed + template: ui-deployment.yaml + asserts: + - notContains: path: spec.template.spec.containers[0].env content: name: NEXT_PUBLIC_BACKEND_URL - value: "https://example.com/api" + value: "/api" + - notContains: + path: spec.template.spec.containers[0].env + content: + name: NEXT_PUBLIC_SSO_REDIRECT_PATH + value: "/oauth2/start" + - notContains: + path: spec.template.spec.containers[0].env + content: + name: BACKEND_INTERNAL_URL + value: "http://RELEASE-NAME-controller.NAMESPACE.svc:8083/api" + # The browser reaches gRPC over the same origin, through nginx, so there is + # no separate gRPC URL for anything to be configured with. + - notContains: + path: spec.template.spec.containers[0].env + content: + name: BACKEND_GRPC_URL + value: "http://RELEASE-NAME-controller.NAMESPACE.svc:8084" + + - it: should not mount a nextjs build cache + template: ui-deployment.yaml + asserts: + - notContains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: nextjs-cache + mountPath: /app/ui/.next/cache diff --git a/helm/kagent/tests/ui-nginx-configmap_test.yaml b/helm/kagent/tests/ui-nginx-configmap_test.yaml index 26e0ae1fc..96c673772 100644 --- a/helm/kagent/tests/ui-nginx-configmap_test.yaml +++ b/helm/kagent/tests/ui-nginx-configmap_test.yaml @@ -14,7 +14,11 @@ tests: value: RELEASE-NAME-ui-config - exists: path: data["nginx.conf"] - - exists: + + - it: should no longer ship a supervisord config + template: ui-nginx-configmap.yaml + asserts: + - notExists: path: data["supervisord.conf"] - it: should only have IPv4 listen by default @@ -40,32 +44,141 @@ tests: path: data["nginx.conf"] pattern: "listen \\[::\\]:8080;" - - it: should use IPv4 HOSTNAME by default + - it: should proxy api upstream to controller Cluster DNS template: ui-nginx-configmap.yaml asserts: - matchRegex: - path: data["supervisord.conf"] - pattern: 'HOSTNAME="0\.0\.0\.0"' + path: data["nginx.conf"] + pattern: 'server RELEASE-NAME-controller\.NAMESPACE\.svc:8083;' + + # --------------------------------------------------------------------------- + # Static SPA serving + # --------------------------------------------------------------------------- + - it: should serve the built bundle from disk with a SPA fallback + template: ui-nginx-configmap.yaml + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: 'root /usr/share/nginx/html;' + # Deep links such as /agents are client-side routes, not files. + - matchRegex: + path: data["nginx.conf"] + pattern: 'try_files \$uri \$uri/ /index\.html;' + + - it: should drop the node upstream now that no server process runs + template: ui-nginx-configmap.yaml + asserts: - notMatchRegex: - path: data["supervisord.conf"] - pattern: 'HOSTNAME="::"' + path: data["nginx.conf"] + pattern: 'upstream kagent_ui' + - notMatchRegex: + path: data["nginx.conf"] + pattern: '127\.0\.0\.1:8001' - - it: should use dual-stack HOSTNAME when ipv6 is enabled + - it: should keep the health endpoint template: ui-nginx-configmap.yaml - set: - ipv6: - enabled: true asserts: - matchRegex: - path: data["supervisord.conf"] - pattern: 'HOSTNAME="::"' + path: data["nginx.conf"] + pattern: "location /health" + + # The name matters as much as the caching: the document loads this by that exact + # path before the app, so a rename here is a blank page rather than a stale setting. + - it: should serve runtime env-config.js uncached + template: ui-nginx-configmap.yaml + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: 'location = /env-config\.js' + - matchRegex: + path: data["nginx.conf"] + pattern: 'alias /tmp/kagent/env-config\.js;' + - matchRegex: + path: data["nginx.conf"] + pattern: 'add_header Cache-Control "no-store" always;' + + - it: should refuse the mock service worker outright + template: ui-nginx-configmap.yaml + asserts: + # The SPA fallback would otherwise answer this with index.html and a 200. + # An exact-match location also beats the fallback if a future build ever + # ships the worker again. + - matchRegex: + path: data["nginx.conf"] + pattern: 'location = /mockServiceWorker\.js' + - matchRegex: + path: data["nginx.conf"] + pattern: 'return 404;' + + - it: should never cache the bundle entrypoint + template: ui-nginx-configmap.yaml + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: 'location = /index\.html' + + # --------------------------------------------------------------------------- + # Streaming + # --------------------------------------------------------------------------- + - it: should route a2a to the backend, not the removed node server + template: ui-nginx-configmap.yaml + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_pass http://kagent_backend/a2a/;' - notMatchRegex: - path: data["supervisord.conf"] - pattern: 'HOSTNAME="0\.0\.0\.0"' + path: data["nginx.conf"] + pattern: 'proxy_pass http://kagent_ui' - - it: should proxy api upstream to controller Cluster DNS + - it: should keep streaming settings on a2a so SSE is not buffered template: ui-nginx-configmap.yaml + set: + ui: + nginx: + proxyReadTimeout: 900s + proxySendTimeout: 800s asserts: - matchRegex: path: data["nginx.conf"] - pattern: 'server RELEASE-NAME-controller\.NAMESPACE\.svc:8083;' + pattern: 'proxy_buffering off;' + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_read_timeout 900s;' + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_send_timeout 800s;' + + # --------------------------------------------------------------------------- + # Identity header hygiene + # + # The Next proxy forwarded a strict allowlist. nginx forwards everything by + # default, so auth-proxy identity headers are cleared unless opted in. + # --------------------------------------------------------------------------- + - it: should strip spoofable identity headers by default + template: ui-nginx-configmap.yaml + asserts: + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_set_header x-auth-request-email "";' + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_set_header x-forwarded-email "";' + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_set_header x-forwarded-user "";' + + - it: should forward an identity header the operator opted into + template: ui-nginx-configmap.yaml + set: + ui: + additionalForwardedHeaders: + - X-Forwarded-Email + asserts: + # Opted in, so no longer cleared... + - notMatchRegex: + path: data["nginx.conf"] + pattern: 'proxy_set_header x-forwarded-email "";' + # ...while the rest of the set stays stripped. + - matchRegex: + path: data["nginx.conf"] + pattern: 'proxy_set_header x-forwarded-user "";' diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 330001c8c..2e3875e99 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -443,21 +443,26 @@ ui: # -- proxy_send_timeout: max time between two successive writes to the upstream. proxySendTimeout: 1800s env: {} # Additional configuration key-value pairs for the ui ConfigMap - # -- Additional request headers (beyond Authorization) the UI proxy will forward - # to the backend. Names are case-insensitive. Hop-by-hop headers (Connection, - # Transfer-Encoding, etc.) are silently dropped. + # -- Identity headers the UI's nginx proxy will forward to the backend on + # /api/ and /a2a/. Names are case-insensitive. Authorization is always + # forwarded; the auth-proxy identity headers (x-auth-request-*, x-forwarded-user, + # x-forwarded-email, x-forwarded-groups, x-forwarded-preferred-username) are + # stripped from client requests unless listed here, so a caller cannot spoof + # an identity the backend trusts. Headers outside that set are forwarded by + # nginx as normal. additionalForwardedHeaders: [] # Example: # additionalForwardedHeaders: # - X-Forwarded-User # - X-Forwarded-Email - # Browser uses this path on the UI hostname (nginx proxies /api → controller). Required for client-side /api and WebSockets from outside the cluster. + # -- Base URL the browser calls the controller API on. Reaches the browser at + # runtime as the `apiBaseUrl` key of /config.json, which the app fetches on + # startup — it is deliberately not baked into the bundle, so changing it here + # takes effect on pod restart rather than requiring an image rebuild. + # The default is a path on the UI's own hostname, which nginx proxies to the + # controller; set an absolute URL only if the browser must reach the API + # somewhere other than the UI origin. publicBackendUrl: "/api" - # Next.js server HTTP base for A2A and other protocol-native routes. The - # kagent application API uses backendGrpcUrl instead. - backendInternalUrl: "" - # Next.js server-only native gRPC target. Include http:// for internal h2c. - backendGrpcUrl: "" # -- Pod-level security context for the UI pod. Overrides the global podSecurityContext. # @default -- (uses global podSecurityContext) podSecurityContext: {} @@ -466,11 +471,10 @@ ui: # @default -- (uses global securityContext) securityContext: {} # readOnlyRootFilesystem: true - # -- EmptyDir volume sizes for Next.js UI workload (typically used when enabling readOnlyRootFilesystem) + # -- EmptyDir volume sizes for the UI workload (typically used when enabling readOnlyRootFilesystem) volumes: - # -- Size limit for Next.js build cache (.next/cache). Default 100Mi is sufficient for typical Next.js apps with moderate caching needs. - nextjsCache: 100Mi - # -- Size limit for temporary files (/tmp). Default 50Mi provides ample space for Next.js runtime temporary data. + # -- Size limit for temporary files (/tmp). Holds the nginx temp directories + # and the generated config.json. Default 50Mi is ample for both. tmp: 50Mi # capabilities: # drop: @@ -821,7 +825,11 @@ oauth2-proxy: # Skip authentication for kagent's branded login page, health checks, and static assets # This allows unauthenticated users to see the landing page and k8s probes to work skip-auth-route: "^/(health|login)$" - skip-auth-regex: "^/(login|_next/static|_next/image|login-bg\\.(jpg|png|webp)|logo-.*\\.png|favicon\\.ico|api/agentharnesses/.*/gateway).*$" + # The single-page app serves /login from index.html and loads its bundle from + # /assets/ with env-config.js ahead of it, so all three have to be reachable + # unauthenticated or the branded login page renders blank. The Next.js paths + # this replaces (_next/static, _next/image, login-bg) no longer exist. + skip-auth-regex: "^/(login|assets/|env-config\\.js|logo-.*\\.png|favicon\\.ico|api/agentharnesses/.*/gateway).*$" # Use custom templates that redirect to kagent's branded /login page custom-templates-dir: "/templates" diff --git a/helm/tools/grafana-mcp/templates/_helpers.tpl b/helm/tools/grafana-mcp/templates/_helpers.tpl index af4da7f48..1f2d8dcbb 100644 --- a/helm/tools/grafana-mcp/templates/_helpers.tpl +++ b/helm/tools/grafana-mcp/templates/_helpers.tpl @@ -68,6 +68,39 @@ Create the grafana server URL {{- printf "http://%s.%s:%d/mcp" (include "grafana-mcp.fullname" .) .Release.Namespace (.Values.service.port | int) }} {{- end }} +{{/* +Host header values the MCP server will answer to. + +The server rejects any Host it was not told about — protection against DNS rebinding, +which is aimed at browsers and applies to every client. Its default allow-list is the +loopback forms of --address, so a server reached over the cluster network answers +"forbidden: host not allowed" to the handshake and reports no tools at all. + +Derived from the same fullname, namespace and port as grafana-mcp.serverUrl, because that +URL is precisely what the controller dials: the two cannot be allowed to disagree. The +shorter in-cluster forms are included for anything addressing the service directly, and +loopback for a port-forward. Set allowedHosts to override, including "*" to switch the +check off behind a proxy that rewrites Host. +*/}} +{{- define "grafana-mcp.allowedHosts" -}} +{{- if .Values.allowedHosts -}} +{{- .Values.allowedHosts -}} +{{- else -}} +{{- $name := include "grafana-mcp.fullname" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := .Values.service.port | int -}} +{{- $hosts := list + (printf "%s:%d" $name $port) + (printf "%s.%s:%d" $name $ns $port) + (printf "%s.%s.svc:%d" $name $ns $port) + (printf "%s.%s.svc.cluster.local:%d" $name $ns $port) + (printf "localhost:%d" $port) + (printf "127.0.0.1:%d" $port) +-}} +{{- join "," $hosts -}} +{{- end -}} +{{- end }} + {{/* Join registry/repository/name/tag for grafana-mcp image, skipping empty segments, then append tag */}} diff --git a/helm/tools/grafana-mcp/templates/deployment.yaml b/helm/tools/grafana-mcp/templates/deployment.yaml index bf3be83f3..299610920 100644 --- a/helm/tools/grafana-mcp/templates/deployment.yaml +++ b/helm/tools/grafana-mcp/templates/deployment.yaml @@ -40,6 +40,8 @@ spec: args: - --transport - streamable-http + - -allowed-hosts + - {{ include "grafana-mcp.allowedHosts" . | quote }} {{- with .Values.args }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/helm/tools/grafana-mcp/tests/deployment_test.yaml b/helm/tools/grafana-mcp/tests/deployment_test.yaml index d1ed1c4ad..0f2ae3aa9 100644 --- a/helm/tools/grafana-mcp/tests/deployment_test.yaml +++ b/helm/tools/grafana-mcp/tests/deployment_test.yaml @@ -119,4 +119,41 @@ tests: value: grafana:latest - notMatchRegex: path: spec.template.spec.containers[0].image - pattern: "^/" # no leading slash \ No newline at end of file + pattern: "^/" # no leading slash + + # ============================================================================= + # Host allow-list + # + # The server rejects any Host it was not told about, and its own default covers only + # loopback — so without these args it answers the handshake with "forbidden: host not + # allowed" and the RemoteMCPServer reports no tools. The first assertion holds the + # allow-list to the URL in remotemcpserver.yaml: if that URL ever changes shape, the + # host it implies has to change with it. + # ============================================================================= + - it: should allow the host the RemoteMCPServer URL dials + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: -allowed-hosts + - equal: + path: spec.template.spec.containers[0].args[3] + value: RELEASE-NAME-grafana-mcp:8000,RELEASE-NAME-grafana-mcp.NAMESPACE:8000,RELEASE-NAME-grafana-mcp.NAMESPACE.svc:8000,RELEASE-NAME-grafana-mcp.NAMESPACE.svc.cluster.local:8000,localhost:8000,127.0.0.1:8000 + + - it: should follow the service port into the allow-list + template: deployment.yaml + set: + service.port: 9100 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].args[3] + pattern: RELEASE-NAME-grafana-mcp\.NAMESPACE:9100 + + - it: should let allowedHosts override the default + template: deployment.yaml + set: + allowedHosts: "*" + asserts: + - equal: + path: spec.template.spec.containers[0].args[3] + value: "*" diff --git a/helm/tools/grafana-mcp/values.yaml b/helm/tools/grafana-mcp/values.yaml index 01f0f1f50..65c01526e 100644 --- a/helm/tools/grafana-mcp/values.yaml +++ b/helm/tools/grafana-mcp/values.yaml @@ -53,6 +53,11 @@ resources: # Additional Arguments for the mcp server args: [] +# Host header values the MCP server answers to, comma separated. Empty means the service's +# own in-cluster names and loopback, which is what the RemoteMCPServer URL uses. Set "*" +# only behind a proxy that rewrites Host. +allowedHosts: "" + # Additional volumes on the output Deployment definition. volumes: [] # - name: foo diff --git a/proto/kagent/api/v1alpha1/agent_instances.proto b/proto/kagent/api/v1alpha1/agent_instances.proto index 389801d3e..a83fab0e6 100644 --- a/proto/kagent/api/v1alpha1/agent_instances.proto +++ b/proto/kagent/api/v1alpha1/agent_instances.proto @@ -12,6 +12,7 @@ service AgentInstanceService { rpc CreateAgentInstance(CreateAgentInstanceRequest) returns (CreateAgentInstanceResponse); rpc GetAgentInstance(GetAgentInstanceRequest) returns (GetAgentInstanceResponse); rpc ListAgentInstances(ListAgentInstancesRequest) returns (ListAgentInstancesResponse); + rpc RenameAgentInstance(RenameAgentInstanceRequest) returns (RenameAgentInstanceResponse); rpc SuspendAgentInstance(SuspendAgentInstanceRequest) returns (SuspendAgentInstanceResponse); rpc ResumeAgentInstance(ResumeAgentInstanceRequest) returns (ResumeAgentInstanceResponse); rpc DeleteAgentInstance(DeleteAgentInstanceRequest) returns (DeleteAgentInstanceResponse); @@ -65,6 +66,9 @@ message AgentInstance { google.protobuf.Timestamp created_at = 11; google.protobuf.Timestamp updated_at = 12; map labels = 13; + // Reader-supplied display name for the conversation. Empty means unnamed, + // which is the state every instance created before this field existed is in. + string name = 14; } message CreateAgentInstanceRequest { @@ -75,6 +79,10 @@ message CreateAgentInstanceRequest { min_len: 1 max_len: 128 }]; + // Optional display name. Omit it to create an unnamed conversation. Unvalidated + // because empty is the ordinary case: a conversation is usually named later, or + // never. + string name = 5; } message CreateAgentInstanceResponse { @@ -96,6 +104,12 @@ message ListAgentInstancesRequest { // Includes instances created by other users when authorized. bool all_creators = 3; PageRequest page = 4; + // Narrows the list to the conversations of one agent, an agent being an + // (AgentTemplate, Harness) pair. Either may be given alone. Both are matched + // against the pair the instance's prepared revision was built from, so they + // also select instances created before these fields existed. + string agent_template = 5; + string harness = 6; } message ListAgentInstancesResponse { @@ -103,6 +117,18 @@ message ListAgentInstancesResponse { PageResponse page = 2; } +message RenameAgentInstanceRequest { + string namespace = 1; + string agent_instance_id = 2; + // The new display name. Empty clears the name, returning the conversation to + // being identified by its id. + string name = 3; +} + +message RenameAgentInstanceResponse { + AgentInstance agent_instance = 1; +} + message SuspendAgentInstanceRequest { string namespace = 1 [(buf.validate.field).string.min_len = 1]; string agent_instance_id = 2 [(buf.validate.field).string.min_len = 1]; diff --git a/proto/kagent/api/v1alpha1/agent_templates.proto b/proto/kagent/api/v1alpha1/agent_templates.proto new file mode 100644 index 000000000..9d3381f59 --- /dev/null +++ b/proto/kagent/api/v1alpha1/agent_templates.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package kagent.api.v1alpha1; + +import "kagent/api/v1alpha1/common.proto"; + +option go_package = "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1"; + +// AgentTemplateService is CRUD over the kagent.dev/v1alpha3 AgentTemplate CRD: +// the portable-behavior half of the (Harness, AgentTemplate) pair that +// CreateAgentInstance names. Without it an AgentTemplate can only be authored +// with kubectl, so no caller can offer a template picker or an edit form. +service AgentTemplateService { + rpc ListAgentTemplates(ListAgentTemplatesRequest) returns (ListAgentTemplatesResponse); + rpc GetAgentTemplate(GetAgentTemplateRequest) returns (GetAgentTemplateResponse); + rpc CreateAgentTemplate(CreateAgentTemplateRequest) returns (CreateAgentTemplateResponse); + rpc UpdateAgentTemplate(UpdateAgentTemplateRequest) returns (UpdateAgentTemplateResponse); + rpc DeleteAgentTemplate(DeleteAgentTemplateRequest) returns (DeleteAgentTemplateResponse); +} + +message AgentTemplate { + ResourceReference ref = 1; + + // Resource is the whole AgentTemplate CR. The spec is rich — model config, + // system prompt, tools, skills, plugins — and is carried verbatim rather than + // re-modelled here so that a CRD change cannot silently drift from the API. + StructuredObject resource = 2; + + // ModelConfigRef is spec.modelConfig resolved into the template's namespace. + // Denormalised because it is required on every template and is what a caller + // needs to render a list without parsing each spec. + ResourceReference model_config_ref = 3; + + string description = 4; + + // AdmittingHarnesses names the same-namespace Harnesses whose admission + // selector matches this template, as reported in status. It is the set a + // caller may legally pair with this template in CreateAgentInstance, and it + // is derivable only from the Harness side, so a caller cannot compute it. + repeated string admitting_harnesses = 5; +} + +message ListAgentTemplatesRequest { + string namespace = 1; +} + +message ListAgentTemplatesResponse { + repeated AgentTemplate agent_templates = 1; +} + +message GetAgentTemplateRequest { + ResourceReference ref = 1; +} + +message GetAgentTemplateResponse { + AgentTemplate agent_template = 1; +} + +message CreateAgentTemplateRequest { + ResourceReference ref = 1; + StructuredObject resource = 2; +} + +message CreateAgentTemplateResponse { + AgentTemplate agent_template = 1; +} + +message UpdateAgentTemplateRequest { + ResourceReference ref = 1; + StructuredObject resource = 2; +} + +message UpdateAgentTemplateResponse { + AgentTemplate agent_template = 1; +} + +message DeleteAgentTemplateRequest { + ResourceReference ref = 1; +} + +message DeleteAgentTemplateResponse {} diff --git a/proto/kagent/api/v1alpha1/agents.proto b/proto/kagent/api/v1alpha1/agents.proto index b44e5e724..954c2efbd 100644 --- a/proto/kagent/api/v1alpha1/agents.proto +++ b/proto/kagent/api/v1alpha1/agents.proto @@ -12,6 +12,10 @@ service AgentService { rpc CreateSandboxAgent(CreateSandboxAgentRequest) returns (CreateSandboxAgentResponse); rpc UpdateSandboxAgent(UpdateSandboxAgentRequest) returns (UpdateSandboxAgentResponse); rpc DeleteSandboxAgent(DeleteSandboxAgentRequest) returns (DeleteSandboxAgentResponse); + // The AgentHarness RPCs below operate on the AgentHarness CRD — one agent + // bound to an external ACP backend. They are unrelated to the Harness CRD + // that AgentInstance pairs with an AgentTemplate; that one is served by + // HarnessService in harnesses.proto. rpc GetAgentHarness(GetAgentHarnessRequest) returns (GetAgentHarnessResponse); rpc CreateAgentHarness(CreateAgentHarnessRequest) returns (CreateAgentHarnessResponse); rpc DeleteAgentHarness(DeleteAgentHarnessRequest) returns (DeleteAgentHarnessResponse); diff --git a/proto/kagent/api/v1alpha1/harnesses.proto b/proto/kagent/api/v1alpha1/harnesses.proto new file mode 100644 index 000000000..6e8f658e1 --- /dev/null +++ b/proto/kagent/api/v1alpha1/harnesses.proto @@ -0,0 +1,87 @@ +syntax = "proto3"; + +package kagent.api.v1alpha1; + +import "kagent/api/v1alpha1/common.proto"; + +option go_package = "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1"; + +// HarnessService is CRUD over the kagent.dev/v1alpha3 Harness CRD: the runtime +// and infrastructure policy half of the (Harness, AgentTemplate) pair that +// CreateAgentInstance names. +// +// Harness is NOT AgentHarness. AgentService carries GetAgentHarness / +// CreateAgentHarness / DeleteAgentHarness, and those operate on the separate +// AgentHarness CRD — a single agent bound to an external ACP backend. The +// Harness here is a reusable runtime (one of the kagent, codex or claude +// adapters, plus a Substrate worker pool and snapshot policy) that admits many +// AgentTemplates through a label selector; the v2 reconciler pairs the two in +// go/core/v2/controller/collections.go. The names collide but the kinds do not, +// so this is a separate service rather than more RPCs on AgentService. +service HarnessService { + rpc ListHarnesses(ListHarnessesRequest) returns (ListHarnessesResponse); + rpc GetHarness(GetHarnessRequest) returns (GetHarnessResponse); + rpc CreateHarness(CreateHarnessRequest) returns (CreateHarnessResponse); + rpc UpdateHarness(UpdateHarnessRequest) returns (UpdateHarnessResponse); + rpc DeleteHarness(DeleteHarnessRequest) returns (DeleteHarnessResponse); +} + +message Harness { + ResourceReference ref = 1; + + // Resource is the whole Harness CR. The spec is carried verbatim rather than + // re-modelled here so that a CRD change cannot silently drift from the API. + StructuredObject resource = 2; + + // Runtime is the adapter the spec selects: "kagent", "codex" or "claude". + // Denormalised because callers listing harnesses group and filter by it, and + // would otherwise each reimplement the exactly-one-of check the CRD enforces. + string runtime = 3; + + // WorkloadImage is spec.workload.image, the digest-pinned runtime image. + string workload_image = 4; + + // Ready mirrors the Ready status condition. False also covers a Harness the + // controller has not yet observed. + bool ready = 5; +} + +message ListHarnessesRequest { + string namespace = 1; +} + +message ListHarnessesResponse { + repeated Harness harnesses = 1; +} + +message GetHarnessRequest { + ResourceReference ref = 1; +} + +message GetHarnessResponse { + Harness harness = 1; +} + +message CreateHarnessRequest { + ResourceReference ref = 1; + StructuredObject resource = 2; +} + +message CreateHarnessResponse { + Harness harness = 1; +} + +message UpdateHarnessRequest { + ResourceReference ref = 1; + StructuredObject resource = 2; +} + +message UpdateHarnessResponse { + Harness harness = 1; +} + +message DeleteHarnessRequest { + ResourceReference ref = 1; +} + +message DeleteHarnessResponse {} diff --git a/proto/kagent/api/v1alpha1/system.proto b/proto/kagent/api/v1alpha1/system.proto index 5bc3ef780..04362094b 100644 --- a/proto/kagent/api/v1alpha1/system.proto +++ b/proto/kagent/api/v1alpha1/system.proto @@ -3,6 +3,8 @@ syntax = "proto3"; package kagent.api.v1alpha1; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "kagent/api/v1alpha1/common.proto"; option go_package = "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1"; @@ -10,7 +12,42 @@ service SystemService { rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse); rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse); + + // GetSubstrateStatus returns the entire inventory in one message: every + // worker pool, actor template, actor and worker, unpaginated and unfiltered. + // + // It does not survive a real cluster. A deployment reporting 103,134 actors + // answers with a message the gRPC client refuses outright — "trying to send + // message larger than max (43016460 vs. 16777216)" — so the caller gets no + // inventory at all rather than a large one. Raising the ceiling moves the + // number without changing the shape. + // + // Prefer GetSubstrateSummary with ListSubstrateActors and + // ListSubstrateWorkers, which bound what any single response can carry. This + // RPC is kept for callers that predate them and for the small clusters where + // it still works. rpc GetSubstrateStatus(GetSubstrateStatusRequest) returns (GetSubstrateStatusResponse); + + // GetSubstrateSummary returns counts computed server-side, plus the two lists + // that are inherently small. + // + // This is the only honest source of a total. A caller that counts a page and + // presents the result as a total reports "3 actors" for a cluster running a + // hundred thousand, which is the specific failure the paged RPCs below would + // otherwise introduce. + rpc GetSubstrateSummary(GetSubstrateSummaryRequest) returns (GetSubstrateSummaryResponse); + + // ListSubstrateActors pages the actors, narrowing them server-side. + // + // Paged because this is one of the two lists whose length is set by the + // cluster rather than by configuration, and filtered server-side for the same + // reason: narrowing a page that has already been fetched searches only what + // was fetched, so a match on page nine reads on screen as "no matches". + rpc ListSubstrateActors(ListSubstrateActorsRequest) returns (ListSubstrateActorsResponse); + + // ListSubstrateWorkers pages the worker assignments. The mirror of + // ListSubstrateActors. + rpc ListSubstrateWorkers(ListSubstrateWorkersRequest) returns (ListSubstrateWorkersResponse); } message GetVersionRequest {} @@ -51,6 +88,175 @@ message GetSubstrateStatusResponse { repeated SubstrateWorker workers = 6; } +message GetSubstrateSummaryRequest { + // Namespace narrows the inventory. Empty means every namespace the + // controller observes, as it does on GetSubstrateStatusRequest. + string namespace = 1; +} + +// SubstrateStatusCount is how many rows carry one status. +// +// Status is a plain string on the wire rather than an enum: ate-api and the +// ActorTemplate controller each fill it in their own vocabulary, so a closed +// set here would drop a status a newer substrate reports. Counting whatever +// arrives keeps the tally complete even when a value is one this build has +// never seen. +message SubstrateStatusCount { + string status = 1; + int32 count = 2; +} + +message GetSubstrateSummaryResponse { + // Enabled is false when the controller has no ate-api endpoint configured, + // which is an ordinary deployment rather than a failure. + bool enabled = 1; + + // AteApiError is set when ate-api answered with an error on an otherwise + // successful read: the Kubernetes-derived halves below are complete while the + // runtime counts may be short. Distinct from the RPC failing, and worth + // reporting differently. + string ate_api_error = 2; + + // Worker pools and actor templates are bounded by how the cluster is + // configured rather than by how much work it is doing — a handful either way + // — so they ride inline instead of costing two more round trips. + repeated SubstrateWorkerPool worker_pools = 3; + repeated SubstrateActorTemplate actor_templates = 4; + + // Totals over everything in scope, before any filter. + int32 actor_count = 5; + int32 worker_count = 6; + + // RunningActorCount and BusyWorkerCount are the numerators the inventory is + // actually read by: how much of what exists is doing something. A worker is + // busy when an actor is placed on it. + int32 running_actor_count = 7; + int32 busy_worker_count = 8; + + // ActorStatusCounts is every status present, with how many actors hold it, + // ordered by status. The whole distribution rather than the running count + // alone, so a caller can say what the rest are without reading them. + repeated SubstrateStatusCount actor_status_counts = 9; + + // ComputedAt is when this answer was produced, which is not necessarily now. + // + // The substrate reads are memoised for a fraction of a second, because each one + // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of + // them. A cache is also exactly how a polling control becomes a lie, so the age + // travels with the answer: a caller can say "as of 0.4s ago" rather than + // implying "now", and a reader can tell a stalled cluster from a stalled read. + google.protobuf.Timestamp computed_at = 10; +} + +// SubstrateSortOrder is the direction a paged substrate read is sorted in. +enum SubstrateSortOrder { + // Unspecified sorts ascending, which is what every default order below reads + // naturally in. + SUBSTRATE_SORT_ORDER_UNSPECIFIED = 0; + SUBSTRATE_SORT_ORDER_ASCENDING = 1; + SUBSTRATE_SORT_ORDER_DESCENDING = 2; +} + +// SubstrateActorSortField is the column ListSubstrateActors orders by. +// +// Every order ends in the actor id, which is unique — so a page token, which is +// the sort key of the last row already sent, always identifies exactly one row. +// A key that could tie would skip or repeat rows at a page boundary. +enum SubstrateActorSortField { + // Unspecified groups by status and orders by id within each group. That is the + // order the inventory is most usefully read in, and it is stable: ate-api + // returns actors in whatever order it holds them, so an unsorted list puts a + // different actor on every page each time it is asked. + SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED = 0; + SUBSTRATE_ACTOR_SORT_FIELD_STATUS = 1; + SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID = 2; + SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE = 3; + SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD = 4; +} + +// SubstrateWorkerSortField is the column ListSubstrateWorkers orders by. +// Every order ends in the worker pod, which is unique within its namespace. +enum SubstrateWorkerSortField { + // Unspecified groups by pool and orders by pod within each group. + SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED = 0; + SUBSTRATE_WORKER_SORT_FIELD_POOL = 1; + SUBSTRATE_WORKER_SORT_FIELD_POD = 2; + SUBSTRATE_WORKER_SORT_FIELD_ACTOR = 3; +} + +message ListSubstrateActorsRequest { + string namespace = 1; + + // Filter is matched case-insensitively as a substring against the actor's id, + // status, actor template and worker pod — the fields a row displays. Empty + // matches everything. + string filter = 2; + + PageRequest page = 3; + + // Sorting is server-side because the rows are paged: ordering a page that has + // already been fetched reorders a hundred rows out of hundreds of thousands, + // which looks like sorting and is not. + SubstrateActorSortField sort_field = 4; + SubstrateSortOrder sort_order = 5; +} + +message ListSubstrateActorsResponse { + repeated SubstrateActor actors = 1; + PageResponse page = 2; + + // TotalSize is how many actors match the filter across every page, so a + // caller can say "20 of 4,312" rather than implying the page is the whole + // result. + int32 total_size = 3; + + // The order actually applied, so a caller can say how the rows are sorted + // rather than assuming its request was honoured. An unspecified field and an + // unspecified order both resolve to a concrete value here. + SubstrateActorSortField applied_sort_field = 4; + SubstrateSortOrder applied_sort_order = 5; + + // ComputedAt is when this answer was produced, which is not necessarily now. + // + // The substrate reads are memoised for a fraction of a second, because each one + // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of + // them. A cache is also exactly how a polling control becomes a lie, so the age + // travels with the answer: a caller can say "as of 0.4s ago" rather than + // implying "now", and a reader can tell a stalled cluster from a stalled read. + google.protobuf.Timestamp computed_at = 6; +} + +message ListSubstrateWorkersRequest { + string namespace = 1; + + // Filter is matched case-insensitively as a substring against the worker's + // namespace, pod, pool and placed actor. + string filter = 2; + + PageRequest page = 3; + + SubstrateWorkerSortField sort_field = 4; + SubstrateSortOrder sort_order = 5; +} + +message ListSubstrateWorkersResponse { + repeated SubstrateWorker workers = 1; + PageResponse page = 2; + int32 total_size = 3; + + SubstrateWorkerSortField applied_sort_field = 4; + SubstrateSortOrder applied_sort_order = 5; + + // ComputedAt is when this answer was produced, which is not necessarily now. + // + // The substrate reads are memoised for a fraction of a second, because each one + // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of + // them. A cache is also exactly how a polling control becomes a lie, so the age + // travels with the answer: a caller can say "as of 0.4s ago" rather than + // implying "now", and a reader can tell a stalled cluster from a stalled read. + google.protobuf.Timestamp computed_at = 6; +} + message SubstrateWorkerPool { string namespace = 1; string name = 2; diff --git a/python/packages/kagent-adk/src/kagent/adk/_a2a.py b/python/packages/kagent-adk/src/kagent/adk/_a2a.py index f2ea43153..e8cc5cc8e 100644 --- a/python/packages/kagent-adk/src/kagent/adk/_a2a.py +++ b/python/packages/kagent-adk/src/kagent/adk/_a2a.py @@ -218,7 +218,11 @@ async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): @asynccontextmanager async def lifespan(app: FastAPI): - server = await asyncio.start_server(handle, host="::", port=8081) + # Every interface on every family, not just IPv6. `host="::"` bound an + # IPv6-only socket, so a probe of the actor's IPv4 address was refused — + # Substrate dials one, and the harness sat in ResumeGoldenActor until the + # golden actor timed out, reporting only "connection refused". + server = await asyncio.start_server(handle, host=None, port=8081) try: yield finally: diff --git a/ui/.dockerignore b/ui/.dockerignore index fcb3ee2f5..7f2396387 100644 --- a/ui/.dockerignore +++ b/ui/.dockerignore @@ -6,4 +6,9 @@ env/ htmlcov/ workspace/ node_modules/ -.next/ \ No newline at end of file +# Build output — the builder stage produces its own, and a stale local copy +# would only bloat the build context. +dist/ +# Test artifacts. +test-results/ +playwright-report/ diff --git a/ui/.env.example b/ui/.env.example new file mode 100644 index 000000000..69b752691 --- /dev/null +++ b/ui/.env.example @@ -0,0 +1,92 @@ +# Local development settings. Copy to `.env` — which is git-ignored — and edit. +# +# These are the same names the deployment sets, so what you put here is what an +# operator would configure in the chart. The dev server inlines them into the +# page the way the container renders them at startup; only the keys listed in +# `src/env.ts` and `vite.config.ts` are passed through, so nothing else in your +# shell reaches the browser. +# +# A change here needs no restart, but it does force a full page reload — the +# values are read once as the app boots. +# +# Three sections, in the order the code decides them: what the application reads +# (`CORE_ENV_KEYS` in `src/env.ts`), what an installed extension reads +# (`EXTENSION_ENV_KEYS` in `vite.config.ts`), and last, anything belonging to an +# extension's own tooling, which the application never sees. + + +# --- Settings the application reads --- + +# Serve the whole API from in-browser fixtures. +# +# Off unless you ask for it, in a dev server exactly as in a built image. The dev +# server used to default to fixtures, and the convenience was paid for the wrong +# way round: a backend that was down or misconfigured rendered as a healthy app +# full of plausible data, and you found out by noticing a name you did not +# recognise. Asking is cheap: set this true. (`?mock=ok` only picks which scenario +# the fixtures play — error, empty, slow — and does nothing unless mock mode is +# already on, because the mock backend is a service worker `main.tsx` starts only +# in that mode.) +# +# When it is on it wins over every backend setting below: the app serves fixtures +# even with a real backend configured and running, and anything that reports who is +# signed in says nobody is, because in mock mode there is no backend to have +# signed in to. +# ENABLE_MOCK_UI=true + +# Where the browser calls the API. Relative by default, which the dev server +# proxies to KAGENT_DEV_CONTROLLER_URL (default http://127.0.0.1:8083). +# API_BASE_URL=/api + +# Chat stream inactivity timeout, in milliseconds. +# STREAM_TIMEOUT_MS=1800000 + +# Where "Sign in with SSO" sends the browser. +# SSO_REDIRECT_PATH=/oauth2/start + + +# --- Settings an installed extension reads --- +# +# The application does nothing with these itself. They are listed in +# `EXTENSION_ENV_KEYS` so a developer can set them in `.env` and have them reach +# the browser; with no extension installed, setting them has no effect. + +# A management plane that fronts each cluster's kagent API. Unset means no proxy: +# requests go straight to API_BASE_URL, which is what `yarn dev` and the e2e suite +# want. Setting it turns on the /proxy/cluster//api prefix. +# UI_BACKEND_HOST=http://localhost:8080 + +# The cluster that management plane is installed on, and the default scope until +# another one is picked from the header. +# LOCAL_CLUSTER_NAME=mgmt-cluster + +# A bearer token for that backend. Set it to supply your own; an extension's own +# tooling may mint one per run instead, in which case leave it unset — a stale +# token here would be inherited by a whole test run and fail it half way through +# on 401s that look like a product fault. +# UI_BACKEND_TOKEN= + +# Real sign-in in the browser. +# +# With an issuer set, an extension can run the authorization code flow with PKCE +# itself rather than expecting an authentication proxy in front of it. A token +# obtained that way outranks UI_BACKEND_TOKEN — a stale token in this file must +# never outrank the person signing in. +# +# The client must be public, with the standard flow enabled, and must allow this +# app's origin as a redirect URI. If the provider serves a self-signed +# certificate the browser has to be told to trust it once: open the issuer URL and +# accept it, or the token request is blocked with nothing shown on the page. +# OIDC_ISSUER=https://keycloak.default:8443/realms/kagent-dev +# OIDC_CLIENT_ID=kagent-ui +# OIDC_SCOPES=openid profile email +# OIDC_REDIRECT_PATH=/callback + + +# --- An installed extension's own settings go below this line --- +# +# Anything read by an extension's tooling rather than by the application: ports, +# credentials for minting a token, whatever its scripts need. Nothing above this +# line depends on it, so a branch that installs an extension appends here rather +# than editing the sections above — which is what keeps a merge from this file a +# merge rather than a negotiation. diff --git a/ui/.gitignore b/ui/.gitignore index d0c6d7562..4ee1d16ce 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -2,49 +2,38 @@ # dependencies /node_modules -/.pnp -.pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions +# build output +/dist + # testing /coverage -# next.js -/.next/ -/out/ - -# production -/build - # misc .DS_Store *.pem # debug -npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* # env files (can opt-in for committing if needed) .env* - -# vercel -.vercel +# Committed on purpose: it documents the settings, and holds no values. +!.env.example # typescript *.tsbuildinfo -next-env.d.ts - -# storybook -/storybook-static # playwright /playwright-report -/playwright/test-results +/test-results /playwright/.cache -/playwright/.e2e-pids.json + +# The saved sign-in for the live suite. A real credential — never committed. +playwright/.auth/ diff --git a/ui/.prettierignore b/ui/.prettierignore new file mode 100644 index 000000000..37a4ad045 --- /dev/null +++ b/ui/.prettierignore @@ -0,0 +1,2 @@ +# Disable Prettier for the UI workspace when opened directly in an editor. ESLint is the formatter source of truth. +**/* diff --git a/ui/.storybook/main.ts b/ui/.storybook/main.ts deleted file mode 100644 index 2ee27aac4..000000000 --- a/ui/.storybook/main.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { StorybookConfig } from '@storybook/nextjs-vite'; -import { fileURLToPath } from 'node:url'; - -const config: StorybookConfig = { - stories: [ - "../src/**/*.mdx", - "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" - ], - addons: [ - "@chromatic-com/storybook", - "@storybook/addon-vitest", - "@storybook/addon-a11y", - "@storybook/addon-docs", - "@storybook/addon-onboarding" - ], - framework: "@storybook/nextjs-vite", - staticDirs: ["../public"], - viteFinal: async (config) => { - config.resolve ??= {}; - const aliases = Array.isArray(config.resolve.alias) - ? config.resolve.alias - : Object.entries(config.resolve.alias ?? {}).map(([find, replacement]) => ({ find, replacement })); - config.resolve.alias = [ - { - find: "@/lib/grpc/client", - replacement: fileURLToPath(new URL("./mocks/grpc-client.ts", import.meta.url)), - }, - { - find: "@/app/actions/sessions", - replacement: fileURLToPath(new URL("./mocks/sessions.ts", import.meta.url)), - }, - { - find: "@/app/actions/mcp-apps", - replacement: fileURLToPath(new URL("./mocks/mcp-apps.ts", import.meta.url)), - }, - { - find: "@/app/actions/agents", - replacement: fileURLToPath(new URL("./mocks/agents.ts", import.meta.url)), - }, - { - find: "@/app/actions/sessionShares", - replacement: fileURLToPath(new URL("./mocks/session-shares.ts", import.meta.url)), - }, - { - find: "@/app/actions/namespaces", - replacement: fileURLToPath(new URL("./mocks/namespaces.ts", import.meta.url)), - }, - ...aliases, - ]; - return config; - }, -}; -export default config; diff --git a/ui/.storybook/mocks/agents.ts b/ui/.storybook/mocks/agents.ts deleted file mode 100644 index c3ac50c9e..000000000 --- a/ui/.storybook/mocks/agents.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { fn } from "storybook/test"; - -import type { AgentFormData } from "@/lib/agentFormDomain"; -import type { Agent, AgentResponse, BaseResponse } from "@/types"; - -export const getAgent = fn< - ( - agentName: string, - namespace: string, - kubernetesKind?: string, - ) => Promise> ->(async () => ({ message: "Agent not found" })); - -export const getAgentWithResolvedKind = fn< - (agentName: string, namespace: string) => Promise> ->(async () => ({ message: "Agent not found" })); - -export const waitForSandboxAgentReady = fn< - ( - agentName: string, - namespace: string, - options?: { timeoutMs?: number; intervalMs?: number }, - ) => Promise<{ ok: boolean; error?: string }> ->(async () => ({ ok: true })); - -export const deleteAgent = fn< - ( - agentName: string, - namespace: string, - kubernetesKind?: string, - ) => Promise> ->(async () => ({ message: "Agent deleted" })); - -export const createAgent = fn< - (agentConfig: AgentFormData, update?: boolean) => Promise> ->(async () => ({ message: "Agent saved" })); - -export const getAgents = fn< - (options?: { namespace?: string }) => Promise> ->(async () => ({ message: "Agents fetched", data: [] })); diff --git a/ui/.storybook/mocks/grpc-client.ts b/ui/.storybook/mocks/grpc-client.ts deleted file mode 100644 index 5927ab6d3..000000000 --- a/ui/.storybook/mocks/grpc-client.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type AgentKubernetesKind = "Agent" | "SandboxAgent" | "AgentHarness"; - -function unavailableGateway(name: string): Promise { - return Promise.reject( - new Error( - `${name} is unavailable in Storybook. Mock the server action used by this story.`, - ), - ); -} - -export const getSystemGrpcGateway = () => unavailableGateway("System gRPC gateway"); -export const getFeedbackGrpcGateway = () => unavailableGateway("Feedback gRPC gateway"); -export const getModelGrpcGateway = () => unavailableGateway("Model gRPC gateway"); -export const getAgentGrpcGateway = () => unavailableGateway("Agent gRPC gateway"); -export const getToolGrpcGateway = () => unavailableGateway("Tool gRPC gateway"); -export const getPromptTemplateGrpcGateway = () => unavailableGateway("Prompt template gRPC gateway"); -export const getSessionGrpcGateway = () => unavailableGateway("Session gRPC gateway"); -export const getMemoryGrpcGateway = () => unavailableGateway("Memory gRPC gateway"); diff --git a/ui/.storybook/mocks/mcp-apps.ts b/ui/.storybook/mocks/mcp-apps.ts deleted file mode 100644 index 0f1c990ca..000000000 --- a/ui/.storybook/mocks/mcp-apps.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; -import { fn } from "storybook/test"; - -import type { BaseResponse } from "@/types"; - -export interface McpAppTool { - name: string; - description?: string; - inputSchema?: unknown; - uiResourceUri?: string; - _meta?: Record; -} - -export const listMcpAppTools = fn< - (namespace: string, name: string, groupKind?: string) => Promise> ->(); -export const callMcpAppTool = fn< - ( - namespace: string, - name: string, - toolName: string, - args?: Record, - groupKind?: string, - ) => Promise> ->(); -export const readMcpAppResource = fn< - ( - namespace: string, - name: string, - uri: string, - groupKind?: string, - ) => Promise> ->(); diff --git a/ui/.storybook/mocks/namespaces.ts b/ui/.storybook/mocks/namespaces.ts deleted file mode 100644 index f8c04d35c..000000000 --- a/ui/.storybook/mocks/namespaces.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { fn } from "storybook/test"; - -import type { BaseResponse } from "@/types"; - -export interface NamespaceResponse { - name: string; - status: string; -} - -export const listNamespaces = fn< - () => Promise> ->(async () => ({ - message: "Namespaces fetched", - data: [ - { name: "default", status: "Active" }, - { name: "kagent", status: "Active" }, - ], -})); diff --git a/ui/.storybook/mocks/session-shares.ts b/ui/.storybook/mocks/session-shares.ts deleted file mode 100644 index 9051f1ef8..000000000 --- a/ui/.storybook/mocks/session-shares.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { fn } from "storybook/test"; - -import type { BaseResponse } from "@/types"; - -export interface SessionShare { - token: string; - session_id: string; - read_only: boolean; - created_at: string; -} - -export const createSessionShare = fn< - (sessionId: string, readOnly?: boolean) => Promise> ->(async () => ({ message: "Share creation is not configured in this story" })); - -export const listSessionShares = fn< - (sessionId: string) => Promise> ->(async () => ({ message: "Shares listed", data: [] })); - -export const deleteSessionShare = fn< - (sessionId: string, token: string) => Promise> ->(async () => ({ message: "Share deleted" })); diff --git a/ui/.storybook/mocks/sessions.ts b/ui/.storybook/mocks/sessions.ts deleted file mode 100644 index 7980f4959..000000000 --- a/ui/.storybook/mocks/sessions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Task } from "@a2a-js/sdk"; -import { fn } from "storybook/test"; - -import type { BaseResponse, CreateSessionRequest, Session } from "@/types"; - -export interface SessionWithEvents { - session: Session; - events: unknown[]; - read_only?: boolean | null; -} - -export const deleteSession = fn< - (sessionId: string) => Promise> ->(); -export const getSession = fn< - (sessionId: string, shareToken?: string) => Promise> ->(); -export const getSessionsForAgent = fn< - (namespace: string, agentName: string) => Promise> ->(); -export const createSession = fn< - (session: CreateSessionRequest) => Promise> ->(); -export const renameSession = fn< - (sessionId: string, name: string) => Promise> ->(); -export const getSessionTasks = fn< - (sessionId: string, shareToken?: string) => Promise> ->(); -export const getSubagentSessionWithEvents = fn< - (sessionId: string) => Promise> ->(); -export const getSessionWithEvents = fn< - (sessionId: string, shareToken?: string) => Promise> ->(); -export const checkSessionExists = fn< - (sessionId: string) => Promise> ->(); diff --git a/ui/.storybook/preview.tsx b/ui/.storybook/preview.tsx deleted file mode 100644 index 09af5b7f7..000000000 --- a/ui/.storybook/preview.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { Preview } from '@storybook/nextjs-vite' -import React, { ReactNode } from 'react' -import '../src/app/globals.css' -import { AgentsContext } from '../src/components/AgentsProvider' -import type { AgentsContextType } from '../src/components/AgentsProvider' -import type { Agent } from '../src/types' - -const mockContextValue: AgentsContextType = { - agents: [], - models: [], - loading: false, - error: "", - tools: [], - refreshAgents: async () => {}, - refreshModels: async () => {}, - refreshTools: async () => {}, - createNewAgent: async () => ({ message: "mock", data: {} as Agent }), - updateAgent: async () => ({ message: "mock", data: {} as Agent }), - getAgent: async () => null, - validateAgentData: () => ({}), -}; - -interface MockAgentsProviderProps { - children: ReactNode; - value?: Partial; -} - -function MockAgentsProvider({ children, value }: MockAgentsProviderProps) { - return ( - - {children} - - ); -} - -const preview: Preview = { - parameters: { - nextjs: { - appDirectory: true, - }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - a11y: { - test: 'todo' - } - }, - decorators: [ - (Story) => { - document.documentElement.classList.add('dark'); - return ( - - - - ); - }, - ], -}; - -export default preview; diff --git a/ui/.storybook/vitest.setup.ts b/ui/.storybook/vitest.setup.ts deleted file mode 100644 index c5ed05fa0..000000000 --- a/ui/.storybook/vitest.setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; -import { setProjectAnnotations } from '@storybook/nextjs-vite'; -import * as projectAnnotations from './preview'; - -// This is an important step to apply the right configuration when testing your stories. -// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); \ No newline at end of file diff --git a/ui/.yarnrc.yml b/ui/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/ui/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/ui/Dockerfile b/ui/Dockerfile index 256b2e86d..cb2ea5c8e 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -1,4 +1,4 @@ -### STAGE 1: Dependencies and Build +### STAGE 1: Dependencies ARG BASE_IMAGE_REGISTRY=cgr.dev ARG TOOLS_NODE_VERSION=24 ARG BUILDPLATFORM @@ -10,21 +10,24 @@ ARG BUILDPLATFORM ARG TOOLS_NODE_VERSION RUN echo "Installing on $BUILDPLATFORM" \ - && apk add --no-cache curl bash openssl unzip ca-certificates nginx supervisor "nodejs~${TOOLS_NODE_VERSION}" node-gyp \ + && apk add --no-cache curl bash openssl unzip ca-certificates "nodejs~${TOOLS_NODE_VERSION}" npm node-gyp \ && update-ca-certificates ENV DO_NOT_TRACK=1 -ENV NEXT_TELEMETRY_DISABLED=1 WORKDIR /app/ui -# Pin npm to package.json's "packageManager" version; the wolfi-base default -# npm is a newer major that rejects the committed package-lock.json. -COPY package*.json ./ +# Yarn 4, pinned to package.json's "packageManager" so the image and a developer +# machine resolve the lockfile identically. Corepack is the supported way to get +# it, but wolfi's nodejs package does not bundle corepack, so it comes from npm +# first. `--immutable` fails the build if yarn.lock would have to change. +COPY package.json yarn.lock .yarnrc.yml ./ RUN --mount=type=cache,target=/root/.npm,rw \ - NPM_VERSION="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"npm@\([0-9.]*\)".*/\1/p' package.json)" \ - && apk add --no-cache "npm=~${NPM_VERSION}" \ - && npm ci + YARN_VERSION="$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"yarn@\([0-9.]*\)".*/\1/p' package.json)" \ + && npm install -g corepack \ + && corepack enable \ + && corepack prepare "yarn@${YARN_VERSION}" --activate \ + && yarn install --immutable ### STAGE 2: Build FROM --platform=$BUILDPLATFORM deps AS builder @@ -32,47 +35,54 @@ FROM --platform=$BUILDPLATFORM deps AS builder # Copy source files COPY . . -# Build the application (native compilation for speed) -RUN --mount=type=cache,target=/root/.npm,rw \ - --mount=type=cache,target=/app/ui/.next/cache,rw \ - export NEXT_TELEMETRY_DEBUG=1 \ - && npm run build \ - && mkdir -p /app/ui/public +# Produces a static bundle in dist/. `yarn build` type-checks first, so a type +# error fails the image build rather than shipping. +# +# The mock service worker is a development and e2e fixture. It is dropped here +# rather than in the runtime COPY so it cannot reach the image at all: a worker +# sitting at the production web root is one build-mode mistake away from being +# registered and serving fixtures to real users. It stays in the repo, where dev +# and Playwright need it. +RUN yarn build \ + && rm -f dist/mockServiceWorker.js ### STAGE 3: Runtime +# nginx only — the UI is a static bundle now, so there is no node process and no +# node runtime in the final image. FROM $BASE_IMAGE_REGISTRY/chainguard/wolfi-base:latest AS final ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 -ENV NODE_ENV=production # This is used to print the build platform in the logs ARG BUILDPLATFORM -ARG TOOLS_NODE_VERSION RUN echo "Installing on $BUILDPLATFORM" \ - && apk add --no-cache curl bash openssl unzip ca-certificates nginx supervisor "nodejs~${TOOLS_NODE_VERSION}" \ + && apk add --no-cache curl bash openssl ca-certificates nginx \ && update-ca-certificates -RUN mkdir -p /app/ui/public /tmp/nginx/client_temp /tmp/nginx/proxy_temp /tmp/nginx/fastcgi_temp /tmp/nginx/uwsgi_temp /tmp/nginx/scgi_temp \ - && addgroup -g 1001 nginx \ - && adduser -u 1001 -G nginx -s /bin/bash -D nextjs \ - && adduser -u 1002 -G nginx -s /bin/bash -D nginx \ - && chown -vR nextjs:nginx /app/ui \ - && chown -vR nextjs:nginx /tmp/nginx/ +# `kagent` owns both the static root and the writable runtime dirs. /tmp/kagent +# holds env-config.js, which init.sh renders from env on every start — it lives +# under /tmp because the container is expected to run with a read-only root +# filesystem, where /tmp is the mounted emptyDir. +RUN addgroup -g 1001 kagent \ + && adduser -u 1001 -G kagent -s /bin/bash -D kagent \ + && mkdir -p /usr/share/nginx/html \ + /tmp/kagent \ + /tmp/nginx/client_temp \ + /tmp/nginx/proxy_temp \ + /tmp/nginx/fastcgi_temp \ + /tmp/nginx/uwsgi_temp \ + /tmp/nginx/scgi_temp \ + && chown -R kagent:kagent /usr/share/nginx/html /tmp/kagent /tmp/nginx \ + # nginx opens its compiled-in default error log before it parses the config + # that redirects logging to stderr. On a read-only root filesystem that open + # fails and every start logs an alert, so point the default at stderr too. + && mkdir -p /var/lib/nginx/logs \ + && ln -sf /dev/stderr /var/lib/nginx/logs/error.log -WORKDIR /app COPY scripts/init.sh /usr/local/bin/init.sh +COPY --from=builder --chown=kagent:kagent /app/ui/dist /usr/share/nginx/html -WORKDIR /app/ui -COPY --from=builder /app/ui/next.config.ts ./ -COPY --from=builder /app/ui/public ./public -COPY --from=builder /app/ui/package.json ./package.json -COPY --from=builder --chown=nextjs:nginx /app/ui/.next/standalone ./ -COPY --from=builder --chown=nextjs:nginx /app/ui/.next/static ./.next/static - -# Ensure correct permissions -RUN chown -R nextjs:nginx /app/ui && \ - chmod -R 755 /app && \ - chmod +x /usr/local/bin/init.sh +RUN chmod +x /usr/local/bin/init.sh EXPOSE 8080 ARG VERSION diff --git a/ui/Makefile b/ui/Makefile index 36720d5f5..a274686a3 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -1,9 +1,10 @@ # Node.js is required: https://nodejs.org/ or use nvm/brew +# The Node version is pinned in .nvmrc; Yarn ships via corepack. .PHONY: build build: - npm ci - npm run build + yarn install --immutable + yarn build .PHONY: clean clean: @@ -15,15 +16,23 @@ clean: install-node: brew install node +.PHONY: lint +lint: + yarn lint + yarn typecheck + +.PHONY: test +test: + yarn test + .PHONY: audit audit: @echo "Running security audit..." - npm audit + yarn npm audit --all --recursive @echo "Security audit completed." .PHONY: update update: @echo "Updating UI lock file..." - npm install - npm audit fix --package-lock-only - @echo "Updating UI lock file done." \ No newline at end of file + yarn up '*' + @echo "Updating UI lock file done." diff --git a/ui/README.md b/ui/README.md index cfc5fb204..09594c133 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,30 +1,107 @@ -# kagents ui +# kagent UI +React + TypeScript, built with Vite. Routing is React Router, data fetching is +SWR, components are Ant Design styled with Emotion, and the UI is extensible +through named extension points — see +[docs/vendor-extensions.md](./docs/vendor-extensions.md). -```bash -# install the dependencies -npm install +Node is pinned in [`.nvmrc`](.nvmrc); Yarn ships via corepack. + +## Running locally -# run the frontend -npm run dev +```bash +yarn install +yarn dev # http://localhost:8001 ``` -# Testing the UI against a k8s backend +**No cluster is required.** By default the app runs against an in-browser mock +backend, so a fresh checkout is usable immediately. + +### Driving states in mock mode + +Append `?mock=` to any route to force a state that is otherwise hard to +reach by hand: -Often during UI development, you'll want to test the UI against a k8s backend. +| Scenario | Effect | +| --- | --- | +| `ok` | normal data (the default) | +| `empty` | every collection comes back empty | +| `error` | requests fail, so error handling is visible | +| `slow` | responses are delayed, so loading states are visible | -To do this, you can follow the instructions in the [DEVELOPMENT.md](../DEVELOPMENT.md) file. +The choice persists across in-app navigation, e.g. `/agents?mock=error`. -Once you have kagent running, you'll need to port-forward the kagent-controller service to your local machine. +### Running against a real backend + +Follow [DEVELOPMENT.md](../DEVELOPMENT.md) to get kagent running, then +port-forward the controller: ```bash kubectl port-forward svc/kagent-controller 8083 ``` -Once you have the backend running, you can run the UI with the following command: +Then start the UI in live mode: ```bash -npm run dev +cp .env.example .env # then set ENABLE_MOCK_UI=false +yarn dev ``` -This will start the UI and connect to the backend running in the kind cluster. \ No newline at end of file +The dev server proxies `/api` and `/a2a` to `127.0.0.1:8083`, matching what nginx +does in a deployed cluster, so the app uses the same relative URLs either way. +Override the target with `KAGENT_DEV_CONTROLLER_URL`, or set `API_BASE_URL` to +call a backend directly and bypass the proxy. + +Which backend is serving is decided in exactly one place, `src/api/config.ts`. +Nothing above the data layer knows or cares. + +### Deployment settings + +Settings an operator configures per deployment cannot travel in the bundle — the +bundler inlines `import.meta.env` at build time, which would freeze the chart's +values as of the image build. They arrive on `window.environmentVariables` +instead, rendered from the pod's environment by `scripts/init.sh` on every start +and loaded by a script tag ahead of the app, so they are readable synchronously +by the first module that runs. `src/env.ts` lists them; `.env.example` documents +setting them locally. + +That synchronous delivery is load-bearing rather than incidental: the API base URL +is a module-level constant, so there is no point early enough for an awaited fetch +to have landed. + +| Setting | Effect | +| --- | --- | +| `API_BASE_URL` | where the browser calls the API (default `/api`) | +| `ENABLE_MOCK_UI` | `true` serves the whole API from in-browser fixtures | +| `SSO_REDIRECT_PATH` | where "Sign in with SSO" sends the browser | +| `STREAM_TIMEOUT_MS` | chat stream inactivity timeout | + +`ENABLE_MOCK_UI` is a development setting. The release image deliberately ships no +mock backend, so a built bundle logs a warning and uses the real API rather than +entering a mode it cannot serve. + +## Checks + +```bash +yarn typecheck # tsc +yarn lint # eslint +yarn test # unit tests (vitest) +yarn test:pw # end-to-end tests (playwright) +``` + +The end-to-end suite runs against the mock backend and needs **no cluster, no +port-forward, and no provider credentials**. See +[playwright/README.md](./playwright/README.md), and +[playwright/DEFERRED.md](./playwright/DEFERRED.md) for coverage not yet ported. + +## Layout + +| Path | Contents | +| --- | --- | +| `src/api/` | data layer — typed domain models, SWR hooks, the chat client interface | +| `src/mocks/` | in-browser mock backend and its fixtures | +| `src/pages/` | one module per route | +| `src/components/Structure/` | app shell — header, sidebar, page frame | +| `src/vendorExtensions/` | extension framework, and a worked example | +| `src/theme/` | design tokens, in one place so styling can be overridden | +| `playwright/` | end-to-end suite | diff --git a/ui/bunfig.toml b/ui/bunfig.toml deleted file mode 100644 index 4bfbf5ff5..000000000 --- a/ui/bunfig.toml +++ /dev/null @@ -1,19 +0,0 @@ -[install] - -# equivalent to `--production` flag -production = true - -# whether to install devDependencies -dev = true - -# whether to install optionalDependencies -optional = true - -# whether to install peerDependencies -peer = false - -# equivalent to `--save-text-lockfile` flag -saveTextLockfile = false - -# equivalent to `--frozen-lockfile` flag -frozenLockfile = true \ No newline at end of file diff --git a/ui/components.json b/ui/components.json deleted file mode 100644 index 91b839b30..000000000 --- a/ui/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/app/globals.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/ui/dev-scripts/README.md b/ui/dev-scripts/README.md new file mode 100644 index 000000000..91d09638f --- /dev/null +++ b/ui/dev-scripts/README.md @@ -0,0 +1,113 @@ +# Trying the UI against a real cluster + +One command builds a Kind cluster and installs **this checkout** on it — the controller +and the UI are both built from the working tree and swapped in over the chart's +published images, so what runs on the cluster is the code you are reviewing. + +```sh +./ui/dev-scripts/setup-cluster.sh # ~20 min, mostly image builds +``` + +Then forward the UI and open it: + +```sh +kubectl -n kagent port-forward svc/kagent-ui 8080:8080 +``` + +**http://localhost:8080** + +The script leaves one agent on the cluster — an `assistant` template on a `kagent` +harness — so **Agents** has something in it and you can start a conversation straight +away. Add more from **Agents → New harness / New template**; a template is only run by +a harness that admits its labels, and with a single harness on the cluster the +new-template form applies those labels for you. + +## Iterating on the UI + +The dev server talks to the same cluster, with hot reload, and is the better loop while +changing code. It reaches the controller directly rather than through the UI pod's +nginx, so it needs its own port-forward: + +```sh +cd ui +kubectl -n kagent port-forward svc/kagent-controller 8083:8083 & +yarn dev +``` + +**http://localhost:8001** + +Worth knowing which one you are looking at: the dev server does not exercise the +Dockerfile, nginx, or `scripts/init.sh` rendering settings at start. To see a change +the way it will ship, rebuild the image and swap it in: + +```sh +docker buildx build --push --platform linux/arm64 \ + -t localhost:5001/kagent-dev/kagent/ui:dev -f ui/Dockerfile ./ui +kubectl -n kagent rollout restart deploy/kagent-ui +``` + +## No cluster at all + +If you would rather not build one, the UI runs entirely on in-browser fixtures: + +```sh +cd ui +ENABLE_MOCK_UI=true yarn dev +``` + +Every page works and says on the page that the data is not real. `?mock=empty`, +`?mock=error` and `?mock=slow` pick which scenario the fixtures play. + +## Before the first run + +`docker`, `kind`, `kubectl`, `helm`, `jq`, `openssl`, `yarn`. Set `OPENAI_API_KEY` if you +want agents that answer; without it everything installs and chats fail at the model call. + +## Starting over + +```sh +kind delete cluster --name kagent +docker rm -f $(docker ps -aq) +docker volume prune -f +``` + +## Why this is a script and not four commands + +Five things make the obvious path fail, and each fails silently: + +- **`make create-kind-cluster && make helm-install` does not work.** Every substrate + workload mounts secrets that do not exist until the `kubectl-ate` pool commands have + run, so the controller crash-loops while its pod reports `1/1 Ready`. +- **The chart deploys `controller-v2`**, which does not implement agents, models, tool + servers or prompt libraries — those pages read an empty API. The script builds + `core/cmd/controller/main.go` and swaps it in. Note that `make build-controller` + builds v2, the other one. +- **`kubectl-ate` exits 0 slightly before its secret is readable**, so the next step + waits for the secret rather than trusting the exit code. +- **The chart installs published images**, so a cluster built without this script runs + somebody else's build of both the controller and the UI, and none of the local + changes are on it — while everything looks installed and healthy. +- **A harness runs the Go ADK, not the Python one.** An actor starts by restoring its + template's golden snapshot, and the Python runtime does not survive that — it comes + back with `Fatal Python error: Illegal instruction` and never serves `/readyz`, so + the harness sits in `ResumeGoldenActor` and every message times out at the router + with a 504. A static Go binary restores cleanly. The chart names `golang-adk` as the + image for declarative agents, and the script builds that. + +## When a page looks wrong + +One command distinguishes a broken transport from an empty list, which look identical: + +```sh +printf '\x00\x00\x00\x00\x00' > /tmp/gw.bin +curl -s -D - -X POST -H 'Content-Type: application/grpc-web+proto' -H 'X-Grpc-Web: 1' \ + --data-binary @/tmp/gw.bin \ + http://127.0.0.1:8083/api/kagent.api.v1alpha1.SystemService/GetVersion | head +``` + +`grpc-status: 0` with a framed body means the whole path is up. An empty `200` means no +gRPC-Web handler is mounted; `status 12` means that service is not registered in the +binary you are running. + +Restarting the controller kills the port-forward and the UI reports it as a failed read, +so start it again before concluding anything is broken. diff --git a/ui/dev-scripts/setup-cluster.sh b/ui/dev-scripts/setup-cluster.sh new file mode 100755 index 000000000..8967e0c10 --- /dev/null +++ b/ui/dev-scripts/setup-cluster.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# Stand up a kagent dev cluster from nothing, on this machine (arm64). +# +# Follows ui/HANDOFF.md's "Standing this up on a fresh cluster" plus the two pieces it +# defers to .github/workflows/ci.yaml for. The order matters: every substrate workload +# mounts secrets that do not exist until the kubectl-ate commands have run. +set -euo pipefail + +# The repo this script lives in, so it works from any checkout and any directory. +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SUBSTRATE_VERSION=0.0.20 +cd "$REPO" + +step() { printf '\n\033[1;36m==> %s\033[0m\n' "$1"; } + +step "1/10 Kind cluster and local registry on :5001" +make create-kind-cluster + +step "2/10 kubectl-ate, the tool that mints the CA and JWT pools" +# This one runs on *this* machine rather than in the cluster, so it follows the host OS. +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +HOSTARCH="$(uname -m)"; [ "$HOSTARCH" = "x86_64" ] && HOSTARCH=amd64; [ "$HOSTARCH" = "aarch64" ] && HOSTARCH=arm64 +if [ ! -x /tmp/kubectl-ate ]; then + curl -fsSL -o /tmp/kubectl-ate \ + "https://github.com/kagent-dev/substrate/releases/download/v${SUBSTRATE_VERSION}/kubectl-ate-${OS}-${HOSTARCH}" + chmod +x /tmp/kubectl-ate +fi +ATE=/tmp/kubectl-ate + +step "3/10 Substrate CRDs and substrate" +helm upgrade --install substrate-crds \ + "oci://ghcr.io/kagent-dev/substrate/helm/substrate-crds" --version "$SUBSTRATE_VERSION" \ + --namespace ate-system --create-namespace +helm upgrade --install substrate \ + "oci://ghcr.io/kagent-dev/substrate/helm/substrate" --version "$SUBSTRATE_VERSION" \ + --namespace ate-system \ + --set-string 'atelet.extraArgs[0]=--localhost-registry-replacement=kind-registry:5000' + +step "4/10 CA and JWT pools" +kubectl create namespace podcertificate-controller-system --dry-run=client -o yaml | kubectl apply -f - +$ATE --context kind-kagent admin make-ca-pool --ca-id=1 --name=service-dns-ca-pool --secret-namespace=podcertificate-controller-system +$ATE --context kind-kagent admin make-ca-pool --ca-id=1 --name=pod-identity-ca-pool --secret-namespace=podcertificate-controller-system +$ATE --context kind-kagent admin make-jwt-pool --key-id=1 --name=actor-id-jwt-pool --secret-namespace=ate-system +$ATE --context kind-kagent admin make-ca-pool --ca-id=1 --name=actor-id-ca-pool --secret-namespace=ate-system + +# kubectl-ate prints "Successfully created" and exits 0 slightly BEFORE the secret is +# readable, so wait on the secret rather than trusting the exit code (HANDOFF trap). +for i in $(seq 1 60); do + kubectl get secret actor-id-ca-pool -n ate-system >/dev/null 2>&1 && break + sleep 2 +done + +step "5/10 Actor identity CA cert and the API authentication ConfigMap" +actor_id_ca_root="$(kubectl get secret actor-id-ca-pool -n ate-system -o jsonpath='{.data.pool}' \ + | base64 --decode | jq -r '.CAs[0].RootCertificateDER' | base64 --decode \ + | openssl x509 -inform der -outform pem)" +kubectl create secret generic actor-id-ca-certs -n ate-system \ + --from-literal=ca.crt="${actor_id_ca_root}" --dry-run=client -o yaml | kubectl apply -f - +kubectl create configmap ate-api-authentication -n ate-system \ + --from-literal=authentication.yaml=$'actorIdentityJWTProvider: kubernetes\njwtProviders:\n- name: kubernetes\n issuer: https://kubernetes.default.svc\n audiences: [api.ate-system.svc]\n certificateAuthorityFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt\n discoveryTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token\n' \ + --dry-run=client -o yaml | kubectl apply -f - + +helm upgrade substrate "oci://ghcr.io/kagent-dev/substrate/helm/substrate" \ + --version "$SUBSTRATE_VERSION" --namespace ate-system --reuse-values --wait --timeout 5m + +step "6/10 kagent" +make helm-install KAGENT_HELM_EXTRA_ARGS="\ + --set controller.substrate.enabled=true \ + --set controller.substrate.ateApiEndpoint=dns:///api.ate-system.svc:443 \ + --set controller.substrate.atenetRouterURL=http://atenet-router.ate-system.svc:80 \ + --set controller.substrate.defaultWorkerPool.name=kagent-default \ + --set substrateWorkerPool.create=true \ + --set substrateWorkerPool.replicas=8 \ + --set-string substrateWorkerPool.ateomImage=ghcr.io/kagent-dev/substrate/ateom-gvisor:v${SUBSTRATE_VERSION}" + +step "7/10 The controller and the UI, both built from this checkout" +# The chart installs published images, so without this the cluster would run somebody +# else's build and none of the local changes would be on it. Both are replaced. +# +# The controller is the v1 binary specifically: the chart deploys controller-v2, which +# does not implement agents, models, tool servers or prompt libraries, so every page +# would read an empty API. Note that `make build-controller` builds v2 -- the other one. +# +# Built for this machine's own architecture: the images run on the Kind node, which is +# a container on this host, so a cross-built one would not start. +ARCH="$(uname -m)"; [ "$ARCH" = "x86_64" ] && ARCH=amd64; [ "$ARCH" = "aarch64" ] && ARCH=arm64 +docker buildx build --push --platform "linux/${ARCH}" \ + --build-arg BASE_IMAGE_REGISTRY=cgr.dev \ + --build-arg BUILD_PACKAGE=core/cmd/controller/main.go \ + -t localhost:5001/kagent-dev/kagent/controller:v1full -f go/Dockerfile ./go +kubectl -n kagent set image deploy/kagent-controller controller=localhost:5001/kagent-dev/kagent/controller:v1full + +docker buildx build --push --platform "linux/${ARCH}" \ + -t localhost:5001/kagent-dev/kagent/ui:dev -f ui/Dockerfile ./ui +kubectl -n kagent set image deploy/kagent-ui ui=localhost:5001/kagent-dev/kagent/ui:dev + +kubectl -n kagent rollout status deploy/kagent-controller --timeout=5m +kubectl -n kagent rollout status deploy/kagent-ui --timeout=5m + +step "8/10 The agent runtime image, pinned by digest" +# The Go ADK, which is what the chart names as the image for declarative agents, and +# the one that survives being an actor: an actor starts by restoring the template's +# golden snapshot, and the Python runtime dies on restore with SIGILL where a static +# Go binary comes back. Substrate requires a digest, and only a registry can give one, +# so it is pushed rather than loaded. +docker buildx build --push --platform "linux/${ARCH}" \ + --build-arg BASE_IMAGE_REGISTRY=cgr.dev \ + --build-arg BUILD_PACKAGE=adk/cmd/main.go \ + -t localhost:5001/kagent-dev/kagent/golang-adk:dev -f go/Dockerfile ./go +HARNESS_DIGEST="$(docker buildx imagetools inspect localhost:5001/kagent-dev/kagent/golang-adk:dev \ + | awk '/^Digest:/{print $2; exit}')" + +step "9/10 A harness and an agent template, so the app has an agent in it" +# An agent is a Harness x AgentTemplate pair, so both are needed before anything is +# listed. The harness admits templates by label, and the template carries the label it +# admits -- a template no harness admits is created successfully and then does nothing, +# which is the single most confusing state to arrive in. +kubectl apply -f - </dev/null || true)" + [ "$ready" = "True" ] && break + printf '.'; sleep 15 +done +echo +kubectl get agenttemplate -n kagent assistant \ + -o jsonpath='agent assistant x kagent: Ready={.status.harnesses[0].conditions[?(@.type=="Ready")].status}{"\n"}' + +step "10/10 Done" +kubectl get pods -n kagent + +# The UI the cluster is running, which is the one built above -- not a dev server. +# +# The script ends by holding this open rather than printing one more command to run, +# so the last thing it does is give you a working URL. Ctrl-C ends it; the cluster +# stays up, and `kubectl -n kagent port-forward svc/kagent-ui 8080:8080` brings it +# back. +printf '\n\033[1;32m==> The UI is at http://localhost:8080\033[0m\n' +printf ' (port-forward running in this shell; Ctrl-C to stop)\n\n' +exec kubectl -n kagent port-forward svc/kagent-ui 8080:8080 diff --git a/ui/docs/vendor-extensions.md b/ui/docs/vendor-extensions.md new file mode 100644 index 000000000..493d49f82 --- /dev/null +++ b/ui/docs/vendor-extensions.md @@ -0,0 +1,515 @@ +# Developing a vendor extension + +This UI is built to be extended without being forked into a divergent codebase. +A downstream distribution contributes navigation entries, whole pages, components +at named points inside existing pages, extra form fields, API endpoint overrides +and payload transforms, and app-level React providers — all declared in **one +configuration object**. + +The model is deliberately close to [Backstage](https://backstage.io) plugins: a +plugin is a self-contained module, and installing it costs a small, explicit edit +in the host application rather than magic discovery. + +--- + +## The two-edit install + +1. Build a `VendorExtensionConfig` somewhere under your own directory. +2. Point `src/vendorExtensions/activeConfig.ts` at it. + +That is the whole integration surface. If you maintain a fork, `activeConfig.ts` +is intended to be one of the very few files your fork ever changes. + +```ts +// src/vendorExtensions/activeConfig.ts +import { exampleVendorExtension } from "./example/exampleExtension"; + +export const activeVendorExtensionConfig = exampleVendorExtension; +``` + +Import everything from the `@/vendorExtensions` barrel. Anything below it is +internal and free to move: + +```ts +import { VendorSlot, defineVendorFormField } from "@/vendorExtensions"; +import type { VendorExtensionConfig } from "@/vendorExtensions"; +``` + +### Running the bundled example + +A complete worked example lives in `src/vendorExtensions/example/`. The +application ships with **no** extension installed, so a default build renders +only what this project itself provides. Switch the example on with: + +```bash +VITE_VENDOR_EXTENSIONS=example yarn dev +``` + +Read that directory alongside this document — it exercises every extension point +described here. + +--- + +## The configuration object + +```ts +interface VendorExtensionConfig { + id: string; // stable machine id, e.g. "example" + name: string; // human-readable name + navItems?: readonly VendorNavItemContribution[]; // sidebar entries + routes?: readonly VendorRouteContribution[]; // whole pages + slots?: VendorSlotComponents; // components at named points + formFields?: readonly VendorFormFieldContribution[]; // extra fields in core forms + api?: VendorApiExtension; // endpoint overrides + transforms + providers?: readonly VendorProviderComponent[]; // app-level context providers +} +``` + +Only `id` and `name` are required. A config that contributes a single nav item is +a complete, valid config. + +### One opinion worth stating up front + +**The extension always supplies the whole renderer.** There is no "just give me a +label and a link" shorthand anywhere, and none will be added. Partial +configuration surfaces multiply forever — every consumer eventually needs one +more property, an icon, a badge, a tooltip, a variant — and each one becomes a +compatibility obligation for this project. Requiring a component costs an +extension a few lines once and costs the host nothing thereafter. + +If you want something that looks exactly like a core element, import the core +component in your fork and render it yourself. + +--- + +## Navigation entries + +`order` is the only positioning input. Core items sit at multiples of 100, so +`250` lands **between** Agents (200) and Models (300) — contributions interleave +with the application's own navigation rather than being appended after it. + +```tsx +const exampleNavItem: VendorNavItemContribution = { + key: "exampleInsights", // unique across core and vendor items + order: 250, + path: "/example/insights", // used for active-state matching only + Component: ExampleNavItem, // receives { isActive: boolean } +}; +``` + +Your component renders its own link. `path` is optional and exists only so the +framework can tell you whether you are the active item. + +## Pages + +Contributed routes are merged into the router ahead of the catch-all, so `*` +keeps meaning "not found". A contributed page renders **inside** the app shell by +default, because a vendor page is a page of this application rather than a +separate site. `standalone: true` opts out for full-screen flows such as your own +login. + +```tsx +routes: [ + { path: "/example/insights", element: }, + { path: "/example/onboarding", element: , standalone: true }, +] +``` + +A path that collides with a core route is rejected at startup — see +[Validation](#validation). + +## Component slots + +Every point the application offers is listed in `EXTENSION_POINT_IDS`. IDs are +shaped `app____`, so the name alone says where the +point lives. + +| Extension point ID | Context passed to your component | +| --- | --- | +| `app_shell_appHeader_actions_leading` | none | +| `app_shell_appLayout_contentArea_leadingBanner` | none | +| `app_shell_appLayout_contentArea_globalOverlay` | none | +| `app_shell_appLayout_appSidebar_footer` | none | +| `app_agents_agentsList_pageHeader_actions` | none | +| `app_agents_agentsList_agentListItem_badge` | `{ agentName: string; namespace: string }` | +| `app_agents_agentChat_agentChatMessage_additionalActionsButton` | `{ messageId: string; role: "user" \| "agent"; text: string }` | +| `app_dashboard_dashboardOverview_summaryGrid_leadingCard` | none | + +Mount a component by naming the point: + +```tsx +slots: { + app_shell_appLayout_contentArea_leadingBanner: ExamplePolicyBanner, + app_agents_agentsList_agentListItem_badge: ExampleAgentBadge, // gets agentName + namespace +} +``` + +The ID union is derived from the runtime list, so a point cannot exist in the +type system without also existing at runtime. **A typo is a compile error**, and +`tsc` will suggest the correct ID. + +### Render modes + +Each point declares how it reaches the DOM, in `EXTENSION_POINT_RENDER_MODE`: + +- **`inline`** — rendered where the slot sits. Correct whenever the contribution + belongs in the surrounding layout flow. This is nearly everything. +- **`portal`** — rendered into `document.body`. Used only where a contribution + must escape its parent's DOM position. Today that is + `app_shell_appLayout_contentArea_globalOverlay`: the content area is an + `overflow: auto` scroll container with its own stacking context, so a floating + overlay declared inside it would be clipped by the scroll box and trapped + beneath sibling chrome. + +Prefer `inline`. Reach for `portal` only when clipping or stacking genuinely +requires it. + +## Form fields + +A contributed field declares its own component **and** how its value maps into +and out of the request payload — necessary because a downstream API rarely uses +the same shape as the reference one. + +Target forms are listed in `VENDOR_FORM_IDS`: +`app_agents_agentNew_agentForm`, `app_models_modelNew_modelForm`, +`app_mcpServers_mcpServerNew_mcpServerForm`. + +```ts +export const exampleComplianceTierField = defineVendorFormField({ + id: "exampleComplianceTier", + formId: "app_agents_agentNew_agentForm", + Component: ExampleComplianceTierField, + fromPayload: (payload) => payload.metadata?.labels?.["example.tier"] ?? "standard", + toPayload: (payload, value) => ({ + ...payload, + metadata: { + ...payload.metadata, + labels: { ...payload.metadata?.labels, "example.tier": value }, + }, + }), + validate: (value) => (value ? undefined : "Pick a compliance tier"), +}); +``` + +`fromPayload` seeds the field when editing; `toPayload` writes it wherever your +API expects it; `validate` returns a message or `undefined`. + +## Table columns + +A slot cannot add a column. A slot occupies a position in the DOM, whereas a +column is a heading, a per-row renderer and a place in an ordering — three things +that must be declared together for a table to lay out at all. + +This is how a product whose domain is wider than this application's shows that +extra dimension on a page the application still owns. Nothing replaces the page. + +```ts +export const clusterColumn = defineVendorTableColumn({ + id: "cluster", + tableId: "app_agents_agentsList_table", + title: "Cluster", + after: "namespace", // positioned after that core column's key + render: (row) => row.agent.metadata.labels?.["cluster"] ?? "—", +}); +``` + +Target tables are listed in `VENDOR_TABLE_IDS`. `after` naming a column the table +does not have puts the contribution at the end rather than dropping it, so a core +table can lose a column without an extension's disappearing with it. + +Pages fold contributions in with `withVendorColumns`, so adding one needs no +change to the page. + +## API overrides and transforms + +Keyed by the data layer's own endpoint IDs, so naming a call that does not exist +fails to compile. + +```ts +api: { + baseUrl: "https://control-plane.example.com/api", // optional: replace the API root + endpoints: { "agents.list": "/managed-agents" }, // optional: per-endpoint path + transforms: { + "agents.list": { + request: (context) => ({ + ...context, + headers: { ...context.headers, "x-example-tenant": currentTenant() }, + }), + response: (body) => unwrapExampleEnvelope(body), + }, + }, +} +``` + +`request` runs after the URL resolves and before the call is sent; `response` +runs on the parsed body before it reaches the caller. Both may be async. +`installVendorApiExtension` folds this declarative shape into the data layer's +runtime registry — resolution itself belongs to `src/api`, so there is exactly +one description of a request in the codebase. + +### Something every request needs + +A control plane usually demands something of *all* its traffic rather than of one +endpoint — an authorization header, a tenant, a correlation id. The per-endpoint +table is the wrong shape for that: it means an entry per endpoint, and the endpoint +somebody adds next week goes out without it. + +```ts +api: { + baseUrl: "https://control-plane.example.com/api", + request: (context) => ({ + ...context, + headers: { ...context.headers, authorization: `Bearer ${token()}` }, + }), +} +``` + +This hook runs **last** — after `baseUrl` and after any per-endpoint transform — +so `context.url` is the URL the request will actually be sent to. That ordering is +the point rather than an accident: a hook attaching a credential needs to be able +to tell a call bound for the vendor's own control plane from one going anywhere +else, and it can only do that if it sees the final destination. + +It cannot call hooks — it runs per request, outside React. Anything it needs from +application state has to be reachable without one: a module-level value, storage, +or something a provider published on its way past. + +## Restyling the application + +A slot changes what is inside it. A product with its own design language needs +the *application's* components to look different too — its buttons, tables, +inputs and headings, none of which the extension owns. + +Overriding design tokens is what achieves that. Every component in this project +reads its colours, radii and fonts from the tokens, so replacing values restyles +components the extension never touches. + +```ts +theme: { + tokens: { + color: { primary: "#0084c0", primaryHover: "#006ba6" }, + radius: { sm: 2, md: 4, lg: 6 }, + font: { body: "'Open Sans', sans-serif" }, + }, + // The component library's own internals, where a token cannot reach. + antd: { components: { Button: { controlHeight: 36 } } }, + // Anything neither reaches — gradient borders, scrollbars, resets. Applied + // after the application's own global styles, so it wins on ties. + globalStyles: css` + [data-testid="app-header"] { + border-bottom: 1px solid transparent; + border-image: linear-gradient(90deg, #0084c0, #79d4f8) 1; + } + `, + // Fetched before the first render; a font arriving later reflows the page. + stylesheets: ["https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700"], +} +``` + +Token **names** are fixed and a typo is a compile error; token **values** are +not, so any colour or radius is accepted. The spacing scale is deliberately not +overridable — it is a function every component calls, and replacing it would +make layout unpredictable in ways no reviewer could anticipate. + +### If the extension renders components from its own library + +A component library outside this project reads the Emotion theme in whatever +shape *it* was built against, which is unlikely to be the shape above. This +project nests its tokens (`theme.color.bg`); a library may expect flat keys +(`theme.background`). Nothing warns you: a library shipping no Emotion module +declaration leaves `Theme` widened only by this project, so TypeScript accepts +`theme.background`, and at runtime every colour resolves to `undefined` — the +components render, unstyled or invisibly low-contrast, with no error anywhere. + +Supply both shapes by nesting a provider around the library's components rather +than replacing the outer theme: + +```tsx + ({ ...outer, ...myLibraryTheme })}> +``` + +The extension's own components then still see `theme.color.*`, and the library +sees the keys it expects. Verify it by reading a computed colour off a rendered +library component — not by checking that it mounted, which it will either way. + +## Replacing a region of the shell + +Contributing a nav item is enough when a product wants its pages listed +alongside the application's. It is not enough when the navigation is a different +*shape* — grouped sections, a collapse control, a logo, a footer — because those +are properties of the sidebar itself and no number of items adds them. + +```tsx +shell: { + Sidebar: MySidebar, // receives { coreNavItems, vendorNavItems } + Header: MyHeader, + Layout: MyLayout, // replaces the whole shell; takes precedence over both +} +``` + +`Layout` is for when the shell's *arrangement* differs rather than its regions. +Swapping the sidebar can only produce a variation on this application's +arrangement — header above, sidebar beside. A product whose logo lives in the +sidebar and which has no top bar needs a different arrangement, so it replaces +the layout. **A replacement layout must render React Router's ``**, or +no page appears at all. + +## Changing the application's own navigation + +Contributing an entry covers "this product has a page the application does not". +This covers the other half: a product that lists the *same* pages differently, or +that supplies its own version of a destination. + +```ts +navOverrides: { + dashboard: { path: "/overview" }, // send a familiar entry somewhere else + substrate: { hidden: true }, // unlist it — the route still resolves + mcpServers: { label: "Tool Servers", order: 250 }, +} +``` + +Keys are the application's own nav keys and are type-checked. `hidden` only +unlists an entry — the route still resolves, so a typed URL never 404s because of +a navigation choice. + +## Replacing one of the application's routes + +A path the application already claims is rejected by default: an accidental +collision should always be an error. A contribution that **declares what it +replaces** is allowed to take that path. + +```tsx +routes: [ + { path: "/", element: , replaces: "dashboard" }, +] +``` + +The named route is dropped and the contribution serves the path instead. Naming +the route rather than matching its path means the replacement survives a path +change, and that a genuine collision is still caught. + +Reach for this only when the destination **belongs to the product** rather than +being a variant of the application's page. Replacing a page the application +maintains means its improvements stop arriving — which is the fork problem this +framework exists to avoid. Adding a point to the page is almost always better. + +## Branding + +Identity is not styling, and it should not cost a layout replacement — a product +happy with this application's chrome may still want its own mark on it. + +```tsx +branding: { + AppIcon: MyMark, // receives { collapsed }; supplied whole, like everything else + appName: "My Product", // used for the document title +} +``` + +A replacement owns the region completely, **including rendering the +application's own navigation** — which is why it is handed `coreNavItems` rather +than keeping a copy that drifts as pages are added. + +## App-level providers + +Wrapped around the application, outermost first — your query client, feature +flags, telemetry, tenant context: + +```ts +providers: [ExampleTenantProvider, ExampleTelemetryProvider], +``` + +They sit inside the app's own theming, so they can read it, and outside the +router, so they survive navigation. + +--- + +## Adding a new extension point + +Points are added by this project, not by extensions — an extension cannot invent +one, because the ID union is the contract. + +1. Add the ID to `EXTENSION_POINT_IDS` in `src/vendorExtensions/extensionPoints.ts`. +2. Add an entry to `EXTENSION_POINT_RENDER_MODE` (`inline` unless clipping demands + `portal`). +3. If the point passes context, add its shape to `ExtensionPointPropsMap`. +4. Mount it where it belongs: ``, plus `context={{ … }}` + for a context-carrying point. + +```tsx +import { VendorSlot } from "@/vendorExtensions"; + +// contextless — the id alone + + +// per-item context — required, and shape-checked + +``` + +`context` is *conditionally* required: mandatory for points that declare a +contract, and not accepted for points that don't. Omitting it, misspelling the +ID, or passing an unexpected key are all compile errors rather than runtime +surprises. + +Changing a point's context shape is a one-line edit to the props map; the +compiler then lists every site that needs updating. + +### Slots are safe to leave mounted + +A slot with nothing configured renders `null` — no component, no wrapper element, +no whitespace. It is also safe with no provider above it, because the context +defaults to the empty config rather than throwing, so page unit tests need no +setup. Mount points permanently and let configuration decide. + +--- + +## Validation + +The config is validated at startup and fails loudly rather than degrading +quietly. `VendorExtensionConfigError` is raised for: + +- a slot naming an unknown extension point +- a form field targeting an unknown form ID +- a nav item key declared twice +- a contributed route colliding with a core route, or declared twice + +Every problem is collected before throwing, so one boot reports the whole list +instead of making you fix them one restart at a time. + +A silent no-op is the worst outcome here: an extension author cannot tell the +difference between "my component is not mounted" and "I named the point wrong." +The slot check is the load-bearing one — TypeScript already rejects an unknown +point in typed config, but a config deserialised from JSON has no compiler in +front of it. + +--- + +## Testing an extension + +- **Both states.** Verify your application with the extension installed *and* + with nothing installed. The uninstalled path is what a default build ships, and + it is easy to break by accident. +- **Assert the mechanism, not the copy.** `VendorSlot` emits a + `vendor-slot-` test id, so a test can assert that a component mounted at a + point without depending on what it renders. +- **Watch out for identical contributions.** If a per-item contribution renders + the same text for every row, a text assertion passes even when the per-item + context is broken. Assert something that actually varies per item. +- **Fail on console errors.** A component can satisfy every assertion while + throwing in an effect. The e2e suite's shared fixture fails any test where the + application logged an error; keep that habit. + +--- + +## Deliberate limitations + +- **One config object, not a list.** Merging several independent extensions is + not supported. Ordering conflicts, duplicate keys, and competing API + transforms all need a resolution policy that nothing yet requires. +- **No partial configuration.** Discussed [above](#one-opinion-worth-stating-up-front). +- **Points exist only where the application declares them.** If you need one that + isn't there, add it upstream — that keeps the set of extension points a + reviewed, documented surface rather than an accident of what happened to be + reachable. diff --git a/ui/eslint.config.mjs b/ui/eslint.config.mjs index 471e88f7a..6bf014059 100644 --- a/ui/eslint.config.mjs +++ b/ui/eslint.config.mjs @@ -1,37 +1,41 @@ -// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format -import storybook from "eslint-plugin-storybook"; +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, +export default tseslint.config( { - files: ["**/*.ts", "**/*.tsx"], - rules: { - "@typescript-eslint/no-unused-vars": ["warn", { varsIgnorePattern: "^_", argsIgnorePattern: "^_" }], - }, + ignores: [ + "dist", + "node_modules", + "playwright-report", + "test-results", + // Generated by `msw init`; not ours to lint. + "public/mockServiceWorker.js", + // Generated by `make proto-generate` (buf) and checked by `make proto-check`. + // Never hand-edited, so a lint finding here has nowhere to be fixed. + "src/generated", + ], }, { - files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"], + files: ["**/*.{ts,tsx}"], + extends: [js.configs.recommended, ...tseslint.configs.recommended], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "react-hooks/rules-of-hooks": "off", - "react-hooks/exhaustive-deps": "off", - "no-console": "off", + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-namespace": "off", }, }, - globalIgnores([ - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - "storybook-static/**", - ]), - ...storybook.configs["flat/recommended"], -]); - -export default eslintConfig; +); diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 000000000..81b27b97e --- /dev/null +++ b/ui/index.html @@ -0,0 +1,47 @@ + + + + + + kagent + + + + +
+ + + + + diff --git a/ui/jest.config.ts b/ui/jest.config.ts deleted file mode 100644 index 903ad1d66..000000000 --- a/ui/jest.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Config } from 'jest'; -import nextJest from 'next/jest.js'; - -const createJestConfig = nextJest({ - // Provide the path to your Next.js app to load next.config.js and .env files in your test environment - dir: './', -}); - -// Add any custom config to be passed to Jest -const config: Config = { - setupFilesAfterEnv: ['/jest.setup.ts'], - testEnvironment: 'jest-environment-jsdom', - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - testMatch: [ - '/src/**/*.test.ts', - '/src/**/*.test.tsx', - ], - collectCoverageFrom: [ - 'src/**/*.{js,jsx,ts,tsx}', - '!src/**/*.d.ts', - '!src/**/*.stories.{js,jsx,ts,tsx}', - '!src/**/*.test.{js,jsx,ts,tsx}', - ], - // Transform ESM modules that Jest can't handle by default - transformIgnorePatterns: [ - '/node_modules/(?!(uuid|@a2a-js|jose)/)', - ], -}; - -// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async -export default createJestConfig(config); \ No newline at end of file diff --git a/ui/jest.setup.ts b/ui/jest.setup.ts deleted file mode 100644 index 06be639a9..000000000 --- a/ui/jest.setup.ts +++ /dev/null @@ -1,84 +0,0 @@ -import '@testing-library/jest-dom'; -import { TextEncoder, TextDecoder } from 'util'; - -// Mock uuid module (ESM-only, needs mocking for Jest) -jest.mock('uuid', () => ({ - v4: jest.fn(() => 'test-uuid-v4'), -})); - -// @a2a-js/sdk's CJS bundle requires `jose` at module-load time. -// UI tests do not exercise the signature helpers, so a stub is enough. -jest.mock('jose', () => ({})); - -// Polyfill TextEncoder/TextDecoder for Node.js test environment -global.TextEncoder = TextEncoder; -global.TextDecoder = TextDecoder as typeof global.TextDecoder; - -// Polyfill Request/Response for Next.js server actions -if (typeof Request === 'undefined') { - global.Request = class Request {} as unknown as typeof Request; -} -if (typeof Response === 'undefined') { - global.Response = class Response {} as unknown as typeof Response; -} -if (typeof Headers === 'undefined') { - global.Headers = class Headers {} as unknown as typeof Headers; -} - -// cmdk / Radix use ResizeObserver -global.ResizeObserver = class ResizeObserver { - observe(): void {} - unobserve(): void {} - disconnect(): void {} -} as unknown as typeof ResizeObserver; - -// jsdom: cmdk scrolls selected items into view -Element.prototype.scrollIntoView = function scrollIntoView() {}; - -// jsdom: SidebarProvider / useIsMobile -Object.defineProperty(window, "matchMedia", { - writable: true, - configurable: true, - value: jest.fn().mockImplementation((query: string) => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), -}); - -// Mock next/router -jest.mock('next/router', () => ({ - useRouter() { - return { - route: '/', - pathname: '', - query: {}, - asPath: '', - push: jest.fn(), - replace: jest.fn(), - }; - }, -})); - -// Mock next/navigation -jest.mock('next/navigation', () => ({ - useRouter() { - return { - push: jest.fn(), - replace: jest.fn(), - refresh: jest.fn(), - back: jest.fn(), - }; - }, - usePathname() { - return ''; - }, - useSearchParams() { - return new URLSearchParams(); - }, -})); \ No newline at end of file diff --git a/ui/next.config.ts b/ui/next.config.ts deleted file mode 100644 index cc63e385f..000000000 --- a/ui/next.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { NextConfig } from "next"; - -const controllerDevURL = - process.env.KAGENT_DEV_CONTROLLER_URL ?? "http://127.0.0.1:8083"; - -const nextConfig: NextConfig = { - output: "standalone", - // Proxy /api to the controller in local dev (next dev :8001 → controller :8083). - async rewrites() { - if (process.env.NODE_ENV === "production") { - return []; - } - return [ - { - source: "/api/:path*", - destination: `${controllerDevURL}/api/:path*`, - }, - ]; - }, - logging: { - fetches: { - fullUrl: true, - }, - }, - experimental: { swcPlugins: [] }, - reactCompiler: true, - compiler: { removeConsole: process.env.NODE_ENV === "production" }, -}; - -export default nextConfig; diff --git a/ui/package-lock.json b/ui/package-lock.json deleted file mode 100644 index 280ef982d..000000000 --- a/ui/package-lock.json +++ /dev/null @@ -1,19241 +0,0 @@ -{ - "name": "kagents-ui", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "kagents-ui", - "version": "0.1.0", - "dependencies": { - "@a2a-js/sdk": "^1.0.1", - "@bufbuild/protobuf": "2.14.0", - "@connectrpc/connect": "2.1.2", - "@connectrpc/connect-node": "2.1.2", - "@hookform/resolvers": "^5.9.1", - "@mcp-ui/client": "^7.1.1", - "@modelcontextprotocol/ext-apps": "^1.7.5", - "@modelcontextprotocol/sdk": "^1.30.0", - "@radix-ui/react-accordion": "^1.2.20", - "@radix-ui/react-alert-dialog": "^1.1.23", - "@radix-ui/react-checkbox": "^1.3.11", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.24", - "@radix-ui/react-label": "^2.1.15", - "@radix-ui/react-popover": "^1.1.23", - "@radix-ui/react-progress": "^1.1.16", - "@radix-ui/react-radio-group": "^1.4.7", - "@radix-ui/react-scroll-area": "^1.2.18", - "@radix-ui/react-select": "^2.3.7", - "@radix-ui/react-separator": "^1.1.15", - "@radix-ui/react-slot": "^1.3.3", - "@radix-ui/react-switch": "^1.3.7", - "@radix-ui/react-tabs": "^1.1.21", - "@radix-ui/react-tooltip": "^1.2.16", - "@tailwindcss/typography": "^0.5.20", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^4.4.0", - "jose": "^6.2.9", - "lucide-react": "^0.577.0", - "next": "^16.3.1", - "next-themes": "^0.4.6", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "react-hook-form": "^7.85.0", - "react-markdown": "^10.1.0", - "rehype-external-links": "^3.0.0", - "remark-gfm": "^4.0.1", - "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", - "tailwindcss-animate": "^1.0.7", - "uuid": "^14.0.2", - "zod": "^4.4.3", - "zustand": "^5.0.15" - }, - "devDependencies": { - "@chromatic-com/storybook": "^5.3.0", - "@eslint/eslintrc": "^3.3.6", - "@jest/globals": "^30.4.1", - "@playwright/test": "1.62.1", - "@storybook/addon-a11y": "^10.5.10", - "@storybook/addon-docs": "^10.5.10", - "@storybook/addon-onboarding": "^10.5.10", - "@storybook/addon-vitest": "^10.5.10", - "@storybook/nextjs-vite": "^10.5.10", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.5", - "@types/jest": "^30.0.0", - "@types/node": "25.9.5", - "@types/react": "19.2.18", - "@types/react-beautiful-dnd": "^13.1.8", - "@types/react-dom": "^19.2.4", - "@types/ws": "^8.18.1", - "@vitest/browser-playwright": "^4.1.11", - "@vitest/coverage-v8": "^4.1.2", - "autoprefixer": "^10.5.4", - "babel-plugin-react-compiler": "^1.0.0", - "chromatic": "^18.5.0", - "eslint": "^9.39.5", - "eslint-config-next": "16.3.1", - "eslint-plugin-storybook": "^10.5.10", - "jest": "^30.4.2", - "jest-environment-jsdom": "^30.4.1", - "playwright": "1.62.1", - "postcss": "^8.5.26", - "storybook": "^10.2.10", - "tailwindcss": "^3.4.17", - "ts-jest": "^29.4.12", - "ts-node": "^10.9.2", - "typescript": "5.9.3", - "vite": "^8.2.2", - "vitest": "^4.0.18" - } - }, - "node_modules/@a2a-js/sdk": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-1.0.1.tgz", - "integrity": "sha512-CJQdh3Wzwo8qIx5UUkSJ7+7BEI16PB+MXMHHNSmx8JQsQed2HlQgvx1ENOiKUfYA3PlcEvxIwv14dBblhDuPmw==", - "license": "Apache-2.0", - "dependencies": { - "jose": "^6.2.3", - "uuid": "^11.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@bufbuild/protobuf": "^2.10.2", - "@grpc/grpc-js": "^1.11.0", - "express": "^4.21.2 || ^5.1.0" - }, - "peerDependenciesMeta": { - "@bufbuild/protobuf": { - "optional": true - }, - "@grpc/grpc-js": { - "optional": true - }, - "express": { - "optional": true - } - } - }, - "node_modules/@a2a-js/sdk/node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@blazediff/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@bufbuild/protobuf": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.14.0.tgz", - "integrity": "sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true - }, - "node_modules/@chromatic-com/storybook": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-5.3.0.tgz", - "integrity": "sha512-gMAfVYJnF/tjdY8C3Q54KYz/low2NFjPIZMpCqSFuMUiTJ2DNWCTb7mFhuuzZ1iduWFDhewlJyilFmvKcEDYRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@neoconfetti/react": "^1.0.0", - "chromatic": "^18.0.1", - "jsonfile": "^6.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20.0.0", - "yarn": ">=1.22.18" - }, - "peerDependencies": { - "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0" - } - }, - "node_modules/@chromatic-com/storybook/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@chromatic-com/storybook/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@connectrpc/connect": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.2.tgz", - "integrity": "sha512-MXkBijtcX09R10Eb6sFeIetc6w6746eio6xtfuyVOH7oQAacT1X0GzMIQFux6Qy8cq3W/T5qX5Bei8YbFtmRGA==", - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@bufbuild/protobuf": "^2.7.0" - } - }, - "node_modules/@connectrpc/connect-node": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-2.1.2.tgz", - "integrity": "sha512-+i/aAOpsI8sIx1mbYp6d99zvxaUSF6t/jP9Ux9maAmjsZPgmIQ3JuIeYi0zJIP9zlCnBlJjkpPosshCgdRuThQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@bufbuild/protobuf": "^2.7.0", - "@connectrpc/connect": "2.1.2" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", - "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.3.0", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "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" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@hookform/resolvers": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", - "integrity": "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==", - "license": "MIT", - "dependencies": { - "@standard-schema/utils": "^0.3.0" - }, - "peerDependencies": { - "@sinclair/typebox": ">=0.25.24", - "@standard-schema/spec": "^1.0.0", - "@typeschema/main": ">=0.13.7", - "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", - "ajv": "^8.12.0", - "ajv-errors": "^3.0.0", - "ajv-formats": "^2.1.1", - "arktype": "^2.0.0", - "ata-validator": "^1.2.0", - "class-transformer": ">=0.4.0", - "class-validator": ">=0.12.0", - "computed-types": "^1.0.0", - "effect": "^3.10.3", - "fluentvalidation-ts": "^3.0.0", - "fp-ts": "^2.7.0", - "io-ts": "^2.0.0", - "joi": "^17.0.0 || ^18.0.0", - "nope-validator": ">=0.12.0", - "react-hook-form": "^7.55.0", - "superstruct": ">=0.12.0", - "typanion": "^3.3.2", - "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", - "vest": ">=6.0.0", - "yup": "^1.0.0", - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@sinclair/typebox": { - "optional": true - }, - "@standard-schema/spec": { - "optional": true - }, - "@typeschema/main": { - "optional": true - }, - "@vinejs/vine": { - "optional": true - }, - "ajv": { - "optional": true - }, - "ajv-errors": { - "optional": true - }, - "ajv-formats": { - "optional": true - }, - "arktype": { - "optional": true - }, - "ata-validator": { - "optional": true - }, - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - }, - "computed-types": { - "optional": true - }, - "effect": { - "optional": true - }, - "fluentvalidation-ts": { - "optional": true - }, - "fp-ts": { - "optional": true - }, - "io-ts": { - "optional": true - }, - "joi": { - "optional": true - }, - "nope-validator": { - "optional": true - }, - "superstruct": { - "optional": true - }, - "typanion": { - "optional": true - }, - "valibot": { - "optional": true - }, - "vest": { - "optional": true - }, - "yup": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.1" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", - "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", - "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/pattern": "30.4.0", - "@jest/reporters": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.4.1", - "jest-config": "30.4.2", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-resolve-dependencies": "30.4.2", - "jest-runner": "30.4.2", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "jest-watcher": "30.4.1", - "pretty-format": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", - "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/jsdom": "^21.1.7", - "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/@jest/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.4.1", - "jest-snapshot": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", - "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@sinonjs/fake-timers": "^15.4.0", - "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", - "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/types": "30.4.1", - "jest-mock": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", - "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", - "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", - "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/types": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", - "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", - "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", - "integrity": "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^13.0.1", - "react-docgen-typescript": "^2.2.2" - }, - "peerDependencies": { - "typescript": ">= 4.3.x", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mcp-ui/client": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-7.1.1.tgz", - "integrity": "sha512-Yy0q3YFl6WmcHRW0pRwD2F+Fs9Y/TFm1xpBpkuqvS1IRBaGGgc7PvkB0nvrKTqO6QZm+MufObfQM+oxo8mmLFw==", - "license": "Apache-2.0", - "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.2.0", - "@modelcontextprotocol/sdk": "^1.27.1", - "zod": "^3.23.8" - }, - "peerDependencies": { - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - } - }, - "node_modules/@mcp-ui/client/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "dev": true, - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz", - "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==", - "license": "MIT", - "workspaces": [ - "examples/*" - ], - "dependencies": { - "@standard-schema/spec": "^1.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "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.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "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.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@neoconfetti/react": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@neoconfetti/react/-/react-1.0.0.tgz", - "integrity": "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==", - "dev": true - }, - "node_modules/@next/env": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.1.tgz", - "integrity": "sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.1.tgz", - "integrity": "sha512-B4SznlXwVpaLDa7Tbi6zLuueria2d/PmFDhXyDymPGrk2r1n/RMJmcn5FZq1L64k+Jsyte1lKvOr7lP9Xo80mQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "4.9.1", - "fast-glob": "3.3.1" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.1.tgz", - "integrity": "sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.1.tgz", - "integrity": "sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.1.tgz", - "integrity": "sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.1.tgz", - "integrity": "sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.1.tgz", - "integrity": "sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.1.tgz", - "integrity": "sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.1.tgz", - "integrity": "sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.1.tgz", - "integrity": "sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@oxc-parser/binding-android-arm-eabi": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz", - "integrity": "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.127.0.tgz", - "integrity": "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.127.0.tgz", - "integrity": "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.127.0.tgz", - "integrity": "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.127.0.tgz", - "integrity": "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.127.0.tgz", - "integrity": "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.127.0.tgz", - "integrity": "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.127.0.tgz", - "integrity": "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.127.0.tgz", - "integrity": "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.127.0.tgz", - "integrity": "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.127.0.tgz", - "integrity": "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-musl": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.127.0.tgz", - "integrity": "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.127.0.tgz", - "integrity": "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.127.0.tgz", - "integrity": "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.127.0.tgz", - "integrity": "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-openharmony-arm64": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.127.0.tgz", - "integrity": "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.127.0.tgz", - "integrity": "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz", - "integrity": "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-ia32-msvc": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.127.0.tgz", - "integrity": "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.127.0.tgz", - "integrity": "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz", - "integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz", - "integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz", - "integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz", - "integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz", - "integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz", - "integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz", - "integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz", - "integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz", - "integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz", - "integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz", - "integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz", - "integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz", - "integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz", - "integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz", - "integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz", - "integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz", - "integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.0", - "@emnapi/runtime": "1.11.0", - "@napi-rs/wasm-runtime": "^1.1.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", - "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" - } - }, - "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz", - "integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz", - "integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@playwright/test": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", - "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", - "devOptional": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "playwright": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/number": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", - "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", - "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", - "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-collapsible": "1.1.20", - "@radix-ui/react-collection": "1.1.15", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-controllable-state": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.23.tgz", - "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-dialog": "1.1.23", - "@radix-ui/react-primitive": "2.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", - "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", - "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-size": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", - "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", - "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", - "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", - "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", - "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-dismissable-layer": "1.1.19", - "@radix-ui/react-focus-guards": "1.1.6", - "@radix-ui/react-focus-scope": "1.1.16", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-layout-effect": "1.1.4", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", - "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", - "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-effect-event": "0.0.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", - "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-menu": "2.1.24", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-controllable-state": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", - "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", - "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", - "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", - "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", - "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-collection": "1.1.15", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.19", - "@radix-ui/react-focus-guards": "1.1.6", - "@radix-ui/react-focus-scope": "1.1.16", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-popper": "1.3.7", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-roving-focus": "1.1.19", - "@radix-ui/react-slot": "1.3.3", - "@radix-ui/react-use-callback-ref": "1.1.4", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", - "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-dismissable-layer": "1.1.19", - "@radix-ui/react-focus-guards": "1.1.6", - "@radix-ui/react-focus-scope": "1.1.16", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-popper": "1.3.7", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3", - "@radix-ui/react-use-controllable-state": "1.2.6", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", - "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.15", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-layout-effect": "1.1.4", - "@radix-ui/react-use-rect": "1.1.4", - "@radix-ui/react-use-size": "1.1.4", - "@radix-ui/rect": "1.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", - "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", - "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", - "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.3.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", - "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-primitive": "2.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.7.tgz", - "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-roving-focus": "1.1.19", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-size": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", - "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-collection": "1.1.15", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-is-hydrated": "0.1.3", - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.18", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", - "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.3", - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", - "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.3", - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-collection": "1.1.15", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.19", - "@radix-ui/react-focus-guards": "1.1.6", - "@radix-ui/react-focus-scope": "1.1.16", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-popper": "1.3.7", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-layout-effect": "1.1.4", - "@radix-ui/react-use-previous": "1.1.4", - "@radix-ui/react-visually-hidden": "1.2.11", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", - "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", - "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.5" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", - "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-size": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", - "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-roving-focus": "1.1.19", - "@radix-ui/react-use-controllable-state": "1.2.6" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", - "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-dismissable-layer": "1.1.19", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-popper": "1.3.7", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-layout-effect": "1.1.4", - "@radix-ui/react-visually-hidden": "1.2.11" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", - "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", - "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-use-effect-event": "0.0.5", - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", - "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", - "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", - "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", - "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", - "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", - "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", - "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.10" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", - "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", - "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", - "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", - "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", - "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", - "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", - "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", - "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", - "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", - "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", - "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", - "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", - "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", - "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", - "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", - "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "devOptional": true, - "license": "MIT", - "peer": true - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT", - "peer": true - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@storybook/addon-a11y": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.10.tgz", - "integrity": "sha512-RpRQV5xUbrl6hCiNrd5FSMIo6pnRZ0VZxWvEW/ASLcreGkKUW5jl2AeLCe5YROE2i80s/dU+6VPzOYKrwWNFbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "axe-core": "^4.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^10.5.10" - } - }, - "node_modules/@storybook/addon-docs": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.10.tgz", - "integrity": "sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.5.10", - "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.5.10", - "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", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.10" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@storybook/addon-onboarding": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-10.5.10.tgz", - "integrity": "sha512-Qm1JyjM1wOabeZJykSuyT8mSyZE0/WdjT5wLjOnelE31nCWCh4DsBOgj8A1QDRt+3Hijq9/AW00olowJtf+eYA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^10.5.10" - } - }, - "node_modules/@storybook/addon-vitest": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.10.tgz", - "integrity": "sha512-JNQ9DSkLfxC8qqytBCej91zBExIZ7z97B410U2zgfQPki4HkI9Ffz97a15f5yhVZ79ppIrZ/ssI+WcyWn0ykXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/icons": "^2.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@vitest/browser": "^3.0.0 || ^4.0.0", - "@vitest/browser-playwright": "^4.0.0", - "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.5.10", - "vitest": "^3.0.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/runner": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@storybook/builder-vite": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.10.tgz", - "integrity": "sha512-O4GgIP0tKLRueom3EmU3OaBUHKjNYj+jkOvmTIkn3PYTiWVkCuHqSKEs4ADvRyaQuLH+peHhFe4JtkNC9KbtrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/csf-plugin": "10.5.10", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^10.5.10", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@storybook/csf-plugin": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz", - "integrity": "sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "unplugin": "^2.3.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "esbuild": "*", - "rollup": "*", - "storybook": "^10.5.10", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "esbuild": { - "optional": true - }, - "rollup": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/@storybook/global": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", - "dev": true - }, - "node_modules/@storybook/icons": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.0.2.tgz", - "integrity": "sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "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" - } - }, - "node_modules/@storybook/nextjs-vite": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/nextjs-vite/-/nextjs-vite-10.5.10.tgz", - "integrity": "sha512-kDzhmW3pXWVI/trOcBu0XG7irieTBBdezL3gtmw6ZVyY7RJWWMlkzbVdXha76eWCM78kseLHafAxu8KgOsCI4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/builder-vite": "10.5.10", - "@storybook/react": "10.5.10", - "@storybook/react-vite": "10.5.10", - "styled-jsx": "5.1.6", - "vite-plugin-storybook-nextjs": "^3.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "next": "^14.1.0 || ^15.0.0 || ^16.0.0", - "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", - "storybook": "^10.5.10", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.10.tgz", - "integrity": "sha512-4MBV5e1SXIMfPynLHzr+Mp0dwGv/FW1bklWAsS4ynBOAbC98W9p/I9vqBnUctsvE3BJkhzHQQyPwMHL5tTcHVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.5.10", - "react-docgen": "^8.0.2", - "react-docgen-typescript": "^2.2.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "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", - "storybook": "^10.5.10", - "typescript": ">= 4.9.x" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-dom-shim": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz", - "integrity": "sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "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", - "storybook": "^10.5.10" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/react-vite": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.10.tgz", - "integrity": "sha512-xOztxefUnqKeuyvcnjspqmlDnER4cExL+liltrpdXLPJVqfFNr9lgM49FyEPajzsUVG9W/vHJWjbaQGGu1UsYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", - "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.5.10", - "@storybook/react": "10.5.10", - "empathic": "^2.0.0", - "magic-string": "^0.30.0", - "react-docgen": "^8.0.2", - "resolve": "^1.22.8", - "tsconfig-paths": "^4.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "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", - "storybook": "^10.5.10", - "typescript": ">= 4.9.x", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-vite/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@storybook/react-vite/node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", - "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "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" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.5", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.5.tgz", - "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true - }, - "node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsdom": { - "version": "21.1.7", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", - "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "dev": true - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", - "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-beautiful-dnd": { - "version": "13.1.8", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz", - "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", - "devOptional": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", - "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vitest/browser": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz", - "integrity": "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.11", - "@vitest/utils": "4.1.11", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.11" - } - }, - "node_modules/@vitest/browser-playwright": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.11.tgz", - "integrity": "sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/browser": "4.1.11", - "@vitest/mocker": "4.1.11", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "4.1.11" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": false - } - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", - "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.11", - "ast-v8-to-istanbul": "^1.0.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.1.11", - "vitest": "4.1.11" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.11", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/utils": "4.1.11", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@webcontainer/env": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", - "integrity": "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==", - "dev": true, - "license": "MIT" - }, - "node_modules/@xterm/addon-fit": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", - "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", - "license": "MIT" - }, - "node_modules/@xterm/xterm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", - "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", - "license": "MIT", - "workspaces": [ - "addons/*" - ] - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/autoprefixer": { - "version": "10.5.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", - "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.6", - "caniuse-lite": "^1.0.30001806", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/babel-jest": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", - "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.4.1", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.4.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", - "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/babel-plugin-react-compiler": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", - "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "devOptional": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.26.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", - "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.4.0", - "babel-preset-current-node-syntax": "^1.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.43", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", - "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", - "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.10.42", - "caniuse-lite": "^1.0.30001803", - "electron-to-chromium": "^1.5.389", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chromatic": { - "version": "18.5.0", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-18.5.0.tgz", - "integrity": "sha512-3oBcGP4V+6SV0qu2NJnutnPYsrxnPpOHhtQAbzxEtIQrDJNzn1wfXtzwkEiJGQm5eANTW0AvL4WxHtz+BTJbYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "bin": { - "chroma": "dist/bin.cjs", - "chromatic": "dist/bin.cjs", - "chromatic-cli": "dist/bin.cjs" - }, - "engines": { - "node": ">=22.0.0" - }, - "peerDependencies": { - "@chromatic-com/cypress": "^0.*.* || ^1.0.0", - "@chromatic-com/playwright": "^0.*.* || ^1.0.0", - "@chromatic-com/vitest": "^0.*.* || ^1.0.0" - }, - "peerDependenciesMeta": { - "@chromatic-com/cypress": { - "optional": true - }, - "@chromatic-com/playwright": { - "optional": true - }, - "@chromatic-com/vitest": { - "optional": true - } - } - }, - "node_modules/chromatic/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-fns": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", - "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.389", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", - "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/empathic": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", - "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", - "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "9.39.5", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-next": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.1.tgz", - "integrity": "sha512-0vtrpwFVHFEkycUgV/DyrG29OS+HSRdah5Yu8YuZoiBMtlAT6NIiWzaLwDkJZxr2kGfx+9LIvfQ7KHAlEs0VsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "16.3.1", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-storybook": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.10.tgz", - "integrity": "sha512-NeOu3axmhNZfRuHRciFH0a/OVgvtAJLRHAwiJjcGkPAcElNmwEPp+pqoiwxytx7kHInz7mVVoBfPwVc8kPmOLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "^8.60.0", - "@typescript-eslint/utils": "^8.60.0" - }, - "peerDependencies": { - "eslint": ">=8" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.65.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "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" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "peer": true, - "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" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hono": { - "version": "4.12.27", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", - "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "dev": true, - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-absolute-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", - "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", - "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jest/core": "30.4.2", - "@jest/types": "30.4.1", - "import-local": "^3.2.0", - "jest-cli": "30.4.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", - "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/jest-changed-files/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/jest-circus": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", - "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0", - "pretty-format": "30.4.1", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", - "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.4.2", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", - "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.4.0", - "@jest/test-sequencer": "30.4.1", - "@jest/types": "30.4.1", - "babel-jest": "30.4.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-circus": "30.4.2", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-runner": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "parse-json": "^5.2.0", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", - "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", - "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "jest-util": "30.4.1", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", - "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/environment-jsdom-abstract": "30.4.1", - "jsdom": "^26.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jest-environment-node": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", - "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", - "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "picomatch": "^4.0.3", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-leak-detector": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", - "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", - "picomatch": "^4.0.3", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", - "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", - "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "30.4.0", - "jest-snapshot": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", - "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/environment": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-haste-map": "30.4.1", - "jest-leak-detector": "30.4.1", - "jest-message-util": "30.4.1", - "jest-resolve": "30.4.1", - "jest-runtime": "30.4.2", - "jest-util": "30.4.1", - "jest-watcher": "30.4.1", - "jest-worker": "30.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", - "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/globals": "30.4.1", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", - "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-diff": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "pretty-format": "30.4.1", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", - "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-watcher": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", - "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.4.1", - "string-length": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", - "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.4.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "peer": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/jose": { - "version": "6.2.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", - "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/module-alias": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.3.4.tgz", - "integrity": "sha512-bOclZt8hkpuGgSSoG07PKmvzTizROilUTvLNyrMqvlC9snhs7y7GzjNWAVbISIOlhCP1T14rH1PDAV9iNyBq/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/next": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.1.tgz", - "integrity": "sha512-hsAp0i7Rh+/dhe7DGIeN2YlpLM1DP4MNxti9EtDMtqcO612X81MvvEj388/oTce9U1EcEIOWDlGq0zRwrBKvuA==", - "license": "MIT", - "dependencies": { - "@next/env": "16.3.1", - "@swc/helpers": "0.5.23", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.5.23", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.1", - "@next/swc-darwin-x64": "16.3.1", - "@next/swc-linux-arm64-gnu": "16.3.1", - "@next/swc-linux-arm64-musl": "16.3.1", - "@next/swc-linux-x64-gnu": "16.3.1", - "@next/swc-linux-x64-musl": "16.3.1", - "@next/swc-win32-arm64-msvc": "16.3.1", - "@next/swc-win32-x64-msvc": "16.3.1", - "sharp": "^0.35.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.5.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", - "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ] - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/oxc-parser": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.127.0.tgz", - "integrity": "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "^0.127.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-parser/binding-android-arm-eabi": "0.127.0", - "@oxc-parser/binding-android-arm64": "0.127.0", - "@oxc-parser/binding-darwin-arm64": "0.127.0", - "@oxc-parser/binding-darwin-x64": "0.127.0", - "@oxc-parser/binding-freebsd-x64": "0.127.0", - "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", - "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", - "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", - "@oxc-parser/binding-linux-arm64-musl": "0.127.0", - "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", - "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", - "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", - "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", - "@oxc-parser/binding-linux-x64-gnu": "0.127.0", - "@oxc-parser/binding-linux-x64-musl": "0.127.0", - "@oxc-parser/binding-openharmony-arm64": "0.127.0", - "@oxc-parser/binding-wasm32-wasi": "0.127.0", - "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", - "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", - "@oxc-parser/binding-win32-x64-msvc": "0.127.0" - } - }, - "node_modules/oxc-resolver": { - "version": "11.21.2", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz", - "integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.21.2", - "@oxc-resolver/binding-android-arm64": "11.21.2", - "@oxc-resolver/binding-darwin-arm64": "11.21.2", - "@oxc-resolver/binding-darwin-x64": "11.21.2", - "@oxc-resolver/binding-freebsd-x64": "11.21.2", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", - "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", - "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", - "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", - "@oxc-resolver/binding-linux-x64-musl": "11.21.2", - "@oxc-resolver/binding-openharmony-arm64": "11.21.2", - "@oxc-resolver/binding-wasm32-wasi": "11.21.2", - "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", - "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.62.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-docgen": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.3.tgz", - "integrity": "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.2", - "@types/babel__core": "^7.20.5", - "@types/babel__traverse": "^7.20.7", - "@types/doctrine": "^0.0.9", - "@types/resolve": "^1.20.2", - "doctrine": "^3.0.0", - "resolve": "^1.22.1", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": "^20.9.0 || >=22" - } - }, - "node_modules/react-docgen-typescript": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", - "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">= 4.3.x" - } - }, - "node_modules/react-docgen/node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/react-docgen/node_modules/strip-indent": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", - "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } - }, - "node_modules/react-hook-form": { - "version": "7.85.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", - "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", - "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-markdown": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", - "dev": true, - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rehype-external-links": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", - "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-is-element": "^3.0.0", - "is-absolute-url": "^4.0.0", - "space-separated-tokens": "^2.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rolldown": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", - "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.146.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.5", - "@rolldown/binding-android-arm64": "1.2.5", - "@rolldown/binding-darwin-arm64": "1.2.5", - "@rolldown/binding-darwin-x64": "1.2.5", - "@rolldown/binding-freebsd-x64": "1.2.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", - "@rolldown/binding-linux-arm64-gnu": "1.2.5", - "@rolldown/binding-linux-arm64-musl": "1.2.5", - "@rolldown/binding-linux-ppc64-gnu": "1.2.5", - "@rolldown/binding-linux-s390x-gnu": "1.2.5", - "@rolldown/binding-linux-x64-gnu": "1.2.5", - "@rolldown/binding-linux-x64-musl": "1.2.5", - "@rolldown/binding-openharmony-arm64": "1.2.5", - "@rolldown/binding-win32-arm64-msvc": "1.2.5", - "@rolldown/binding-win32-x64-msvc": "1.2.5" - } - }, - "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.146.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", - "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.1.0", - "detect-libc": "^2.1.2", - "semver": "^7.8.5" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.3", - "@img/sharp-darwin-x64": "0.35.3", - "@img/sharp-freebsd-wasm32": "0.35.3", - "@img/sharp-libvips-darwin-arm64": "1.3.2", - "@img/sharp-libvips-darwin-x64": "1.3.2", - "@img/sharp-libvips-linux-arm": "1.3.2", - "@img/sharp-libvips-linux-arm64": "1.3.2", - "@img/sharp-libvips-linux-ppc64": "1.3.2", - "@img/sharp-libvips-linux-riscv64": "1.3.2", - "@img/sharp-libvips-linux-s390x": "1.3.2", - "@img/sharp-libvips-linux-x64": "1.3.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", - "@img/sharp-libvips-linuxmusl-x64": "1.3.2", - "@img/sharp-linux-arm": "0.35.3", - "@img/sharp-linux-arm64": "0.35.3", - "@img/sharp-linux-ppc64": "0.35.3", - "@img/sharp-linux-riscv64": "0.35.3", - "@img/sharp-linux-s390x": "0.35.3", - "@img/sharp-linux-x64": "0.35.3", - "@img/sharp-linuxmusl-arm64": "0.35.3", - "@img/sharp-linuxmusl-x64": "0.35.3", - "@img/sharp-webcontainers-wasm32": "0.35.3", - "@img/sharp-win32-arm64": "0.35.3", - "@img/sharp-win32-ia32": "0.35.3", - "@img/sharp-win32-x64": "0.35.3" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sonner": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", - "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", - "dev": true - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/storybook": { - "version": "10.5.10", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.10.tgz", - "integrity": "sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@storybook/global": "^5.0.0", - "@storybook/icons": "^2.0.2", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "6.9.1", - "@testing-library/user-event": "^14.6.1", - "@vitest/expect": "3.2.4", - "@vitest/spy": "3.2.4", - "@webcontainer/env": "^1.1.1", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", - "jsonc-parser": "^3.3.1", - "open": "^10.2.0", - "oxc-parser": "^0.127.0", - "oxc-resolver": "11.21.2", - "recast": "^0.23.5", - "semver": "^7.7.3", - "use-sync-external-store": "^1.5.0", - "ws": "^8.21.1" - }, - "bin": { - "storybook": "dist/bin/dispatcher.js" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "prettier": "^2 || ^3", - "vite-plus": "^0.1.15 || ^0.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "prettier": { - "optional": true - }, - "vite-plus": { - "optional": true - } - } - }, - "node_modules/storybook/node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/storybook/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tailwind-merge": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", - "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tailwindcss/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/tailwindcss/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true, - "engines": { - "node": ">=6.10" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, - "node_modules/ts-jest": { - "version": "29.4.12", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", - "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.9", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.8.5", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <7" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unplugin": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", - "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", - "dev": true, - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "devOptional": true, - "peer": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-storybook-nextjs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-storybook-nextjs/-/vite-plugin-storybook-nextjs-3.3.0.tgz", - "integrity": "sha512-46DqDN/2Jdst9HdnKaJctdkZJAzwatulgiapSOFOmnZhxwnHrrIFo222ylEWmvTi8W8k2xhj8vfuBbqkWgctiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/env": "16.0.0", - "image-size": "^2.0.0", - "magic-string": "^0.30.11", - "module-alias": "^2.2.3", - "ts-dedent": "^2.2.0", - "vite-tsconfig-paths": "^5.1.4" - }, - "peerDependencies": { - "next": "^14.1.0 || ^15.0.0 || ^16.0.0", - "storybook": "^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/vite-plugin-storybook-nextjs/node_modules/@next/env": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.0.tgz", - "integrity": "sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite-tsconfig-paths": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", - "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ws": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/zustand": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", - "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/ui/package.json b/ui/package.json index 381689167..35e6aac31 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,121 +1,68 @@ { - "name": "kagents-ui", - "version": "0.1.0", + "name": "kagent-ui", "private": true, - "packageManager": "npm@11.6.2", + "version": "0.1.0", + "type": "module", + "packageManager": "yarn@4.9.0", + "engines": { + "node": ">=24.13.0" + }, "scripts": { - "dev": "next dev -p 8001 -H 0.0.0.0", - "build": "next build", - "start": "next start", + "dev": "vite", + "build": "yarn typecheck && vite build", + "preview": "vite preview", "lint": "eslint .", - "test": "jest", - "test:vitest": "vitest run", - "test:watch": "jest --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", "test:pw": "playwright test", - "test:e2e": "npm run test:pw", - "test:pw:ui": "playwright test --ui", - "test:pw:headed": "playwright test --headed", - "test:pw:debug": "playwright test --debug", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", - "chromatic": "chromatic --exit-zero-on-changes --storybook-build-dir storybook-static --project-token chpt_3e29f54d624610f" + "test:e2e": "yarn test:pw", + "test:pw:live": "UI_LOOP_LIVE=true playwright test" }, "dependencies": { - "@a2a-js/sdk": "^1.0.1", - "@bufbuild/protobuf": "2.14.0", + "@bufbuild/protobuf": "2.13.0", "@connectrpc/connect": "2.1.2", - "@connectrpc/connect-node": "2.1.2", - "@hookform/resolvers": "^5.9.1", - "@mcp-ui/client": "^7.1.1", - "@modelcontextprotocol/ext-apps": "^1.7.5", - "@modelcontextprotocol/sdk": "^1.30.0", - "@radix-ui/react-accordion": "^1.2.20", - "@radix-ui/react-alert-dialog": "^1.1.23", - "@radix-ui/react-checkbox": "^1.3.11", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.24", - "@radix-ui/react-label": "^2.1.15", - "@radix-ui/react-popover": "^1.1.23", - "@radix-ui/react-progress": "^1.1.16", - "@radix-ui/react-radio-group": "^1.4.7", - "@radix-ui/react-scroll-area": "^1.2.18", - "@radix-ui/react-select": "^2.3.7", - "@radix-ui/react-separator": "^1.1.15", - "@radix-ui/react-slot": "^1.3.3", - "@radix-ui/react-switch": "^1.3.7", - "@radix-ui/react-tabs": "^1.1.21", - "@radix-ui/react-tooltip": "^1.2.16", - "@tailwindcss/typography": "^0.5.20", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", + "@connectrpc/connect-web": "2.1.2", + "@emotion/react": "^11.14.0", + "antd": "^6.5.2", + "chart.js": "^4.5.1", "date-fns": "^4.4.0", - "jose": "^6.2.9", - "lucide-react": "^0.577.0", - "next": "^16.3.1", - "next-themes": "^0.4.6", + "lucide-react": "^1.28.0", "react": "^19.2.8", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.8", - "react-hook-form": "^7.85.0", + "react-hot-toast": "^2.6.0", "react-markdown": "^10.1.0", - "rehype-external-links": "^3.0.0", + "react-router-dom": "^7.18.2", + "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", - "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", - "tailwindcss-animate": "^1.0.7", - "uuid": "^14.0.2", - "zod": "^4.4.3", - "zustand": "^5.0.15" - }, - "overrides": { - "braces": ">=3.0.3", - "micromatch": ">=4.0.8", - "diff": ">=8.0.3", - "tar": ">=7.5.7", - "fast-uri": "^3.1.3", - "qs": ">=6.15.2", - "sharp": "0.35.3" + "swr": "^2.4.2" }, "devDependencies": { - "@chromatic-com/storybook": "^5.3.0", - "@eslint/eslintrc": "^3.3.6", - "@jest/globals": "^30.4.1", - "@playwright/test": "1.62.1", - "@storybook/addon-a11y": "^10.5.10", - "@storybook/addon-docs": "^10.5.10", - "@storybook/addon-onboarding": "^10.5.10", - "@storybook/addon-vitest": "^10.5.10", - "@storybook/nextjs-vite": "^10.5.10", - "@testing-library/jest-dom": "^6.9.1", + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.62.1", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.5", - "@types/jest": "^30.0.0", - "@types/node": "25.9.5", - "@types/react": "19.2.18", - "@types/react-beautiful-dnd": "^13.1.8", - "@types/react-dom": "^19.2.4", - "@types/ws": "^8.18.1", - "@vitest/browser-playwright": "^4.1.11", - "@vitest/coverage-v8": "^4.1.2", - "autoprefixer": "^10.5.4", - "babel-plugin-react-compiler": "^1.0.0", - "chromatic": "^18.5.0", - "eslint": "^9.39.5", - "eslint-config-next": "16.3.1", - "eslint-plugin-storybook": "^10.5.10", - "jest": "^30.4.2", - "jest-environment-jsdom": "^30.4.1", - "playwright": "1.62.1", - "postcss": "^8.5.26", - "storybook": "^10.2.10", - "tailwindcss": "^3.4.17", - "ts-jest": "^29.4.12", - "ts-node": "^10.9.2", - "typescript": "5.9.3", - "vite": "^8.2.2", - "vitest": "^4.0.18" + "@testing-library/user-event": "^14.6.1", + "@types/node": "^26.1.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.5", + "eslint": "^10.8.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.3", + "globals": "^17.8.0", + "jsdom": "^30.0.1", + "msw": "^2.15.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.65.0", + "vite": "^8.2.0", + "vitest": "^4.1.10" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 327aa768d..29563f92a 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -1,102 +1,204 @@ import { defineConfig, devices } from "@playwright/test"; -import { KAGENT_BACKEND_URL } from "./playwright/backend"; /** - * Playwright E2E config for the kagent UI. + * Overridable so concurrent runs can each own a port — but fixed, and + * deliberately not derived from the process: Playwright loads this file in the + * main process and again in every worker, so anything varying per process gives + * each worker a different base URL from the one the servers were started on. + */ +const PORT = Number(process.env.UI_LOOP_PORT ?? 8001); +const BASE_URL = `http://localhost:${PORT}`; + +/** + * A second app, booted with the example vendor extension installed. + * + * Which extension a build ships with is decided at build time + * (`src/vendorExtensions/activeConfig.ts` reads an env var), so "installed" and + * "not installed" cannot be two states of one server — they are two servers. + * That split is also what lets the default suite assert the app is bare, which + * is the shape a build with no extension takes. + */ +// Deliberately not PORT + 1: Vite falls forward to the next free port when the +// one it is told to use is busy, so adjacent ports let a slow-to-die server from +// a previous run push one app onto the other's port. +const VENDOR_PORT = Number(process.env.UI_LOOP_VENDOR_PORT ?? PORT + 50); +const VENDOR_BASE_URL = `http://localhost:${VENDOR_PORT}`; + +/** Specs that need the extension installed opt in by filename. */ +const VENDOR_SPECS = /\.vendor\.spec\.ts$/; + +/** + * The suite is the acceptance bar, so what it runs against cannot depend on the + * shell it was started from: both servers are pinned to the in-browser mock + * backend. An inherited VITE_API_MODE=live would otherwise point a whole run at + * a real cluster. + */ +const MOCK_BACKEND = { VITE_API_MODE: "mock" }; + +/** + * What each of the three servers is pinned to. + * + * Named here, rather than written inline below, so that what a server serves is + * stated in one place — and so a branch that installs an extension changes a + * value instead of restructuring the `projects`/`webServer` blocks. + * + * `VITE_VENDOR_EXTENSIONS` is pinned on the bare server for the same reason + * `VITE_API_MODE` is: an inherited value must not be able to decide what a run + * measures. Left unpinned, the bare project measures whatever the shell happened + * to export. + */ +const BARE_APP = { ...MOCK_BACKEND, VITE_VENDOR_EXTENSIONS: "none" }; +const EXAMPLE_APP = { ...MOCK_BACKEND, VITE_VENDOR_EXTENSIONS: "example" }; + +/** + * The third mode: one app, wired to a real backend. + * + * Selected by an environment variable rather than added as a third project + * alongside the mock two, because the two modes have incompatible requirements + * and each would break the other: * - * Data is fetched server-side (Next.js server actions), so browser-level - * `page.route` cannot intercept `/api/**`. Instead we boot a standalone stub - * backend (playwright/mocks/server.mjs) and point Next at it via - * BACKEND_INTERNAL_URL — `getBackendUrl()` (src/lib/utils.ts) checks that env - * var first. Both servers are started by the `webServer` block below. + * - A live run needs a cluster and a port-forward. Adding it to the default + * project list would make `yarn test:pw` — which is meant to need nothing but a + * machine that can run the dev server — fail on any laptop without a cluster in + * front of it. + * - A live run has no use for the two mock servers, and starting them would cost + * every live run the time to boot two more Vite instances. * - * See playwright/README.md for the full test strategy. + * So `LIVE` swaps the whole `projects`/`webServer` pair rather than appending to + * it. `yarn test:pw` and `yarn test:pw:live` are two disjoint runs. */ +const LIVE = process.env.UI_LOOP_LIVE === "true"; -const CI = !!process.env.CI; +/** + * Its own port, far from the mock servers' 8001/8051, for the same reason those + * two are 50 apart: Vite falls forward to the next free port when the one it is + * told to use is busy, so a live run must not be able to land on a port a mock + * server is about to want, or vice versa. + */ +const LIVE_PORT = Number(process.env.UI_LOOP_LIVE_PORT ?? 8301); +const LIVE_BASE_URL = `http://localhost:${LIVE_PORT}`; -const STUB_PORT = 8899; -const STUB_URL = `http://127.0.0.1:${STUB_PORT}`; -const APP_URL = "http://localhost:8001"; -// KAGENT_BACKEND_URL — origin of the REAL kagent backend the proxy forwards to — -// is defined in playwright/backend.ts alongside the port-forward config in -// playwright/setup.ts, so the proxy target and the port-forward stay in sync. +/** Read by `playwright/globalSetup.ts` to decide what to verify about a server. */ +export const LIVE_PROJECT = "chromium-live"; -// `slowMo` adds an idle delay between every Playwright action (click, fill, -// goto). The recorded videos play at real time, so without slowMo the test -// runs fast enough that a human can't follow what's happening. 250ms feels -// natural in the recording without bloating wall-clock test time too much. -// Coerce + validate the env override so a malformed value (non-numeric → NaN, -// or negative) falls back to the default instead of reaching Playwright. -const DEFAULT_SLOW_MO_MS = 250; -const parsedSlowMo = Number(process.env.E2E_SLOW_MO_MS); -const SLOW_MO_MS = - Number.isFinite(parsedSlowMo) && parsedSlowMo >= 0 - ? parsedSlowMo - : DEFAULT_SLOW_MO_MS; +/** + * A live run reaches the backend through Vite's proxy, exactly as a deployed + * build reaches it through nginx — so the app uses the same relative URLs either + * way and this mode tests the addressing a real deployment uses. + * + * `VITE_API_MODE` is pinned as well as the runtime flag: the build-time pin is + * the one thing an inherited `.env` cannot override, and a live suite that + * silently answered from fixtures would be worse than a red one. + */ +const LIVE_APP = { VITE_API_MODE: "live", ENABLE_MOCK_UI: "false" }; + +/** + * How the live server is started. + * + * Named for the same reason the three env pins above are: a branch whose backend + * needs more than a dev server — a credential minted per run, a port-forward + * probed before Vite starts — replaces this line rather than the block below. + */ +const LIVE_COMMAND = `yarn dev --port ${LIVE_PORT}`; export default defineConfig({ testDir: "./playwright/tests", - outputDir: "./playwright/test-results", - // Port-forward the real controller before the run, tear it down after. - globalSetup: "./playwright/setup.ts", - globalTeardown: "./playwright/teardown.ts", - // Parallelism stays off until Stage 1 per-test data isolation lands: one - // shared stub backend + one Next server means concurrent tests would race - // against shared state (see README). Flip both `fullyParallel` and `workers` - // together when isolation is in place. - fullyParallel: false, - forbidOnly: CI, - retries: CI ? 1 : 0, - workers: 1, - // Real-backend flows do create/list/delete round trips, so allow headroom. - timeout: 60_000, - expect: { timeout: 10_000 }, - reporter: [["html", { open: "never" }], ["list"]], + // Both servers have to be rendering, not merely listening, before any test + // navigates — see the file for what goes wrong otherwise. + globalSetup: "./playwright/globalSetup.ts", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? "github" : "list", + // A real backend behind a port-forward answers in tens of seconds where the + // in-browser mock answers in milliseconds, so the defaults that suit the mock + // suite are too tight to distinguish "slow cluster" from "broken page". + ...(LIVE ? { timeout: 120_000, expect: { timeout: 30_000 } } : {}), use: { - baseURL: APP_URL, + trace: "on-first-retry", screenshot: "only-on-failure", - trace: "retain-on-failure", - video: "retain-on-failure", - launchOptions: { - slowMo: SLOW_MO_MS, - }, }, - projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - webServer: [ - { - command: "node playwright/mocks/server.mjs", - url: `${STUB_URL}/__mock/health`, - reuseExistingServer: !CI, - timeout: 30_000, - stdout: "pipe", - stderr: "pipe", - // Pin the proxy port (health-check / BACKEND_INTERNAL_URL address) and tell - // it where the real backend is (the port-forward from playwright/setup.ts). - env: { STUB_PORT: String(STUB_PORT), KAGENT_BACKEND_URL }, - }, - { - // Force the webpack dev server (the default `npm run dev` uses Turbopack). - // Turbopack's dev server corrupts the RSC client manifest when a route is - // recompiled after a full-page navigation cycle (`evalManifest` throws - // "Invalid or unexpected token"), which surfaces as a Runtime Error overlay - // on the *second* cold load of "/" in a session and breaks every test that - // re-navigates there (onboarding variants, the app-shell error state). The - // bug is dev-only and Turbopack-specific, so we opt this run out of it while - // leaving local `npm run dev` on Turbopack for day-to-day speed. - command: "npm run dev -- --webpack", - url: APP_URL, - // Never reuse an existing dev server: the BACKEND_INTERNAL_URL below is - // only applied to a server Playwright starts. A reused server (e.g. a - // hand-started `npm run dev`) would silently bypass the stub. Always - // boot our own so the redirect is guaranteed; a busy port fails loudly. - reuseExistingServer: false, - timeout: 120_000, - env: { - // Route the UI's server-side backend fetches (and the /a2a route handler) - // through the proxy, which forwards to the real backend and mocks chat. - BACKEND_INTERNAL_URL: `${STUB_URL}/api`, - }, - }, - ], + projects: LIVE + ? [ + { + name: LIVE_PROJECT, + testDir: "./playwright/live", + use: { + ...devices["Desktop Chrome"], + baseURL: LIVE_BASE_URL, + // Worth keeping for a live failure: unlike the mock suite there is + // no fixed fixture to re-read, so the trace is the only record of what + // the cluster actually answered. + trace: "retain-on-failure", + }, + }, + ] + : [ + { + name: "chromium", + testIgnore: VENDOR_SPECS, + use: { ...devices["Desktop Chrome"], baseURL: BASE_URL }, + }, + { + // The same suite in a second engine, against the same server. + // + // Not redundancy: the two disagree about things this app depends on — + // flex and grid sizing, scroll metrics, focus and selection, and how + // streamed responses are delivered. A chat that pins to the bottom and + // a rail that stays put are exactly the kind of thing one engine gets + // right by accident. + // + // The vendor split below is a build-time difference, not a browser one, + // so it stays on one engine rather than doubling for no new signal. + name: "firefox", + testIgnore: VENDOR_SPECS, + use: { ...devices["Desktop Firefox"], baseURL: BASE_URL }, + }, + { + name: "chromium-vendor", + testMatch: VENDOR_SPECS, + use: { ...devices["Desktop Chrome"], baseURL: VENDOR_BASE_URL }, + }, + ], + // Never adopt a server this config did not start. Adopting one skips the `env` + // below, so a dev server left over from an earlier run — or one a developer has + // open — silently serves a build with the wrong extension config, and the + // vendor specs then fail looking for contributions that were never installed. + // That was an intermittent failure whose frequency depended only on whether + // something happened to linger. Refusing to adopt makes an occupied port a + // loud startup error instead; set UI_LOOP_PORT / UI_LOOP_VENDOR_PORT to run + // alongside a dev server you want to keep. + webServer: LIVE + ? [ + { + command: LIVE_COMMAND, + url: LIVE_BASE_URL, + reuseExistingServer: false, + timeout: 120_000, + // Whatever starts the live server is the most useful output a failed + // live run has — something that cannot reach the backend says so there, + // and Playwright discards a web server's stdout unless asked to pass it + // through. + stdout: "pipe", + stderr: "pipe", + env: LIVE_APP, + }, + ] + : [ + { + command: `yarn dev --port ${PORT}`, + url: BASE_URL, + reuseExistingServer: false, + timeout: 120_000, + env: BARE_APP, + }, + { + command: `yarn dev --port ${VENDOR_PORT}`, + url: VENDOR_BASE_URL, + reuseExistingServer: false, + timeout: 120_000, + env: EXAMPLE_APP, + }, + ], }); diff --git a/ui/playwright/DEFERRED.md b/ui/playwright/DEFERRED.md new file mode 100644 index 000000000..c1c1fc5b8 --- /dev/null +++ b/ui/playwright/DEFERRED.md @@ -0,0 +1,363 @@ +# Deferred specs + +The old suite had 13 specs. Seven are ported (`app-shell`, `agents`, +`agents-errors`, `models`, `models-errors`, `chat`, `chat-errors`) plus three new +ones (`routing`, and the two extension-point specs). The rest are listed here +rather than committed as skipped tests, because a skipped or vacuous spec reads +as coverage and this list does not. + +Each entry names the surface that has to exist before the spec can assert +anything real. In every case the data layer is already in place — what is +missing is the page. + +| Old spec | Blocked on | Already available | +|---|---|---| +| `onboarding/onboarding.spec.ts` | No onboarding wizard exists on this architecture | — nothing; drop it unless the flow is rebuilt | +| `cleanup.spec.ts` | Not applicable while the suite runs on the mock backend: each test gets a fresh browser context, so there is nothing to sweep. Revisit if the suite gains a live-backend mode. | — | + +## Ported since: chat + +`chat/chat.spec.ts` and `chat/chat-errors.spec.ts` are live. The chat page was +rebuilt on the `ChatClient` port, so both journeys assert against the real page: +history, sending, streaming deltas, tool call and result rendering, a failed +turn with retry, cancelling mid-stream, and the session list failing on its own. + +The chat-message extension point is covered too, now that the example mounts a +component there: `extension-points.vendor.spec.ts` asserts one slot per message +and four *distinguishable* contributions, so per-message context is proven rather +than assumed. Every extension point the app declares now has a runtime +assertion. + +## Also deferred: client-side form validation + +**This coverage existed in the old suite and has not been replaced.** The old +`agents-errors.spec.ts` and `models-errors.spec.ts` asserted that create forms +block submission and show field errors. Those spec *filenames* are now in use for +a different journey — a failed list load — so it would be easy to look at the +suite and conclude validation is covered. It is not. + +What the old specs asserted, and what each needs before it can come back: + +| Assertion | Needs | +|---|---| +| Declarative agent create blocks submit; "Description is required" and "Please select a model" appear; URL stays on the form | `/agents/new` with a real form | +| Agent harness create blocks submit when required fields are empty | an agent-harness create route, which this architecture does not have yet | +| Model create blocks submit with no model selected; "Provider and Model selection is required" appears | `/models/new` with provider and model pickers | + +When those forms land, the validation journeys should come back as their own +specs — `agents-validation.spec.ts` and `models-validation.spec.ts` — rather than +displacing the load-failure journeys, which are worth keeping. + +## Also deferred: the lifecycle half of the ported specs + +The old `agents.spec.ts` and `models.spec.ts` were create → read → update → +delete journeys. Only the read half is ported here. The forms and the per-row +controls do now exist — what is missing is not the product but the point of +testing them against fixtures: a create that posts to a mock proves the mock. +The write half runs against a real cluster instead, in +`live/write/agents-create.spec.ts` and `live/write/models-create.spec.ts`. + +Restoring them needs: + +- **agents** — the create form (name, description, namespace, model picker), + per-row Edit and Delete actions, and the delete confirmation. +- **models** — the create form (provider and model comboboxes, API key field, + name override), per-row Edit and Delete actions. + +Both mutation paths already exist on the API client (`apiClient.models.create`, +`.remove`, and so on) and the mock backend answers them, so the specs should be +able to assert against a real round trip once the forms land. + +## Not started by request + +Vendor extension-point specs. The framework is still being edited and its +contract is not frozen; the team lead will ask for these once it lands. + +## Ported since: MCP servers and prompt libraries + +`mcp-servers/mcp-servers.spec.ts`, `mcp-servers/mcp-servers-errors.spec.ts`, +`prompts/prompt-libraries.spec.ts` and `prompts/prompt-libraries-errors.spec.ts` are +live. + +**These were listed above as blocked on pages that did not exist. The pages did +exist** — `McpServersPage`, `PromptsPage` and `PromptDetailPage` are all real, and were +before the specs were written. The entries were simply stale, which is worth recording: +this file is only useful while it is true, and a stale "blocked on" entry costs more +than no entry at all, because it stops somebody porting work that is already possible. + +The specs cover the list, the per-server tool count including a server that discovered +none, the filter, the step through to a library's fragments and the include expression +a reader copies, and both failure journeys. The detail page's two failure states are +asserted apart — a library that could not be loaded and a library that does not exist +lead to different actions, and the page distinguishes them. + +One thing they needed from the harness: a spec can now declare console output it +provokes on purpose, with `test.use({ expectedNoise: [...] })`. The not-found journey +makes the browser log a 404, and forgiving 404s for the whole suite would have blunted +the guard — a 404 is also what a missing asset looks like, and this repository has +shipped one to production that way before. + +## Lost with the REST path tests, and where it went instead + +`src/api/readPaths.test.ts` and `src/api/writePaths.test.ts` are gone. They drove the API +client over REST URLs against the MSW fixture backend, and neither the URLs nor that +backend's REST routes exist any more — the controller serves its application API as +gRPC-Web. `src/api/operations.test.ts` replaces them, against the real generated service +descriptors served in-process, and covers strictly more of what those two were for: which +RPC each operation invokes, with what identity in the request message, and what the +response converts to. + +**One property could not live there, and it now lives in a browser spec instead.** The old +write tests read each create *back through its list* — "the create returned 200" and "the +thing exists" are different claims, and only a stateful backend can check the second. The +in-process router is stateless per test, so `operations.test.ts` cannot. That property is now +`playwright/tests/agents/harnesses.spec.ts`: create a harness, land back on the tab it was +created from, and find it in the list — and find it reported "not ready yet", which is the +state a cluster reports for one the controller has not observed. Nothing about it is +deferred any more. + +It lived in an agent-create spec until that page was removed: an agent is not something +anybody creates, so the form that appeared to create one went, and the read-back property +moved to the nearest thing that is genuinely created. + +Two things worth keeping from writing it, because both cost time and neither is guessable: + +- **Stay inside one browsing context.** The fixture backend keeps writes in the page's own + memory, deliberately, so one spec's creates cannot leak into the next one's list. A + `page.goto` therefore starts a backend that has never heard of the thing just created, + and the failure reads as "the create did not stick" when nothing is wrong. Click through + from the list. +- **The second read is the point.** `chat-capabilities-toggle` is asserted rather than the + heading or the panel, because those render from the URL and would appear for an agent + that does not exist. That button renders only when the per-agent read resolved a row, so + it is what distinguishes "the list re-fetched" from "the thing exists". The weaker + version of this spec passes and proves less than it looks like it does. + +**Why agents and not the other three.** A created prompt library, model configuration or +MCP server does *not* appear on its list until the reader presses Refresh: `AgentNewPage` +refreshes its list after a create and `PromptNewPage`, `ModelNewPage` and +`McpServerNewPage` do not. The first draft of this journey was written against prompt +libraries, and failing there is how that was found. The same asymmetry exists on the branch +this one was ported from, so it is a shipping defect rather than a regression, and it is +deliberately not fixed here — that would widen a large port. **The journey for those three +belongs in the change that fixes them**, where it is what proves the fix, rather than +sitting red in the suite describing a known bug. + +--- + +## Lost when agents became AgentInstances + +One thing the suite used to cover no longer exists, and it should not be replaced by a +passing test of something adjacent. Two others that were listed here — the capabilities +panel and the sharing loop — have since come back and are covered. + +### An agent's own tools, model and readiness on its details page + +The details page showed a `SandboxAgent`'s spec: its model resolved from a `ModelConfig`, +its tool bindings, and its `Ready` condition with a reason. It now shows the +`AgentInstance` record instead — state, operation, the pair it was cut from, the prepared +revision, the A2A authority and the failure — which is the whole of what the API knows +about an instance. + +That is not a reduction to fix: an instance genuinely has no spec. The configuration +belongs on the `AgentTemplate` and `Harness` surfaces, and those exist now — the agents +landing page carries all three as tabs, and a conversation's record links out to the +template and to the agent rather than duplicating either. What is still not covered in a +browser is that an agent's readiness *reason* is readable end to end, because the +`AgentInstance` record reports a failure message and the template reports a condition, and +no single surface shows both. + +## What the chat fixes could not be covered against + +Three gaps left by the work on the reader's own message, the artifact-append streaming +and the lifecycle indicator. Each is a *mock* gap: the mock backend cannot produce the +state the assertion would need, and inventing one would make the fixture the thing being +tested. + +### The suspending stage of the lifecycle indicator + +`chat.spec.ts` drives the indicator through its resting reading and through `running`, +because a turn produces both. It never sees `resuming` or `suspending`: those come from +`AgentInstance.operation`, which the controller claims and clears as it works, and the +mock backend serves a static record. Faking one would prove only that a fixture can hold +a string. + +The reading itself is covered exhaustively in `src/components/chat/lifecycleReading.test.ts` +— including the case worth guarding hardest, that **no stage is claimed when a turn ends**, +since a substrate agent really does suspend itself then and nothing in the API reports it. +What is missing is a browser journey that suspends an instance from the agents list while a +chat page is open on it and watches the indicator follow. That belongs in `playwright/live/`, +where the operation is real. + +### Streaming, end to end, against a controller + +The client now honours an artifact's `append` flag, which is how this runtime streams: one +`artifactId` for the reply, one frame per token, `append` on every frame after the first, +then a closing frame repeating the whole answer. That shape is pinned in +`src/api/chat/a2aGrpcChatClient.test.ts` against frames captured from the controller on +2026-08-24, and it was confirmed by hand — `grpcurl` at the gateway, and a throwaway +Playwright run against a live instance that rendered the reply. + +**The mock chat client does not reproduce that shape.** It streams with `delta` events, +which is the port's own vocabulary rather than the wire's, so no browser test exercises the +artifact path. Teaching the fixture to emit artifact frames would mean it stopped being a +`ChatClient` and started being an A2A server, which is the wrong seam — the transport is +already covered by unit tests over real bytes. The browser-level gap is a `playwright/live/` +spec that sends a message and asserts the reply grows on screen before the turn completes. + +A related gap worth naming rather than leaving implicit: the mock backend serves one +instance per conversation and never *changes* an instance's `operation`, so the lifecycle +indicator's `resuming` and `suspending` stages have no browser coverage either. Both +belong in the same live spec. + +### Tool approval, and a question asked without the extension + +`ask_user` is now answerable end to end: the question renders with its choices, the +answer names the parked turn and carries the extension payload, and the agent uses it. +What is left are the two neighbouring cases, both of which the UI *recognises* and says +plainly rather than guessing at. + +**A `tool_approval_request`** carries `tools[]` and a `hint` and is answered with +`tool_approval_response` / `approvals[]` — a different payload, and a different control: +per-tool approve or reject, with a rejection reason. The prompt names the tools and +offers only the discard, which is honest. Building the approval controls needs the +product decision about what a reader is being asked to vouch for, and it should not be +guessed from the shape of the payload. + +**A turn parked without the HITL extension activated** has no payload at all — the +question exists only as prose and carries no correlation id, so no answer can be routed +to it. The prompt says so and offers the discard. This build always activates the +extension, so it can only arise from a turn started by something else (a `kubectl`-driven +send, an older client). It is not worth engineering around; it is worth not lying about. + +**The `ask_user` payload still renders as JSON in the transcript**, beside the answerable +prompt — the tool call and its result are structured data and are shown as such. That is +now duplication rather than a defect, and collapsing it needs a decision about whether a +tool call that has an interactive rendering should still show its raw form at all. + +## Blocked on the API: server-side paging, searching and sorting for three lists + +**Models, prompt libraries and MCP servers narrow their rows in the browser, and the +RPCs are why.** Recorded here rather than left implicit, because the shape of the +request is the whole argument: a client-side filter is honest when the response holds +every row and dishonest when it holds one page of them, and only the proto says which. + +| Read | Request today | What it takes | What it needs | +|---|---|---|---| +| `ListModelConfigs` | `ListModelConfigsRequest {}` | nothing at all | `PageRequest page`, `string filter`, a sort field enum and `SortOrder` | +| `ListToolServers` | `ListToolServersRequest {}` | nothing at all | the same four | +| `ListPromptTemplates` | `ListPromptTemplatesRequest { string namespace = 1 }` | one namespace | `PageRequest page`, `string filter`, sort field and order — the namespace is already there | + +There is a worked precedent to copy rather than a design to invent: +`ListSubstrateActors` and `ListSubstrateWorkers` in `system.proto` carry exactly this +shape — `PageRequest{limit, page_token}` / `PageResponse{next_page_token}`, a +case-insensitive substring `filter` over the fields the row displays, and a sort-field +enum whose every order ends in a unique column so a page token names exactly one row. +The commentary in that file is worth reading before adding a fifth variant of it. + +**Until then the pages are client-side and say so on the page**, naming the RPC +(`models-read-note`, `mcp-servers-read-note`, `prompts-read-note`), and +`tests/lists/list-filters.spec.ts` asserts that they do. That is defensible only while +the response is the whole list. **The moment any of these three RPCs starts paging, its +page must lose its client-side search and sort in the same change** — a filter over a +page reports "no matches" about a row on page nine, which is the defect the substrate +page was rewritten to remove. `substrate.spec.ts`'s "the paged tables do not pretend to +sort, and the inline ones do" is the assertion that draws the line; the last step of +`list-filters.spec.ts` keeps these pages on the correct side of it. + +The prompts page is a partial exception worth not losing: `ListPromptTemplates` takes a +namespace, so `usePrompts` fans out one call per namespace and its **namespace filter is +genuinely server-side already**. Only its search and sort are not. + +### Not deferred, but named here so it is not looked for: paging is client-side too + +All three tables show a page control. It pages rows that are already in the browser, +which is a real convenience on a long list and is not a claim about the server. The +totals beside the controls and in the pager are therefore true totals — unlike a paged +read, where counting what arrived and calling it a total is the failure +`GetSubstrateSummary` exists to prevent. + +--- + +## Auto-titling costs a read per row, so the table still does not do it + +A conversation is named by the reader, and an unnamed one can be titled from its first +message — `ListTasks{ContextID: instanceId}` returns the history. That is **free on the +chat page**, which has already read the transcript because it is rendering it. + +The **rail** now pays for the rest, bounded at thirty: every row but the open one used to +read `Untitled · 50b46891`, which made the list very nearly unusable — the one row a +reader could identify was the one they were already looking at. Thirty reads for a rail +somebody is navigating by is a trade worth making; failures are per-row and silent, +because a title is a convenience over an id that already identifies the row. + +The agent's conversation **table** still falls back to `Untitled · `, and that +is a decision rather than an omission. It pages, it is sorted and searched server-side, +and titling a page of rows to put a label on a table is the shape of cost the substrate +page's three RPCs exist to avoid. + +Two ways it could stop being a trade-off, both server-side and neither invented here: + +- **`AgentInstance` carries the first message**, denormalised the way `description` and + `model_config_ref` already are on `AgentTemplate`. One extra string on a message the + list returns anyway, and no extra call at all. +- **`ListAgentInstances` gains a field mask** for it, so callers that want it pay and + callers that do not are unaffected. + +Either would let a list show what the chat page already shows. Until then, what a list +renders for an unnamed conversation is pinned by `agents/agent-conversations.spec.ts` — +both that it is never a bare UUID, and that the derived title appears where the +transcript is in hand. + +## An agent's conversation search is over what was fetched, and the page-following is why + +`ListAgentInstances` narrows to one agent **on the server**: it takes `agent_template` +and `harness` and resolves them through the prepared revision. That is the narrowing +that matters, because it is the one the paging is applied after. What the request does +**not** carry is a search term or a sort field, so the agent page's search box and column +sorts run in the browser. + +That is honest here for a reason worth stating, since it is the opposite of the three +lists above: the client follows every page token before rendering anything +(`INSTANCE_PAGE_LIMIT` in `api/grpc/operations.ts`), so what is in the browser is every +conversation with that agent rather than the first fifty. The note under the table says +exactly that. + +**If that page-following is ever removed** — and it should be, once an agent can have +thousands of conversations — the search and the sort must go server-side in the same +change. The fields to add are the ones `ListSubstrateActors` already carries. + +## An agent's page is derived, because a pair is not an object + +`/agents/:namespace/:agentTemplate/on/:harness` reads a template and filters +conversations; there is no `GetAgentPair` because there is no pair *service*. A pair is +derived — the controller materialises it from admission and retires it when the labels +stop matching — so nothing creates one and nothing could name one. + +Two consequences are visible on screen and are deliberate. An agent cannot be renamed, +so two agents cut from one template share a name and are told apart by the harness +column. And an agent's page cannot show a revision history, a creation time, or who made +it: `agent_template_harness_pair` holds all three and no RPC exposes the table. Adding +one is the change that would unblock both, and it is a larger decision than this +surface. + +## A new template labelled for the only harness + +**What is not covered:** that a new agent template arrives already labelled for the +harness that will run it, when the cluster has exactly one. + +**Why:** the fixtures carry more than one harness on purpose — one of them exists +specifically so a template can be admitted by *two*, which is what makes an agent list +show two rows for one template. A single-harness cluster is therefore not a state these +fixtures can be in, and the default correctly does nothing against them. + +The opposite half *is* covered: with several harnesses nothing is chosen for the reader, +and a template no harness admits says so ("creating one, and being told when nothing +will run it"). + +**How it was checked instead:** against the live cluster, which has one harness +(`kagent`) — the same shape the default exists for. + +**What would close it:** a fixture scenario with a single harness. Worth doing when +something else needs one; a scenario knob added for one assertion is a second fixture +backend to keep honest. diff --git a/ui/playwright/README.md b/ui/playwright/README.md index 53b575571..11c63e4a2 100644 --- a/ui/playwright/README.md +++ b/ui/playwright/README.md @@ -1,91 +1,158 @@ -# Playwright E2E tests +# Playwright end-to-end tests -Page-level browser end-to-end tests for the kagent UI, run against a real kagent -backend in a kind cluster. This suite covers **multi-step user flows across -components** — the gap unit tests, Storybook, and Chromatic don't fill. +Browser tests for the kagent UI. This suite is the project's acceptance bar: the +rewrite is done when the same general set of journeys still passes. -## What this suite covers +## Running -| Layer | Tool | -|---|---| -| Atoms (`src/components/ui/*`) | shadcn primitives — skip | -| Visual / render states | Storybook + Chromatic | -| Unit / logic | Jest / Vitest | -| **Multi-step flows: create → configure → use → delete, validation, streaming** | **Playwright (this suite)** | +```bash +cd ui +yarn test:pw # or: yarn test:e2e +UI_LOOP_PORT=8012 yarn test:pw # when something else owns the default port +``` -## How it works +Nothing else is needed — no cluster, no port-forward, no provider key. -The UI fetches data server-side (Next.js server actions), and the chat stream -`POST /a2a/**` runs server-side through the Next route handler -(`src/app/a2a/[namespace]/[agentName]/route.ts`). Both resolve their target via -`getBackendUrl()` (`src/lib/utils.ts`), which reads `BACKEND_INTERNAL_URL`. +There is a second suite that does need all three; see +[Live runs](#live-runs-against-a-real-backend) at the foot of this file. -A lightweight proxy (`mocks/server.mjs`) sits at that address and: +## What changed from the old suite -- forwards every `/api/**` request to the real kagent backend (`KAGENT_BACKEND_URL`); -- intercepts `/a2a/**` and `/a2a-sandboxes/**`, answering with a canned A2A v1 SSE - reply (`statusUpdate` / `ROLE_AGENT` / `TASK_STATE_COMPLETED`), so the suite - never needs a live LLM. +The old suite ran against a real kagent backend in a kind cluster. Running it +meant building images, `make create-kind-cluster` and `make helm-install`, +exporting a provider key, and a port-forward held open for the duration, with a +Node proxy in front to forward `/api/**` to the controller and mock the chat SSE +stream. Roughly: **several minutes of setup, a cluster, and a provider key.** -``` -Browser ─▶ next dev :8001 ─┬─ /api/* (server actions) ─┐ - └─ /a2a/* (route handler) ───┤ - proxy :8899 ─┬─ /a2a/* ─▶ mocked SSE - └─ /api/* ─▶ real backend :8083 -``` +This suite needs none of that — `yarn test:pw`, about eight seconds, on any +machine that can run the dev server. `playwright/setup.ts`, `teardown.ts`, +`mocks/server.mjs` and `scripts/setup.sh` are gone with the apparatus they +served. + +Worth stating plainly because it is a change in how contributors work, and +because it is a trade: the suite no longer exercises the real controller, so it +proves the UI behaves, not that the backend contract still holds. Contract drift +is caught by the Go tests and by whatever runs against a live cluster in CI — not +here. + +## What it runs against -`playwright/setup.ts` port-forwards the controller to `:8083` for the run; -`playwright/teardown.ts` stops it. Playwright's `webServer` boots the proxy and -`next dev`. +The suite runs against the in-browser mock backend (`src/mocks/`), pinned by the +`webServer` block in `playwright.config.ts` so an inherited `VITE_API_MODE=live` +cannot silently point a run at a real cluster. That buys three things the old +kind-cluster setup could not offer: the data is fixed, so a spec can assert exact +rows; the run takes seconds; and failure is a first-class state rather than +something you have to break a cluster to see. + +**Scenarios.** The mock backend reads how it should behave from the query string +on every request, so a spec drives the awkward states by navigating: + +| | | +|---|---| +| `?mock=ok` | normal data (the default) | +| `?mock=empty` | every list comes back empty | +| `?mock=error` | every request fails with a 500 | +| `?mock=slow` | a long delay, so the loading state is observable | + +The scenario is remembered for the browsing session, so **always pass one +explicitly** — `withScenario()` in `helpers/app.ts` does this, and `loadPage()` +defaults to `ok`. A bare path inherits whatever the previous step asked for, +which is convenient in a browser and a trap in a test. ## Layout ``` playwright/ - tests/ # .spec.ts + -errors.spec.ts per area, plus cleanup.spec.ts - helpers/ # page, nav, select, a2a drivers - mocks/ - server.mjs # proxy: forwards /api to the real backend, mocks chat - fixtures/ - test.ts # import { test, expect } from here - scripts/ - setup.sh # build + install kagent into kind + tests/ app-shell, routing, and /{,-errors}.spec.ts + helpers/ app (navigation, tables, scenarios), nav (shell chrome), + extensions (vendor slots) + fixtures/test.ts import { test, expect } from here — never @playwright/test + live/ the live suite: specs, plus helpers/ of its own + DEFERRED.md the specs not yet portable, and what each one is waiting on ``` -## Running +## Vendor extensions: two servers, two projects -```bash -cd ui -npm install -npx playwright install chromium # first time only +Which extension a build ships with is decided at build time, so "installed" and +"not installed" cannot be two states of one server. The config boots two: -./playwright/scripts/setup.sh # kind cluster + real kagent (once) -yarn run test:e2e # (or: npm run test:e2e) -``` +| Project | Server | Specs | +|---|---|---| +| `chromium` | bare — no extension, on `UI_LOOP_PORT` | everything not matching `*.vendor.spec.ts` | +| `chromium-vendor` | `VITE_VENDOR_EXTENSIONS=example`, on `UI_LOOP_PORT + 50` | `*.vendor.spec.ts` | -`setup.sh` builds the images and installs kagent via `make create-kind-cluster` -and `make helm-install`. It needs a provider key; since chat is mocked, a dummy -works (`export OPENAI_API_KEY=fake`). `test:e2e` port-forwards the controller, -boots the proxy + `next dev`, and runs the suite. +A spec opts into the extension-installed app by being named `*.vendor.spec.ts`. -Point at an already-reachable backend with `KAGENT_BACKEND_URL=http://:8083`. +The gap of 50 between the ports is deliberate. Vite falls forward to the next +free port when the one it is told to use is busy, so with adjacent ports a +slow-to-die server from a previous run can push one app onto the other's port — +which surfaces as a spec mysteriously unable to find the contribution it is +asserting on. `globalSetup` also checks that each port is serving the build its +project expects, and fails the run immediately with that explanation if not, so +a harness problem cannot be mistaken for a product one. -Interactive / debug: +**Assert the mechanism, never the example.** The bundled Example extension is +documentation that happens to run, and it is expected to change. Specs go +through the `vendor-slot-` test id that `VendorSlot` emits — that a +configured component mounts at its point, in the DOM position the point promises, +carrying the context the point declares. Nothing asserts Example's copy. -```bash -npm run test:pw:ui # interactive UI mode -npm run test:pw:debug # step-through debugger -``` +One assertion in there is subtler than it looks: every per-row badge renders +*identical* text, so a contribution that ignored its context entirely would +satisfy any text assertion. What proves context is per-row is that the +contributions are **distinguishable from each other** — so that spec asserts +distinctness and deliberately says nothing about the values. ## Conventions -- Import `{ test, expect }` from `../fixtures/test`. -- Two specs per feature area: `.spec.ts` (the success/CRUD journey) and - `-errors.spec.ts` (the validation/error journey). -- Mutating specs create uniquely-named resources and delete them; `cleanup.spec.ts` - sweeps any `e2e-*` leftovers from interrupted runs. Seeded resources (never - prefixed `e2e-`) are left untouched. -- Prefer `getByRole` / `getByLabel`; add `data-testid` only where role/text is - ambiguous (list rows, per-item action buttons). -- The suite runs serially (`workers: 1`) against one shared cluster. +- **Import `{ test, expect }` from `../fixtures/test`.** That fixture fails any + test where the app logged an error or threw, which is how a spec can trust its + own green — a page can satisfy every assertion while throwing in an effect. + Deliberate noise (the 500 the error scenario provokes) is filtered there, in + one place, with a reason. +- **Two specs per area**: `.spec.ts` for the success journey, + `-errors.spec.ts` for the failure journey. +- **One test per journey**, with each criterion a numbered `test.step`. Playwright + records one trace per test, and a journey split across tests loses the thing + worth watching — that the state one step established is the state the next one + acted on. +- **Prefer roles and test ids over prose.** Most of these pages are still going to + be rebuilt; a spec anchored to copy will not survive that, and one anchored to + `nav-agents` or `getByRole("row")` will. +- **Assert against the list a user would read**, not against a toast or a closed + modal. A success message proves the app thinks it worked. + +## Live runs, against a real backend + +```bash +cd ui +yarn test:pw:live +UI_LOOP_LIVE_PORT=8312 yarn test:pw:live # to run beside something on 8301 ``` + +Unlike `yarn test:pw`, this one **does** need a cluster, with the controller +port-forwarded. It is not run in CI. The specs live in `playwright/live/`, and the +coverage deliberately left out of it is in `DEFERRED.md`. + +A live run reaches the controller through Vite's proxy, exactly as a deployed +build reaches it through nginx, so the app uses the same relative URLs either way +and this mode tests the addressing a real deployment uses. + +**Why a separate mode rather than a third project.** `UI_LOOP_LIVE=true` swaps the +whole `projects`/`webServer` pair in `playwright.config.ts` instead of appending to +it, because the two modes' requirements are mutually exclusive. A live project in +the default list would make `yarn test:pw` — which is meant to need nothing but a +machine that can run the dev server — fail on any laptop without a cluster in +front of it. And a live run has no use for the two mock servers, so starting them +would cost every live run the time to boot Vite twice for nothing. The two runs +are disjoint. The live project also gets its own port, 8301, far from the mock +servers' 8001/8051 for the same reason those two are 50 apart. + +**A green live run has to have been live.** `VITE_API_MODE` is pinned at build +time as well as at runtime, because a build-time pin is the one thing an inherited +`.env` cannot override — and a live suite that quietly answered from fixtures +would be worse than a red one, since a green one gets taken as evidence the +cluster works. `globalSetup` asks the page what settings it was actually handed +and refuses the run if they are not the live ones. Traces are kept on failure: +unlike the mock suite there is no fixed fixture to re-read afterwards, so the +trace is the only record of what the cluster answered. diff --git a/ui/playwright/backend.ts b/ui/playwright/backend.ts deleted file mode 100644 index 07b5a114e..000000000 --- a/ui/playwright/backend.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Single source of truth for where the REAL kagent backend lives during E2E. -// -// The proxy (mocks/server.mjs, configured in playwright.config.ts) targets -// KAGENT_BACKEND_URL, and setup.ts port-forwards the controller onto that URL's -// port. Both read this module so the two can never drift apart — setting the env -// var alone stays consistent across the proxy target and the port-forward. - -// In-cluster controller port; also the local port the port-forward defaults to. -export const CONTROLLER_PORT = 8083; - -// Origin of the real backend the proxy forwards to. The proxy mocks only the -// chat A2A stream; every other /api call hits this backend. -export const KAGENT_BACKEND_URL = - process.env.KAGENT_BACKEND_URL ?? `http://127.0.0.1:${CONTROLLER_PORT}`; - -// Local port the port-forward must open, derived from KAGENT_BACKEND_URL so it -// always matches the proxy target. Falls back to CONTROLLER_PORT for a URL with -// no explicit port. -export const LOCAL_PORT = Number(new URL(KAGENT_BACKEND_URL).port || CONTROLLER_PORT); diff --git a/ui/playwright/fixtures/test.ts b/ui/playwright/fixtures/test.ts index 5a7fbfcbc..9cc0b8740 100644 --- a/ui/playwright/fixtures/test.ts +++ b/ui/playwright/fixtures/test.ts @@ -1,18 +1,94 @@ -// Shared test fixture. Import { test, expect } from here in every spec (not -// directly from @playwright/test) so all specs get the same setup: the first-run -// onboarding wizard is bypassed on every navigation. The onboarding spec opts -// back in by setting the flag to "false" in its own init script (init scripts run -// in registration order, so the spec's wins). - -import { test as base, expect } from "@playwright/test"; - -export const test = base.extend({ - page: async ({ page }, run) => { - await page.addInitScript(() => { - window.localStorage.setItem("kagent-onboarding", "true"); - }); - await run(page); - }, +/** + * The shared fixture. Every spec imports `{ test, expect }` from here rather than + * from `@playwright/test`, so they all get the same guard: anything the app logs + * as an error, or throws and fails to catch, is collected and asserted on at the + * end of the test. + * + * That guard is why a spec can trust its own green: a page can satisfy every + * assertion while throwing in an effect, and without this the suite would not + * notice. + */ + +import { test as base, expect, type Page } from "@playwright/test"; + +/** + * Console output a spec deliberately provokes, which is evidence rather than a defect. + * + * Kept as short as it can be. It used to forgive the browser's log of a 500, from + * back when the error scenario answered one — but the API is gRPC-Web served by a + * substituted transport now, so a failed call never becomes a failed HTTP request and + * that line cannot appear. An allowance for noise that can no longer occur costs + * nothing directly and misleads twice: it reads as evidence that HTTP failures still + * happen here, and it widens the guard for every spec at once. Declare noise on the + * spec that earns it instead — the one entry below is here only because it belongs to + * no spec. + */ +const EXPECTED_NOISE: RegExp[] = [ + /* + * Firefox, under parallel load, logging its own mock harness rather than the app. + * + * Firefox occasionally routes a request with an empty URL through the service + * worker while the worker is still taking over the page, and MSW's fetch handler + * has nothing to answer it with. It appears in whichever spec happens to be + * starting at the time, which is why it is here and not on one spec: it belongs to + * the harness, not to any journey. + * + * The pattern names `mockServiceWorker.js` deliberately. A real failure to load a + * real asset produces a message naming that asset, and still fails — the allowance + * is only for the mock worker reporting on itself, which cannot happen in a build + * at all, since the Dockerfile deletes that file. + */ + /A ServiceWorker intercepted the request and encountered an unexpected error[\s\S]*mockServiceWorker\.js/, +]; + +export interface AppErrors { + /** Console errors and uncaught exceptions seen so far, deliberate ones removed. */ + readonly messages: string[]; +} + +/** + * Extra console output one spec provokes on purpose. + * + * Declared per spec with `test.use({ expectedNoise: [...] })` rather than added to + * the shared list, so an allowance stays where its justification is. A spec that + * drives a 404 on purpose needs that 404 forgiven; the rest of the suite must still + * fail on one, because a 404 is also what a missing asset looks like — this repository + * has already shipped a service worker to production that way once. + */ +export type ExpectedNoise = readonly RegExp[]; + +export const test = base.extend<{ + appErrors: AppErrors; + expectedNoise: ExpectedNoise; +}>({ + expectedNoise: [[], { option: true }], + + appErrors: [ + async ( + { page, expectedNoise }: { page: Page; expectedNoise: ExpectedNoise }, + run, + ) => { + const messages: string[] = []; + + page.on("console", (message) => { + if (message.type() !== "error") return; + const text = message.text(); + const allowed = [...EXPECTED_NOISE, ...expectedNoise]; + if (allowed.some((pattern) => pattern.test(text))) return; + messages.push(text); + }); + page.on("pageerror", (error) => messages.push(`uncaught: ${error.message}`)); + + await run({ messages }); + + // Asserted here rather than in each spec so no spec can forget to. + expect( + messages, + `the app logged errors:\n${messages.join("\n")}`, + ).toEqual([]); + }, + { auto: true }, + ], }); export { expect }; diff --git a/ui/playwright/globalSetup.ts b/ui/playwright/globalSetup.ts new file mode 100644 index 000000000..147327bb6 --- /dev/null +++ b/ui/playwright/globalSetup.ts @@ -0,0 +1,124 @@ +import { chromium, type FullConfig, type Page } from "@playwright/test"; +import { LIVE_PROJECT } from "../playwright.config"; + +/** + * Waits until each server actually renders the app, not merely answers on its + * port. + * + * Playwright's `webServer.url` check passes as soon as the dev server responds, + * but Vite pre-bundles dependencies on the *first real page load* and forces a + * full reload when it finishes. A test navigating into that window has the DOM + * pulled out from under it mid-assertion, which showed up as the first run after + * a cold start failing and every run after it passing — the worst kind of flake, + * because it looks like a broken feature rather than a broken harness. + * + * Loading each app once here moves that reload before any test exists. + */ +export default async function globalSetup(config: FullConfig): Promise { + const browser = await chromium.launch(); + try { + for (const project of config.projects) { + const baseUrl = project.use.baseURL; + if (!baseUrl) continue; + + const page = await browser.newPage(); + try { + await page.goto(baseUrl, { waitUntil: "load" }); + // The shell rendering is the signal that the module graph is served and + // any optimisation reload has already happened. + await page.waitForSelector('[data-testid="app-content"]', { + timeout: 120_000, + }); + + // The live project runs alone on its own port, so it cannot be the + // victim of the port swap checked for below, and the vendor check does not + // apply to it at all. What it has instead is a failure the mock projects + // cannot have: coming up against fixtures and passing every assertion + // without touching a backend. + if (project.name === LIVE_PROJECT) { + await verifyLiveWiring(page, baseUrl); + continue; + } + + // Which build a port is serving is decided when its server starts, and + // two dev servers coming up together have been seen to end up the wrong + // way round. Checked here so that failure reads as what it is, at the + // start, instead of surfacing later as a spec that cannot find the + // contribution it was asserting on. + const slots = await page.locator('[data-testid^="vendor-slot-"]').count(); + const wantsVendor = project.name.includes("vendor"); + + if (wantsVendor && slots === 0) { + throw new Error( + `${baseUrl} was expected to serve the app with the example extension ` + + `installed (project "${project.name}"), but no extension points are ` + + `mounted. The server on that port came up without ` + + `VITE_VENDOR_EXTENSIONS=example.`, + ); + } + if (!wantsVendor && slots > 0) { + throw new Error( + `${baseUrl} was expected to serve the app with no extension installed ` + + `(project "${project.name}"), but ${slots} extension points are ` + + `mounted. The two dev servers have come up on each other's ports.`, + ); + } + } finally { + await page.close(); + } + } + } finally { + await browser.close(); + } +} + +/** + * Fails a live run that would otherwise pass without reaching the backend. + * + * A page-load suite is unusually easy to satisfy dishonestly. These pages render + * — heading, toolbar, empty table — whether the data came from a cluster, from + * in-browser fixtures, or from nothing at all. So every way a live run can be a + * lie ends in green, and a green one is taken as evidence the cluster works. + * These are harness faults, so they are caught here, before any test exists, and + * said plainly: a red run is recoverable, a green one that measured nothing is + * not. + * + * Checked by asking the page what it was told rather than by reading `.env` here: + * what matters is the configuration the browser received, which is the only thing + * the app acts on. An installed extension has its own settings to check, and adds + * those checks after the ones below. + */ +async function verifyLiveWiring(page: Page, baseUrl: string): Promise { + // The same object the app itself reads settings from — written into the + // document by the dev server (`vite.config.ts`) the way the container renders + // it at startup, so this reads exactly what the page was configured with. + const settings = await page.evaluate( + () => + (window as unknown as { environmentVariables?: Record }) + .environmentVariables ?? {}, + ); + + if ((settings.ENABLE_MOCK_UI ?? "").toLowerCase().includes("true")) { + throw new Error( + `${baseUrl} is serving the in-browser mock backend (ENABLE_MOCK_UI=` + + `${settings.ENABLE_MOCK_UI}), so a live run would pass without calling the ` + + `controller at all. Unset it in ui/.env, or run the mock suite instead.`, + ); + } + + // A second way a live run can be a lie: the flag can be off while a worker from + // an earlier mock run is still installed and answering, which looks exactly like + // a working backend. + const workerCount = await page.evaluate(async () => { + if (!("serviceWorker" in navigator)) return 0; + const registrations = await navigator.serviceWorker.getRegistrations(); + return registrations.length; + }); + + if (workerCount > 0) { + throw new Error( + `${baseUrl} has ${workerCount} service worker(s) registered. The mock backend ` + + `is a service worker, so a live run cannot be trusted while one is installed.`, + ); + } +} diff --git a/ui/playwright/helpers/a2a.ts b/ui/playwright/helpers/a2a.ts deleted file mode 100644 index 4cf2a4e64..000000000 --- a/ui/playwright/helpers/a2a.ts +++ /dev/null @@ -1,18 +0,0 @@ -// A2A chat helpers for the failure path. -// -// The success-path chat reply is mocked server-side by the proxy (see -// playwright/mocks/server.mjs), which intercepts /a2a and returns a canned SSE -// stream — so specs don't stub the happy path in the browser at all. -// -// The failure path is different: to simulate a broken stream we abort the request -// in the browser before it reaches the Next route handler. `POST /a2a//` -// is a real browser fetch, so page.route CAN intercept it here. - -import { type Page } from "@playwright/test"; - -/** Intercept the chat SSE call and fail it (network error), for the failure path. */ -export async function mockAgentStreamError(page: Page): Promise { - const handler = (route: import("@playwright/test").Route) => route.abort("failed"); - await page.route("**/a2a/**", handler); - await page.route("**/a2a-sandboxes/**", handler); -} diff --git a/ui/playwright/helpers/app.ts b/ui/playwright/helpers/app.ts new file mode 100644 index 000000000..1781c46fc --- /dev/null +++ b/ui/playwright/helpers/app.ts @@ -0,0 +1,160 @@ +/** + * Page-level drivers. + * + * These assert on rendered DOM through roles and test ids, never on prose, so + * they survive the page rebuilds still ahead. Where a test id is used it is one + * the app already ships for the purpose. + */ + +import { expect, type Locator, type Page } from "@playwright/test"; + +/** Routes the suite drives. Mirrors `src/router/routes.ts`. */ +export const routes = { + dashboard: "/", + login: "/login", + agents: "/agents", + agentNew: "/agents/new", + models: "/models", + modelNew: "/models/new", + mcpServers: "/mcp", + prompts: "/prompts", + substrate: "/substrate", + /* The templates list is a tab of the agents page now. The old address still + resolves — it redirects here — but a test should go where the reader goes. */ + agentTemplates: "/agents?tab=templates", + harnesses: "/agents?tab=harnesses", + harnessNew: "/harnesses/new", + agentTemplateNew: "/agent-templates/new", +} as const; + +/** + * The fixture conversations the suite drives, by the id the API addresses them with. + * + * An `AgentInstance` is one conversation, addressed as `(namespace, id)` where the + * id is a UUID — so these are the ids from `src/mocks/fixtures.ts`, named here for + * what each one is *for* rather than pasted into every spec. + */ +export const instances = { + /** Ready, named by the reader, and the one with a seeded transcript behind it. */ + ready: "6f1c9d20-1b7a-4a1e-9a3f-2c0d8e5b1a44", + /** Suspended, so it can be resumed. Unnamed, so it renders as untitled. */ + suspended: "b28e4f13-5c66-4d90-8f2b-77a1e9c34d05", + /** Failed, with a reason the conversation's record page shows. */ + failed: "d4b02f87-3a55-4c18-9e6b-1f70c9a8e332", + /** Somebody else's: listable under its agent, and not openable. */ + someoneElses: "8e5f2b09-6c14-4a7d-83b0-9d1c7e40f5a6", +} as const; + +/** + * The fixture agents, which are `(AgentTemplate, Harness)` pairs. + * + * An agent is named by its template, so a pair is written the way the address reads: + * the template, then the harness that runs it. + */ +export const agents = { + /** `instances.ready` and its siblings are conversations with this one. */ + k8s: { template: "k8s-agent-7f3a91c", harness: "k8s-agent" }, + /** + * One template, two harnesses — so two agents that share a name. + * + * The pair that makes an agent a pair rather than a template: keyed on the + * template alone, these two would be one row and their conversations would merge. + */ + sharedOnK8s: { template: "shared-brain", harness: "k8s-agent" }, + sharedOnFastLane: { template: "shared-brain", harness: "fast-lane" }, + /** Admitted, but with no successful revision — so no conversation can start. */ + preparing: { template: "support-triage-2b91d0e", harness: "support-triage" }, +} as const; + +/** Where one agent lives: its namespace, its template, and the harness it runs on. */ +export const agentPage = ( + agent: { template: string; harness: string }, + namespace = "kagent", +) => `/agents/${namespace}/${agent.template}/on/${agent.harness}`; + +/** + * Where clicking an agent's name goes: a conversation that does not exist yet. + * + * Distinct from `agentPage`, which is the agent's own page listing what it already + * has. Nothing is created until the first message is sent, which is why the two are + * different addresses rather than the same one behaving differently. + */ +export const agentNewChat = ( + agent: { template: string; harness: string }, + namespace = "kagent", +) => `${agentPage(agent, namespace)}/new`; + +/** + * The other conversation with the same agent that the caller can actually see. + * + * Cut from the same `(Harness, AgentTemplate)` pair as `instances.ready`, so the rail + * lists it as a sibling. It is the *suspended* one because the other siblings in the + * fixtures were created by somebody else, and the list returns only the caller's own + * instances unless `all_creators` is asked for — a sibling behind that switch would + * not be in the rail at all. + */ +export const SIBLING_OF_READY = instances.suspended; + +/** Where one agent's conversation lives. */ +export const agentChat = (id: string, namespace = "kagent") => + `/agents/${namespace}/${id}/chat`; + +/** Where one agent's record lives. */ +export const agentDetail = (id: string, namespace = "kagent") => + `/agents/${namespace}/${id}`; + +/** + * How the mock backend should behave for a navigation. + * + * The app reads this from the query string on every request (see + * `src/mocks/scenario.ts`), which is what makes the loading, empty and failure + * paths drivable from a test without a second build or a stubbed module. + */ +export type MockScenario = "ok" | "empty" | "error" | "slow"; + +/** + * Adds the scenario to a path. + * + * Always pass one explicitly: the app remembers the last scenario for the + * browsing session, so a bare path inherits whatever the previous step asked + * for — which is convenient in a browser and a trap in a test. + */ +export function withScenario(path: string, scenario: MockScenario): string { + const separator = path.includes("?") ? "&" : "?"; + return `${path}${separator}mock=${scenario}`; +} + +/** Navigates, then optionally waits for the page's heading to confirm it arrived. */ +export async function loadPage( + page: Page, + path: string, + options: { scenario?: MockScenario; title?: string } = {}, +): Promise { + const { scenario = "ok", title } = options; + await page.goto(withScenario(path, scenario)); + if (title) await expectPageTitle(page, title); +} + +/** The current page's heading, as rendered by the shared page frame. */ +export function pageTitle(page: Page): Locator { + return page.getByTestId("page-title"); +} + +export async function expectPageTitle(page: Page, title: string): Promise { + await expect(pageTitle(page)).toHaveText(title); +} + +/** A table row containing the given text — the row a user would point at. */ +export function rowNamed(page: Page, text: string): Locator { + return page.getByRole("row").filter({ hasText: text }); +} + +/** Data rows only, excluding the header row and any placeholder row. */ +export function dataRows(page: Page): Locator { + return page.locator("tbody tr.ant-table-row"); +} + +/** Resolves once no loading indicator is left on the page. */ +export async function expectSettled(page: Page): Promise { + await expect(page.locator(".ant-spin-spinning")).toHaveCount(0); +} diff --git a/ui/playwright/helpers/extensions.ts b/ui/playwright/helpers/extensions.ts new file mode 100644 index 000000000..17b3759f8 --- /dev/null +++ b/ui/playwright/helpers/extensions.ts @@ -0,0 +1,93 @@ +/** + * Drivers for the vendor extension framework. + * + * These assert the **mechanism**, never the example's content. A point either + * mounts what was configured at it or it does not; the bundled Example extension is + * only the thing being mounted, and it is free to change its copy, its styling, + * or its own test ids without any of this needing an edit. + * + * The one handle these rely on is `vendor-slot-`, which `VendorSlot` emits + * for exactly this purpose. + */ + +import { expect, type Locator, type Page } from "@playwright/test"; + +/** Every extension point the app offers. Mirrors `src/vendorExtensions/extensionPoints.ts`. */ +export const EXTENSION_POINT_IDS = [ + "app_shell_appLayout_contentArea_leadingBanner", + "app_shell_appLayout_contentArea_globalOverlay", + "app_shell_appLayout_appSidebar_footer", + "app_agents_agentsList_pageHeader_actions", + "app_agents_agentsList_agentListItem_badge", + "app_agents_agentChat_agentChatMessage_additionalActionsButton", + "app_dashboard_dashboardOverview_summaryGrid_leadingCard", +] as const; + +export type ExtensionPointId = (typeof EXTENSION_POINT_IDS)[number]; + +/** Whatever is mounted at a point, wherever it ended up in the DOM. */ +export function slot(page: Page, id: ExtensionPointId): Locator { + return page.locator(`[data-testid="vendor-slot-${id}"]`); +} + +/** Every mounted slot on the page, regardless of point. */ +export function allSlots(page: Page): Locator { + return page.locator('[data-testid^="vendor-slot-"]'); +} + +/** + * Sidebar nav entries, in DOM order, as their test ids. + * + * `evaluateAll` is a one-shot read with none of Playwright's auto-waiting, so + * this waits for the sidebar to have rendered first. Without that it answers + * `[]` on a page that simply has not finished mounting — which reads as "the + * sidebar is empty" and would let a broken build pass. + */ +export async function navOrder(page: Page): Promise { + const entries = page.locator('[data-testid="app-sidebar"] [data-testid^="nav-"]'); + await entries.first().waitFor({ state: "attached" }); + return entries.evaluateAll((nodes) => + nodes.map((node) => node.getAttribute("data-testid") ?? ""), + ); +} + +/** The nav entries the application itself ships, in the order it declares them. */ +export const CORE_NAV_ORDER = [ + "nav-dashboard", + "nav-agents", + "nav-models", + "nav-mcpServers", + "nav-prompts", + "nav-substrate", +]; + +/** + * Asserts a point's content escaped the content area rather than rendering + * inside it. + * + * This is the whole reason `portal` render mode exists: the content area is an + * `overflow: auto` scroll box with its own stacking context, so a floating + * overlay declared inside it would be clipped and trapped under sibling chrome. + * Rendering in the right place is not observable from the component's own + * markup — only from where it landed. + */ +export async function expectPortalled(page: Page, id: ExtensionPointId): Promise { + await expect( + page.locator(`[data-testid="app-content"] [data-testid="vendor-slot-${id}"]`), + "portalled content should not be inside the scrolling content area", + ).toHaveCount(0); + await expect( + page.locator(`body > [data-testid="vendor-slot-${id}"]`), + "portalled content should be mounted at the document root", + ).toHaveCount(1); +} + +/** Asserts a point's content rendered where the slot sits, not at the document root. */ +export async function expectInline( + page: Page, + id: ExtensionPointId, + within: Locator, +): Promise { + await expect(within.locator(`[data-testid="vendor-slot-${id}"]`)).toHaveCount(1); + await expect(page.locator(`body > [data-testid="vendor-slot-${id}"]`)).toHaveCount(0); +} diff --git a/ui/playwright/helpers/grpc.ts b/ui/playwright/helpers/grpc.ts deleted file mode 100644 index 7d915b7d3..000000000 --- a/ui/playwright/helpers/grpc.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { createClient } from "@connectrpc/connect"; -import { createGrpcTransport } from "@connectrpc/connect-node"; -import { - AgentKind, - AgentService, -} from "../../src/generated/kagent/api/v1alpha1/agents_pb"; -import { ModelService } from "../../src/generated/kagent/api/v1alpha1/models_pb"; -import { PromptTemplateService } from "../../src/generated/kagent/api/v1alpha1/prompts_pb"; -import { ToolService } from "../../src/generated/kagent/api/v1alpha1/tools_pb"; - -const DEFAULT_GRPC_URL = "http://127.0.0.1:8084"; -const transport = createGrpcTransport({ - baseUrl: process.env.BACKEND_GRPC_URL ?? DEFAULT_GRPC_URL, - defaultTimeoutMs: 30_000, -}); - -const agentClient = createClient(AgentService, transport); -const modelClient = createClient(ModelService, transport); -const promptTemplateClient = createClient(PromptTemplateService, transport); -const toolClient = createClient(ToolService, transport); - -export interface AgentInfo { - namespace: string; - name: string; - kind: AgentKind; - ready: boolean; - accepted: boolean; -} - -export interface ModelConfigInfo { - ref: string; - model: string; - namespace: string; - name: string; -} - -function completeRef(ref: { namespace: string; name: string } | undefined) { - return ref?.namespace && ref.name ? ref : null; -} - -function splitRef(ref: string): { namespace: string; name: string } | null { - const separator = ref.indexOf("/"); - if (separator <= 0 || separator === ref.length - 1) { - return null; - } - return { namespace: ref.slice(0, separator), name: ref.slice(separator + 1) }; -} - -export async function listAgents(): Promise { - const response = await agentClient.listAgents({ namespace: "" }); - return response.agents.flatMap((agent) => { - const ref = completeRef(agent.ref); - return ref === null - ? [] - : [{ - namespace: ref.namespace, - name: ref.name, - kind: agent.kind, - ready: agent.ready, - accepted: agent.accepted, - }]; - }); -} - -export async function listModelConfigs(): Promise { - const response = await modelClient.listModelConfigs({}); - return response.modelConfigs.flatMap((config) => { - const ref = completeRef(config.ref); - if (ref === null) { - return []; - } - const resource = config.resource?.value; - const spec = resource && typeof resource.spec === "object" && resource.spec !== null - ? resource.spec as Record - : {}; - return [{ - ref: `${ref.namespace}/${ref.name}`, - model: typeof spec.model === "string" ? spec.model : "", - namespace: ref.namespace, - name: ref.name, - }]; - }); -} - -export async function listToolServerRefs(): Promise { - const response = await toolClient.listToolServers({}); - return response.toolServers.map((server) => server.ref).filter(Boolean); -} - -export async function listPromptTemplateRefs(namespace: string): Promise { - const response = await promptTemplateClient.listPromptTemplates({ namespace }); - return response.promptTemplates.flatMap((template) => { - const ref = completeRef(template.ref); - return ref === null ? [] : [`${ref.namespace}/${ref.name}`]; - }); -} - -export async function deleteAgent(agent: Pick): Promise { - const ref = { namespace: agent.namespace, name: agent.name }; - switch (agent.kind) { - case AgentKind.SANDBOX_AGENT: - await agentClient.deleteSandboxAgent({ ref }); - return; - case AgentKind.AGENT_HARNESS: - await agentClient.deleteAgentHarness({ ref }); - return; - default: - throw new Error(`unsupported agent kind: ${agent.kind}`); - } -} - -export async function deleteModelConfig(ref: string): Promise { - const parsed = splitRef(ref); - if (parsed !== null) { - await modelClient.deleteModelConfig({ ref: parsed }); - } -} - -export async function deleteToolServer(ref: string): Promise { - const parsed = splitRef(ref); - if (parsed !== null) { - await toolClient.deleteToolServer({ ref: parsed }); - } -} - -export async function deletePromptTemplate(ref: string): Promise { - const parsed = splitRef(ref); - if (parsed !== null) { - await promptTemplateClient.deletePromptTemplate({ ref: parsed }); - } -} diff --git a/ui/playwright/helpers/mockCalls.ts b/ui/playwright/helpers/mockCalls.ts new file mode 100644 index 000000000..df15a60c2 --- /dev/null +++ b/ui/playwright/helpers/mockCalls.ts @@ -0,0 +1,108 @@ +/** + * Counting what a page asked the backend for. + * + * ## Why not count requests + * + * Because there are none. In mock mode the API is served by a substituted + * `Transport` (`src/mocks/transport.ts`), so an operation never becomes an HTTP + * request: `page.on("request")` sees only the navigation, and a `window.fetch` + * wrapper sees nothing at all. Both instruments read zero whether the page is + * polling or idle. + * + * The mock backend therefore tallies calls per RPC and publishes the tally on the + * page. That is also closer to what these tests mean: "polling refreshed + * everything on this page" is a claim about reads, not about HTTP, and it stays + * true the next time the transport changes. + * + * ## Why a missing key throws + * + * A counter that answered `0` for an RPC name nobody registered would make a + * misspelling look like a page that never read anything — an instrument that fails + * silently in the direction of "nothing happened". The mock seeds a zero for every + * RPC it serves and creates no others, so an absent key means the name is wrong, + * and this helper says so instead of returning a number. + * + * Against a real backend, count requests instead: `playwright/live/**` does, and + * that is still the right instrument there. + */ + +import type { Page } from "@playwright/test"; + +/** Where `src/mocks/transport.ts` publishes its per-RPC counts. */ +const PROPERTY = "__kagentMockCalls"; + +/** + * The RPCs the browser suite watches, spelled as the mock backend keys them. + * + * Named here rather than inline in each spec so a rename is one edit, and so a + * typo in a spec is a compile error rather than a lookup that throws at runtime. + */ +export const rpc = { + listAgents: "kagent.api.v1alpha1.AgentService/ListAgents", + listModelConfigs: "kagent.api.v1alpha1.ModelService/ListModelConfigs", + listToolServers: "kagent.api.v1alpha1.ToolService/ListToolServers", + listPromptTemplates: "kagent.api.v1alpha1.PromptTemplateService/ListPromptTemplates", + listNamespaces: "kagent.api.v1alpha1.SystemService/ListNamespaces", + substrateStatus: "kagent.api.v1alpha1.SystemService/GetSubstrateStatus", + substrateSummary: "kagent.api.v1alpha1.SystemService/GetSubstrateSummary", + substrateActors: "kagent.api.v1alpha1.SystemService/ListSubstrateActors", + substrateWorkers: "kagent.api.v1alpha1.SystemService/ListSubstrateWorkers", + listAgentTemplates: "kagent.api.v1alpha1.AgentTemplateService/ListAgentTemplates", + listAgentInstances: "kagent.api.v1alpha1.AgentInstanceService/ListAgentInstances", + getAgentInstance: "kagent.api.v1alpha1.AgentInstanceService/GetAgentInstance", + suspendAgentInstance: "kagent.api.v1alpha1.AgentInstanceService/SuspendAgentInstance", + resumeAgentInstance: "kagent.api.v1alpha1.AgentInstanceService/ResumeAgentInstance", +} as const; + +export type WatchedRpc = (typeof rpc)[keyof typeof rpc]; + +/** How many times the page has called one RPC since it loaded. */ +export async function operationCalls(page: Page, name: WatchedRpc): Promise { + const counts = await operationCallCounts(page, [name]); + return counts[name]; +} + +/** + * The counts for several RPCs, read together. + * + * One evaluation rather than several, so the numbers describe the same moment — + * which matters when the assertion is about a page that is polling while being + * measured. + */ +export async function operationCallCounts( + page: Page, + names: readonly T[], +): Promise> { + const result = await page.evaluate( + ({ property, names: wanted }) => { + const counts = ( + window as unknown as Record | undefined> + )[property]; + if (!counts) return { error: "absent" as const }; + + const missing = wanted.filter((name) => counts[name] === undefined); + if (missing.length > 0) return { error: "unknown" as const, missing }; + + return { + counts: Object.fromEntries(wanted.map((name) => [name, counts[name]])), + }; + }, + { property: PROPERTY, names: [...names] as string[] }, + ); + + if ("error" in result && result.error === "absent") { + throw new Error( + `The mock backend published no call counts on window.${PROPERTY}. ` + + `Either the app is not running in mock mode, or it had not started when this was read.`, + ); + } + if ("error" in result && result.error === "unknown") { + throw new Error( + `The mock backend serves no such RPC: ${result.missing?.join(", ")}. ` + + `Check the name against src/mocks/transport.ts — a count of zero is never ` + + `reported for an unknown RPC, because that reads as "the page did nothing".`, + ); + } + + return (result as { counts: Record }).counts; +} diff --git a/ui/playwright/helpers/nav.ts b/ui/playwright/helpers/nav.ts index d10a5a1bc..32e6903a7 100644 --- a/ui/playwright/helpers/nav.ts +++ b/ui/playwright/helpers/nav.ts @@ -1,52 +1,56 @@ -// Navigation helpers for the persistent header (src/components/Header.tsx). -// -// Routes live inside two Radix dropdown menus ("Create" and "View"), not flat -// links. Menu items only enter the DOM (as role="menuitem") once the menu is -// open. Header markup is duplicated for desktop/mobile, but the hidden block is -// out of the accessibility tree, so role-based locators resolve to the visible -// one on a desktop viewport. +/** + * Navigation drivers for the persistent shell (`src/components/Structure/**`). + * + * The sidebar is an antd Menu whose entries carry stable test ids (`nav-`), + * and the header's Create menu is a dropdown whose items only enter the DOM once + * it is open. + */ -import { type Page } from "@playwright/test"; +import { expect, type Page } from "@playwright/test"; -// The full-screen LoadingState overlay (data-testid="loading-overlay") sits on -// top of the header during a route transition. Waiting only for the URL to -// change leaves it covering the menu triggers, so a follow-up menu click can hit -// the overlay and flake. Wait for it to detach before handing control back. -// "hidden" also resolves immediately when the overlay never mounted. -async function waitForOverlayGone(page: Page): Promise { - await page.getByTestId("loading-overlay").waitFor({ state: "hidden" }); -} +/** Sidebar entries, keyed as in `src/components/Structure/navItems.ts`. */ +/* + * There is no `agentInstances` entry any more, and that is the change rather than + * an omission: an agent *is* an AgentInstance, so the agents page is the instances + * page. Two entries in the navigation for one idea — one of them naming a resource + * the API does not serve — is what this replaced. + */ +export type NavKey = + | "dashboard" + | "agents" + | "models" + | "mcpServers" + | "prompts" + | "substrate"; -async function openMenu(page: Page, trigger: "Create" | "View"): Promise { - await page.getByRole("button", { name: trigger, exact: true }).click(); -} +export const navLabels: Record = { + dashboard: "Dashboard", + agents: "Agents", + models: "Models", + mcpServers: "MCP Servers", + prompts: "Prompts", + substrate: "Substrate", +}; -async function chooseFrom( +/** Clicks a sidebar entry and waits for the route to change. */ +export async function clickNav( page: Page, - trigger: "Create" | "View", - item: string, - urlGlob?: string | RegExp, + key: NavKey, + expectedUrl: RegExp, ): Promise { - await openMenu(page, trigger); - // Exact match: "New Agent" is a substring of "New Agent Harness". - await page.getByRole("menuitem", { name: item, exact: true }).click(); - if (urlGlob) await page.waitForURL(urlGlob); - await waitForOverlayGone(page); -} - -/** Open the "View" menu and go to a listing page, e.g. gotoView(page, "Models", "**\/models"). */ -export function gotoView(page: Page, item: string, urlGlob?: string | RegExp): Promise { - return chooseFrom(page, "View", item, urlGlob); + await page.getByTestId(`nav-${key}`).click(); + await page.waitForURL(expectedUrl); } -/** Open the "Create" menu and go to a creation page, e.g. gotoCreate(page, "New Agent", "**\/agents/new"). */ -export function gotoCreate(page: Page, item: string, urlGlob?: string | RegExp): Promise { - return chooseFrom(page, "Create", item, urlGlob); +/** Asserts the persistent shell chrome is present. */ +export async function expectShell(page: Page): Promise { + await expect(page.getByTestId("app-header")).toBeVisible(); + await expect(page.getByTestId("app-sidebar")).toBeVisible(); + await expect(page.getByTestId("app-content")).toBeVisible(); } -/** Click the direct "Home" link in the header. */ -export async function gotoHome(page: Page): Promise { - await page.getByRole("link", { name: "Home" }).first().click(); - await page.waitForURL(/\/(agents)?$/); - await waitForOverlayGone(page); +/** Asserts the shell chrome is absent — for routes that render standalone. */ +export async function expectNoShell(page: Page): Promise { + await expect(page.getByTestId("app-header")).toHaveCount(0); + await expect(page.getByTestId("app-sidebar")).toHaveCount(0); } diff --git a/ui/playwright/helpers/page.ts b/ui/playwright/helpers/page.ts deleted file mode 100644 index 4aebdbf59..000000000 --- a/ui/playwright/helpers/page.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Page-level driver helpers. Backend-agnostic — they assert on rendered DOM -// (roles/text), so they work regardless of how data is mocked. - -import { expect, type Locator, type Page } from "@playwright/test"; - -/** - * Wait for the full-screen LoadingState overlay to clear. It sits on top of the - * page (z-10, backdrop-blur) while data loads, so content behind it counts as - * "visible" to Playwright even though a user can't see/use it yet. Call this - * before asserting on or interacting with page content. - */ -export async function waitForAppReady(page: Page): Promise { - await expect(page.getByTestId("loading-overlay")).toHaveCount(0); -} - -/** Navigate to a path, wait for loading to finish, and optionally assert the page's

. */ -export async function loadPage( - page: Page, - path: string, - opts: { heading?: string } = {}, -): Promise { - await page.goto(path); - await waitForAppReady(page); - if (opts.heading) { - await expect(page.getByRole("heading", { level: 1, name: opts.heading })).toBeVisible(); - } -} - -/** Assert the ErrorState component ("Error Encountered") is not on the page. */ -export async function expectNoErrors(page: Page): Promise { - await expect(page.getByText("Error Encountered")).toHaveCount(0); -} - -/** - * Scroll a list row (or any element) into view and assert it's actually within the - * viewport. Use after a mutation to prove the item's changed state is visible on the - * real list — `toBeVisible` alone passes for off-screen rows, so this makes the - * "scrolled into view" guarantee explicit (and keeps the row on-screen in the video). - */ -export async function expectScrolledIntoView(locator: Locator): Promise { - await locator.scrollIntoViewIfNeeded(); - await expect(locator).toBeInViewport(); -} - -type ToastType = "success" | "error" | "warning" | "info"; - -/** - * Assert a sonner toast with the given text is visible. Toasts auto-dismiss, so - * call this promptly after the triggering action. Pass `type` to also assert the - * severity (data-type on the toast
  • ). - */ -export async function expectToast( - page: Page, - text: string | RegExp, - opts: { type?: ToastType } = {}, -): Promise { - const selector = opts.type ? `[data-sonner-toast][data-type="${opts.type}"]` : "[data-sonner-toast]"; - await expect(page.locator(selector).filter({ hasText: text })).toBeVisible(); -} diff --git a/ui/playwright/helpers/resources.ts b/ui/playwright/helpers/resources.ts deleted file mode 100644 index abb6ad975..000000000 --- a/ui/playwright/helpers/resources.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Backend discovery helpers. Specs use these to find a dependency that already -// exists in the cluster (a model config, a ready agent) instead of hard-coding a -// seeded resource by name — so a rename or reshuffle of the seeded set doesn't -// break the suite. Application resources use the controller's gRPC API. - -import { expect } from "@playwright/test"; -import { listAgents, listModelConfigs, type ModelConfigInfo } from "./grpc"; - -/** The first available model config, for suites that need one to attach to an agent. */ -export async function firstModelConfig(): Promise { - const cfg = (await listModelConfigs())[0]; - expect(cfg, "no model config available — the suite needs at least one").toBeTruthy(); - return cfg!; -} - -/** The ref ("namespace/name") of a ready agent, for the chat flow. */ -export async function firstReadyAgent(): Promise { - const items = await listAgents(); - const pick = items.find((a) => a.ready && a.accepted) ?? items[0]; - expect(pick, "no agent available for the chat flow").toBeTruthy(); - return `${pick!.namespace}/${pick!.name}`; -} diff --git a/ui/playwright/helpers/select.ts b/ui/playwright/helpers/select.ts deleted file mode 100644 index acd05a80d..000000000 --- a/ui/playwright/helpers/select.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Driver for Radix `, so this locator *is* the input — and a + // read-only input carrying the value is what proves the same component is being + // used rather than a second view that could drift from it. + const description = page.getByTestId("template-form-description"); + await expect(description).toHaveAttribute("readonly", ""); + await expect(description).toHaveValue( + "Answers questions about workloads in the cluster.", + ); + // And nothing that authors: the add buttons only exist when something can be added. + await expect(page.getByTestId("template-form-add-label")).toHaveCount(0); + }); + + await test.step("3. whether anything will run it is on the page, not behind Edit", async () => { + // The single most important fact about a template, and the one nothing about the + // template itself reveals. A reader who has to press Edit to find it will not. + await expect(page.getByTestId("template-admission-status")).toContainText( + "k8s-agent", + ); + }); + + await test.step("4. the Agents tab lists the pairs this template is half of", async () => { + await page.getByRole("tab", { name: /Agents/ }).click(); + const table = page.getByTestId("template-agents-table"); + // One row per admitting harness. A pair is the durable runnable thing — the + // template alone does nothing and an instance is one conversation with a pair. + await expect(table).toContainText("k8s-agent"); + // Counted, which the pair list alone cannot tell you: a pair exists the moment a + // harness admits the template, whether or not anyone has ever talked to it. So + // "which harnesses run this" and "is anything using this" are different questions, + // and only the second one stops a reader deleting something in use. + // + // This was a seam until `ListAgentInstances` gained `agent_template`/`harness` + // filters. It could not be closed with `match_labels`, even though instances do + // carry labels: admission labels are shared by construction, so filtering on one + // returns every template that harness admits. + await expect(page.getByTestId("template-pair-conversations").first()).toContainText( + /\d+ conversations?/, + ); + }); + + await test.step("5. Edit turns the same fields into the form, in place", async () => { + await page.getByRole("tab", { name: "Details" }).click(); + await page.getByTestId("template-edit").click(); + await expect(page.getByTestId("template-submit")).toBeVisible(); + await expect(page.getByTestId("template-form-description")).not.toHaveAttribute( + "readonly", + "", + ); + // The authoring controls are back, which is what "the same component in two + // modes" means in practice. + await expect(page.getByTestId("template-form-add-label")).toBeVisible(); + }); + + await test.step("6. leaving edit mode with a draft asks before throwing it away", async () => { + await page.getByTestId("template-form-description").fill("Edited by the suite."); + // Visible from either tab, because a draft survives a tab switch and a reader who + // wandered off should still be able to see there is one. + await expect(page.getByTestId("template-unsaved")).toBeVisible(); + + await page.getByTestId("template-stop-editing").click(); + await expect(page.getByTestId("template-discard-body")).toContainText( + "have not been saved", + ); + await page.getByRole("button", { name: "Keep editing" }).click(); + // Kept, not lost — the point of asking. + await expect(page.getByTestId("template-form-description")).toHaveValue( + "Edited by the suite.", + ); + }); + + await test.step("7. saving returns to reading, showing what was saved", async () => { + await page.getByTestId("template-submit").click(); + await expect(page.getByTestId("template-edit")).toBeVisible({ timeout: 30_000 }); + // Read back from the re-read template rather than from the draft: a save that did + // not reach the backend would leave the old value here. + await expect(page.getByTestId("template-form-description")).toHaveValue( + "Edited by the suite.", + ); + }); + + await test.step("8. and the edit did not delete what the form cannot show", async () => { + // `k8s-agent-7f3a91c` carries a `skills` entry, which this form does not author. + // A save built from the fields it shows would remove it, and the API would accept + // that without a word — so the notice standing here is the evidence it survived. + await expect(page.getByTestId("template-form-unshown")).toContainText( + "does not remove them", + ); + }); +}); + +test("agent templates: deleting one says what it costs, and the list is re-read", async ({ + page, +}) => { + await test.step("1. a template with an agent on it says so before deleting", async () => { + await loadPage(page, routes.agentTemplates, { title: "Agents" }); + await expect(dataRows(page).first()).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("template-link-k8s-agent-7f3a91c").click(); + await page.waitForURL(/\/agent-templates\/kagent\/k8s-agent-7f3a91c/); + + // Before opening it: the consequence is nowhere on the page. That is the half of + // this property the confirmation itself cannot demonstrate — a warning a reader can + // walk past on the way to the button is a warning they will walk past. + await expect(page.locator("body")).not.toContainText("keep working"); + + // In the header now, beside Edit and Back, rather than at the foot of the page. A + // destructive action a reader only reaches by scrolling past everything else reads + // as a footnote. + const deleteButton = page.getByTestId("delete-k8s-agent-7f3a91c"); + await expect(deleteButton).toContainText("Delete template"); + await deleteButton.click(); + const consequence = page.getByTestId("template-delete-consequence"); + /* + * Measured against the controller, not read off the schema. A scratch template + * with a live pair was deleted over gRPC on a cluster: the call was accepted, the + * resource went, and the `agent_template_harness_pair` row survived in Postgres + * with `retired_at` set — retired, not removed. The revision collector skips any + * revision an `agent_instance.prepared_revision` points at before the + * `ON DELETE RESTRICT` on that column could fire, so an agent's revision is + * retained *for it*; and `GetLatestRuntimeRevisionForInstance` requires + * `retired_at IS NULL`, which is what stops anything new being cut from the + * template afterwards. + * + * The sentence is asserted here, inside the confirmation, and nowhere else on the + * page. A consequence a reader can walk past on the way to the button is one they + * will walk past; a bare "are you sure?" over an object with live dependents is + * the prompt that gets clicked through. + * + * **What it must not say is that the agents keep running.** A (template, harness) + * pair *is* an agent here, and deleting the template retires the pair — that is + * exactly the mechanism that stops new work. What survives is the conversations + * already open, each holding a revision retained for it. The earlier wording had + * the agent surviving and the conversations unmentioned, which is the same noun + * confusion that put instances on the agents page in the first place. + */ + await expect(consequence).toContainText("1 agent is built from this template"); + await expect(consequence).toContainText("Conversations already open with it keep working"); + await expect(consequence).toContainText("no new one can be started"); + await page.getByRole("button", { name: "Keep" }).click(); + }); + + await test.step("2. a template nothing runs says that instead", async () => { + await page.getByRole("button", { name: "Back to templates" }).click(); + await page.waitForURL(/\/agent-templates(\?|$)/); + await expect(rowNamed(page, "note-taker")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("template-link-note-taker").click(); + await page.waitForURL(/\/agent-templates\/kagent\/note-taker/); + + await page.getByTestId("delete-note-taker").click(); + // Different sentence, because it is a different fact — telling a reader that + // conversations will keep working when no harness ever admitted the template + // would be noise dressed as care. + await expect(page.getByTestId("template-delete-consequence")).toContainText( + "no agent was ever built from it", + ); + }); + + await test.step("3. confirming removes it, and the list that opens does not show it", async () => { + // Scoped to the visible popconfirm: every row's confirmation is in the DOM at + // once, so an unscoped Delete can answer a prompt nobody is looking at. + await page + .locator(".ant-popconfirm:visible") + .getByRole("button", { name: "Delete" }) + .click(); + await page.waitForURL(/\/agent-templates\?/, { timeout: 30_000 }); + + // The claim worth making. The list is cached, so landing on it without re-reading + // shows the template that was just removed — which reads as a delete that + // silently failed, and is the reason the page invalidates before navigating. + await expect(rowNamed(page, "note-taker")).toHaveCount(0, { timeout: 30_000 }); + // And the rest of the list is intact, so "gone" means that one rather than the read. + await expect(rowNamed(page, "k8s-agent-7f3a91c")).toBeVisible(); + }); +}); + + +test("agent templates: the list narrows like every other landing page", async ({ page }) => { + /* + * This page was the odd one out. + * + * It picked a single namespace — `kagent` if it existed, otherwise the first — and + * offered a dropdown to change it. So a template in a namespace the reader had not + * selected was not "filtered out": it had never been read, and nothing on screen said + * so. Every other landing page defaults to all namespaces and narrows from there. + */ + await loadPage(page, routes.agentTemplates, { title: "Agents" }); + await expect(dataRows(page).first()).toBeVisible({ timeout: 30_000 }); + + await test.step("1. all namespaces by default, and no pills", async () => { + await expect(page.getByTestId("templates-filters")).toContainText("All namespaces"); + await expect(page.getByTestId("templates-filters-pills")).toHaveCount(0); + }); + + await test.step("2. search covers every row that was read, not one page of it", async () => { + const total = await dataRows(page).count(); + await page.getByTestId("templates-filters-search").fill("note-taker"); + await expect(dataRows(page)).toHaveCount(1); + await expect(rowNamed(page, "note-taker")).toBeVisible(); + // And the count says what was narrowed from, so "1" cannot be mistaken for "all". + await expect(page.getByTestId("templates-summary")).toContainText(`of ${total}`); + }); + + await test.step("3. the term is in the address, so the view can be sent to somebody", async () => { + expect(page.url()).toContain("note-taker"); + await page.reload(); + await expect(dataRows(page)).toHaveCount(1, { timeout: 30_000 }); + await expect(page.getByTestId("templates-filters-search")).toHaveValue("note-taker"); + }); + + await test.step("4. the page says where its narrowing happens", async () => { + // `ListAgentTemplates` takes no page, sort or search parameter, so this narrowing is + // the browser's. Saying so is what stops a reader assuming a search box searched the + // cluster — the defect the substrate page was fixed for. + await page.getByTestId("templates-filters-search").fill(""); + await expect(page.getByTestId("templates-read-note")).toContainText( + "ListAgentTemplates", + ); + await expect(page.getByTestId("templates-read-note")).toContainText( + "refuses an empty one", + ); + }); + + await test.step("5. columns sort", async () => { + const first = async () => (await dataRows(page).first().textContent()) ?? ""; + const before = await first(); + await page.getByRole("columnheader", { name: /Template/ }).click(); + await expect.poll(first).not.toBe(before); + }); +}); + +test("agent templates: an agent in the Agents tab opens that agent", async ({ page }) => { + // The tab answers "what is built from this template", and each answer is a + // (template, harness) pair — which is what an agent is. Leaving the rows as text made + // it a dead end: it named the thing the reader wanted and gave them no way to reach it. + await loadPage(page, routes.agentTemplates, { title: "Agents" }); + await page.getByTestId("template-link-k8s-agent-7f3a91c").click(); + await page.waitForURL(/\/agent-templates\/kagent\/k8s-agent-7f3a91c/); + + await page.getByRole("tab", { name: /Agents/ }).click(); + await page.getByTestId("template-agent-link-k8s-agent").click(); + + // The agent's own page, addressed as the pair it is. + await page.waitForURL(/\/agents\/kagent\/k8s-agent-7f3a91c\/on\/k8s-agent$/); + // Its own page, offering what an agent offers — a new conversation with it, from the + // rail that page carries. The page itself has no heading or actions row: the rail + // names the agent and holds both. + await expect(page.getByTestId("chat-new-session")).toBeVisible({ timeout: 30_000 }); +}); diff --git a/ui/playwright/tests/agents/agent-chat-entry.spec.ts b/ui/playwright/tests/agents/agent-chat-entry.spec.ts new file mode 100644 index 000000000..a02659b32 --- /dev/null +++ b/ui/playwright/tests/agents/agent-chat-entry.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from "../../fixtures/test"; +import { + agentNewChat, + agents, + expectSettled, + instances, + loadPage, + routes, +} from "../../helpers/app"; + +/** + * Getting from the list of agents to a conversation with one. + * + * Its own spec because the gap it covers is invisible to every other one. The chat + * specs navigate straight to `/agents/:ns/:id/chat`, which is a fair way to test a + * chat and no way at all to test that anything *links* to it — and for a while + * nothing did: the only route to a conversation was an agent card on the dashboard, + * so from the page named after agents there was no way to talk to one, and the whole + * suite stayed green. + * + * So this asserts the journey rather than the destination: start where a reader + * starts, click what they would click, and end up somewhere a message can be typed. + * The chat itself is covered in `chat/chat.spec.ts`. + * + * The journey is two hops now rather than one, and that is the shape being tested: + * the agents list holds agents, and a conversation is inside one. Clicking an agent + * expecting a chat is exactly the mistake this spec exists to catch. + */ + +test("agents: the list is the way in to a conversation, through the agent", async ({ + page, +}) => { + await test.step("1. the agents list offers each agent by name", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expectSettled(page); + + // An agent is named by its template, and the link is the affordance under test: + // matching a cell's text would pass just as well on a name that links nowhere, + // which is the exact bug this spec exists for. + await expect( + page.getByTestId(`agent-link-kagent-${agents.k8s.template}-${agents.k8s.harness}`), + ).toBeVisible(); + }); + + await test.step("2. clicking an agent offers a new conversation, and creates nothing", async () => { + await page + .getByTestId(`agent-link-kagent-${agents.k8s.template}-${agents.k8s.harness}`) + .click(); + // A conversation that does not exist yet, addressed by the agent. It must not open + // somebody's existing chat, and it must not create one: an instance created by a + // click that changes its mind is permanent, holds a prepared revision, and is what + // left nine empty conversations on the live cluster. + await expect(page).toHaveURL(new RegExp(`${agentNewChat(agents.k8s)}$`)); + await expectSettled(page); + await expect(page.getByTestId("new-chat-empty")).toBeVisible(); + await expect(page.getByTestId("chat-input")).toBeVisible(); + }); + + await test.step("3. and a conversation already open with it is one click away", async () => { + // From the rail, which is the point of the rail: arriving at a new conversation does + // not cut the reader off from the ones they already have. The rail is the only + // navigation on this page — there is no table, because there is nothing to tabulate + // until a conversation exists. + await page.getByTestId(`chat-session-${instances.ready}`).click(); + + // That conversation's chat, not a generic one: the namespace and the instance id + // are both in the path, and opening the wrong one is a failure this would catch. + await expect(page).toHaveURL(new RegExp(`/agents/kagent/${instances.ready}/chat$`)); + await expect(page.getByTestId("chat-panel")).toBeVisible(); + }); + + await test.step("4. a message can be typed straight away", async () => { + // The box is there without anything being clicked: the instance *is* the + // conversation, so arriving at one is arriving at somewhere to talk. + await expect(page.getByTestId("chat-input")).toBeVisible(); + // A composer that cannot be typed into is not an arrival worth asserting. + await expect(page.getByTestId("chat-input")).toBeEditable(); + }); + + await test.step("5. the card names the agent; the list names the conversation", async () => { + // The card is what opens the agent switcher, so it names the thing being switched + // — a template and the harness that runs it. It used to name the conversation, + // which meant it changed every time a reader opened a different conversation with + // the same agent while the menu behind it listed agents that never changed. + const identity = page.getByTestId("agent-rail-identity"); + await expect(identity).toContainText(agents.k8s.template); + await expect(identity).toContainText(`on ${agents.k8s.harness}`); + + // The conversation is still named, one row among its siblings, which is where a + // reader picks between them. + await expect(page.getByTestId(`chat-session-${instances.ready}`)).toContainText( + "Tuesday cluster review", + ); + }); + + await test.step("6. starting another goes to the same call to action, creating nothing", async () => { + // "New chat" navigates rather than creating, everywhere it appears. A second + // conversation with one agent is a second instance of the same pair — but it comes + // into existence when a message is sent, not when a button is clicked. + const url = page.url(); + await page.getByTestId("chat-new-session").click(); + + await expect(page).not.toHaveURL(url); + await expect(page).toHaveURL(new RegExp(`${agentNewChat(agents.k8s)}$`)); + await expect(page.getByTestId("new-chat-empty")).toBeVisible(); + }); +}); diff --git a/ui/playwright/tests/agents/agent-conversations.spec.ts b/ui/playwright/tests/agents/agent-conversations.spec.ts new file mode 100644 index 000000000..6d1532c47 --- /dev/null +++ b/ui/playwright/tests/agents/agent-conversations.spec.ts @@ -0,0 +1,510 @@ +import { test, expect } from "../../fixtures/test"; +import { + agentChat, + agentNewChat, + agentPage, + agents, + dataRows, + expectSettled, + instances, + loadPage, + rowNamed, + routes, +} from "../../helpers/app"; + +/** + * One agent, and the conversations people have had with it. + * + * The surface between the agents list and a chat. Three things about it are worth + * pinning, and each is a claim a screenshot cannot check: + * + * - **The list is this agent's conversations, narrowed by the server.** Two agents + * cut from one template must not show each other's, and `ListAgentInstances` takes + * both halves of the pair for exactly that reason. A client-side filter on the + * template alone would pass every visual inspection and merge the two. + * - **A conversation has a name, and an unnamed one is not a bare UUID.** That was + * the thing that made the old list unreadable — rows of hex under a heading — so + * what it degrades to is asserted rather than assumed. + * - **Somebody else's conversation is listed and cannot be opened.** `all_creators` + * is always asked for now, and `GetAgentInstance` is scoped to its creator, so + * such a row is genuinely a dead end. Offering a link into a chat that answers + * `NotFound` would be worse than not listing it at all. + */ + +test("agents: one agent lists its own conversations, and only its own", async ({ + page, +}) => { + await test.step("1. the agents list leads here", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expectSettled(page); + + // Clicked rather than navigated to: what is under test is that the list is a way + // in, which a `page.goto` to the destination could never fail on. + // + // Through the agent's name and then the rail. The name opens a new conversation, + // which is what a reader clicking an agent wants; the agent's own page — what it + // already has — is one step further, reached from the rail that page carries. + await page.getByTestId("agent-link-kagent-shared-brain-k8s-agent").click(); + await page.getByTestId("agent-nav-agent-conversations").click(); + await expect(page).toHaveURL(new RegExp(`${agentPage(agents.sharedOnK8s)}$`)); + await expectSettled(page); + }); + + await test.step("2. the rail names the agent by its template and says what runs it", async () => { + // The rail, not a page heading: this page has none. The rail names the agent, holds + // the way back and offers a new conversation, so a title repeating the name and + // three buttons repeating rail entries were a band across the top saying nothing. + await expect(page.getByTestId("agent-rail-identity")).toContainText("shared-brain"); + await expect(page.getByTestId("agent-identity")).toContainText("k8s-agent"); + }); + + await test.step("3. it lists this pair's conversation and not its twin's", async () => { + // The assertion the server-side filter exists for. `shared-brain` is two agents; + // each has exactly one conversation, and they are indistinguishable on + // everything but the harness. Narrowed on the template alone, both would appear + // here and the page would look perfectly reasonable. + await expect(dataRows(page)).toHaveCount(1); + await expect(rowNamed(page, "Drafting the runbook")).toHaveCount(1); + await expect(page.getByTestId("conversations-table")).not.toContainText("2b6e0c45"); + }); + + await test.step("4. the other agent cut from the same template has the other one", async () => { + await loadPage(page, agentPage(agents.sharedOnFastLane)); + await expectSettled(page); + + await expect(dataRows(page)).toHaveCount(1); + await expect(page.getByTestId("conversations-table")).toContainText("2b6e0c45"); + await expect(page.getByTestId("conversations-table")).not.toContainText( + "Drafting the runbook", + ); + }); + + await test.step("5. and the page says the narrowing was the server's", async () => { + // Because it decides whether "no conversations match" is true. This list is + // paged; a browser-side filter over one page would report an empty agent that + // has forty conversations. + await expect(page.getByTestId("conversations-read-note")).toContainText( + "ListAgentInstances narrows to this agent on the server", + ); + }); +}); + +test("agents: a conversation is named by the reader, and never renders as a bare UUID", async ({ + page, +}) => { + await loadPage(page, agentPage(agents.k8s)); + await expectSettled(page); + + await test.step("1. a named conversation reads as its name", async () => { + await expect(page.getByTestId(`conversation-link-${instances.ready}`)).toHaveText( + "Tuesday cluster review", + ); + }); + + await test.step("2. an unnamed one says it is untitled rather than showing its key", async () => { + const untitled = page.getByTestId(`conversation-link-${instances.suspended}`); + // Not the UUID. A database key presented under a "Conversation" heading reads as + // a name somebody chose, and eight rows of it are indistinguishable at a glance + // — which is the specific failure that started this rework. + await expect(untitled).not.toHaveText(instances.suspended); + await expect(untitled).toContainText("Untitled"); + // The short id is still there: two untitled conversations with one agent have + // nothing else to tell them apart. + await expect(untitled).toContainText(instances.suspended.slice(0, 8)); + }); + + await test.step("3. renaming one changes what the list shows", async () => { + await page.getByTestId(`conversation-rename-${instances.suspended}`).click(); + // The box opens *empty* for an unnamed conversation rather than pre-filled with + // the placeholder, or clearing a title would be impossible: saving would turn an + // honest "Untitled" into a literal one. + const field = page.getByTestId("conversation-rename-input").locator("input"); + await expect(field).toHaveValue(""); + + await field.fill("Rollback rehearsal"); + await page.getByRole("button", { name: "Save" }).click(); + + // The list is the proof, not the toast: a success message says the app thinks it + // worked, and a rename that failed would still show one on a broken backend. + await expect(rowNamed(page, "Rollback rehearsal")).toHaveCount(1); + await expect( + page.getByTestId(`conversation-link-${instances.suspended}`), + ).toHaveText("Rollback rehearsal"); + }); + + await test.step("4. a name the controller would refuse is refused before the round trip", async () => { + await page.getByTestId(`conversation-rename-${instances.suspended}`).click(); + const field = page.getByTestId("conversation-rename-input").locator("input"); + await field.fill(" leading space"); + + // Refused rather than trimmed, which is what the controller does — and the + // reason matters: silently rewriting what somebody typed reads on screen as a + // rename that did not take. + await expect(page.getByTestId("conversation-rename-problem")).toContainText( + "cannot start or end with a space", + ); + await expect(page.getByRole("button", { name: "Save" })).toBeDisabled(); + await page.getByRole("button", { name: "Cancel" }).click(); + }); + + await test.step("5. clearing a name puts it back to being untitled", async () => { + await page.getByTestId(`conversation-rename-${instances.suspended}`).click(); + const field = page.getByTestId("conversation-rename-input").locator("input"); + await expect(field).toHaveValue("Rollback rehearsal"); + await field.fill(""); + await page.getByRole("button", { name: "Save" }).click(); + + await expect( + page.getByTestId(`conversation-link-${instances.suspended}`), + ).toContainText("Untitled"); + }); +}); + +/** + * Auto-titling, which is the other half of naming a conversation. + * + * Its own test rather than a step, because it needs a different seed state: a + * conversation nobody has named that nonetheless has something said in it. Every + * other seeded transcript belongs to a *named* conversation, where the stored name + * wins and this path is unreachable. + * + * And it is asserted on the chat page deliberately. Deriving a title needs the + * conversation's transcript; this page has it because it is rendering it, while a + * *list* would pay a read per row to do the same — so a list falls back to the id + * and says so. Claiming otherwise would be promising a feature that costs a round + * trip per row to deliver. + */ +test("agents: an unnamed conversation is titled from its first message where that is free", async ({ + page, +}) => { + await loadPage(page, agentChat("2b6e0c45-8a71-4f39-9d02-3c85f1a7e6d0")); + await expect(page.getByTestId("chat-panel")).toBeVisible(); + + // In the conversation list, which is where conversations are named. The card above + // it names the *agent* — the pair being switched between — not this conversation. + const row = page.getByTestId("chat-session-2b6e0c45-8a71-4f39-9d02-3c85f1a7e6d0"); + await expect(row).toContainText("Summarise last night's deploy"); + // A title, not the message: cut at a word boundary with an ellipsis, which is what + // says it is a summary rather than the text itself. + await expect(row).toContainText("…"); + // And emphatically not the id, which is what an unnamed conversation falls back to + // when there is nothing said in it to derive from. + await expect(row).not.toContainText("Untitled"); +}); + +/** + * Item 7: the "include agents created by others" toggle is gone, and the consequence + * of removing it is handled rather than hidden. + * + * Always asking for `all_creators` is the easy half. The hard half is that an + * instance is scoped to its creator on *read* — `GetAgentInstance` resolves through + * `WHERE namespace = $1 AND id = $2 AND user_id = $3`, and the A2A gateway reads it + * through that same call — so a conversation somebody else started is listable and + * genuinely not openable. This is what that has to look like. + */ +test("agents: somebody else's conversation is listed, and plainly cannot be opened", async ({ + page, +}) => { + await loadPage(page, agentPage(agents.k8s)); + await expectSettled(page); + + await test.step("1. the toggle and its alert are gone", async () => { + await expect(page.getByTestId("instances-all-creators")).toHaveCount(0); + await expect(page.getByTestId("instances-own-only")).toHaveCount(0); + }); + + await test.step("2. everyone's conversations are listed", async () => { + // Four: two the caller started and two somebody else did. Without `all_creators` + // this would be two, and a shared agent would look half idle. + await expect(dataRows(page)).toHaveCount(4); + await expect(page.getByTestId("conversations-table")).toContainText( + "bob@example.com", + ); + }); + + await test.step("3. and the ones that are not the reader's carry no link", async () => { + // The point of the whole step: no anchor, so nothing invites a click into a chat + // that will answer NotFound. A link here would be worse than not listing the row. + await expect( + page.getByTestId(`conversation-link-${instances.someoneElses}`), + ).toHaveCount(0); + await expect( + page.getByTestId(`conversation-unopenable-${instances.someoneElses}`), + ).toBeVisible(); + // Still named, so it reads as a conversation rather than as a row that failed to + // render. + await expect( + page.getByTestId(`conversation-unopenable-${instances.someoneElses}`), + ).toHaveText("Search relevance spike"); + }); + + await test.step("4. the page says why, once, so the missing link reads as a rule", async () => { + const note = page.getByTestId("conversations-others-note"); + await expect(note).toBeVisible(); + await expect(note).toContainText("started by somebody else"); + // The mechanism, in the words a reader can act on: a share link is the way in. + await expect(note).toContainText("share link"); + }); + + await test.step("5. renaming and deleting are refused for the same reason", async () => { + // Both writes resolve the instance through the creator exactly as the read does, + // so offering them and then failing would be worse than plainly not offering. + await expect( + page.getByTestId(`conversation-rename-${instances.someoneElses}`), + ).toBeDisabled(); + await expect( + page.getByTestId(`conversation-rename-${instances.ready}`), + ).toBeEnabled(); + }); + + await test.step("6. and opening one directly says so in the same terms", async () => { + // The claim above is only worth making if it is what the backend actually does. + // This is the same conversation, addressed directly. + await loadPage(page, `/agents/kagent/${instances.someoneElses}`, { scenario: "ok" }); + const missing = page.getByTestId("instance-not-found"); + await expect(missing).toBeVisible(); + await expect(missing).toContainText("not found"); + }); +}); + +/** + * Item 3: navigation between an agent, its template and its conversations. + * + * As originally written the item asked for "the agents page filtered by that + * template", which is circular under this shape — that filter *is* what an agent's + * page shows. So what is left is a chain of links, and this walks it in both + * directions. + */ +test("agents: an agent links to its template, and a conversation links up to its agent", async ({ + page, +}) => { + await test.step("1. an agent's page links to the template it is cut from", async () => { + await loadPage(page, agentPage(agents.k8s)); + await expectSettled(page); + + // To the template itself, because a template is a real object a reader may want + // to change — and it is the half of the pair that this build can edit. + await expect(page.getByTestId("agent-template-link")).toHaveAttribute( + "href", + `/agent-templates/kagent/${agents.k8s.template}`, + ); + // Said on the page, because it is the thing a reader gets wrong: editing the + // template reaches every agent cut from it, not only this one. + await expect(page.getByTestId("agent-identity-note")).toContainText( + "every agent cut from it", + ); + }); + + await test.step("2. a conversation opens its chat", async () => { + await page.getByTestId(`conversation-link-${instances.ready}`).click(); + await expect(page).toHaveURL(new RegExp(`/agents/kagent/${instances.ready}/chat$`)); + // Arrived somewhere a message can be typed, which is what opening a conversation + // is for. A route that resolved but rendered no composer would pass a URL check. + await expect(page.getByTestId("chat-input")).toBeEditable(); + }); + + await test.step("3. and links back up to its agent from the rail", async () => { + await page.getByTestId("agent-nav-agent-conversations").click(); + await expect(page).toHaveURL(new RegExp(`${agentPage(agents.k8s)}$`)); + await expectSettled(page); + await expect(dataRows(page)).toHaveCount(4); + }); + + await test.step("4. the conversation's own record links up too", async () => { + await loadPage(page, `/agents/kagent/${instances.ready}`, { scenario: "ok" }); + await expectSettled(page); + + await expect(page.getByTestId("instance-agent-link")).toHaveAttribute( + "href", + agentPage(agents.k8s), + ); + await expect(page.getByTestId("instance-template-link")).toHaveAttribute( + "href", + `/agent-templates/kagent/${agents.k8s.template}`, + ); + }); +}); + +/** + * Starting a conversation, which is what "New chat" on an agent does. + * + * The whole story in one journey, because a create verified against anything but the + * list is a create that passes with a broken backend: make it, come back, count. + */ +test("agents: a conversation is created by its first message, not by the click", async ({ + page, +}) => { + /* + * The behaviour this inverts, and why. + * + * "New chat" used to call `CreateAgentInstance` and navigate to the result, so an + * instance existed the moment somebody clicked — and every visit that changed its mind + * left an empty conversation behind for good. That is measured, not feared: the live + * cluster accumulated nine of them, all unnamed, none with a single message, and the + * worker pool ran out twice in one afternoon because of it. An instance is not free — + * it holds a prepared revision, and deleting the last instance referencing a revision + * does not collect it. + * + * So the click opens a page, and the first message creates the conversation. + */ + let before = 0; + + await test.step("1. the list is read first, so the count means something", async () => { + await loadPage(page, agentPage(agents.k8s)); + await expect(dataRows(page).first()).toBeVisible({ timeout: 30_000 }); + await expectSettled(page); + before = await dataRows(page).count(); + expect(before).toBeGreaterThan(0); + }); + + await test.step("2. the click opens a conversation that does not exist yet", async () => { + await page.getByTestId("chat-new-session").click(); + // Addressed by the *agent*, because there is no instance to address it by — which + // is the whole point. An id in this URL would mean something had been created. + await page.waitForURL(new RegExp(`${agentNewChat(agents.k8s)}$`), { timeout: 30_000 }); + await expect(page.getByTestId("new-chat-empty")).toBeVisible(); + await expect(page.getByTestId("chat-input")).toBeEditable(); + }); + + await test.step("3. leaving without sending creates nothing", async () => { + // The assertion the old behaviour could not pass, and the reason for the change. + // Back through the rail, which is how a reader who changed their mind leaves. + await page.getByTestId("agent-nav-agent-conversations").click(); + await expectSettled(page); + await expect(dataRows(page)).toHaveCount(before, { timeout: 30_000 }); + }); + + await test.step("4. sending creates it, and lands in it", async () => { + await page.getByTestId("chat-new-session").click(); + await page.waitForURL(new RegExp(`${agentNewChat(agents.k8s)}$`), { timeout: 30_000 }); + await page.getByTestId("chat-input").fill("Why is checkout crashlooping?"); + await page.getByTestId("chat-send").click(); + + // Now there is an id, because now there is a conversation. + await page.waitForURL(/\/agents\/kagent\/[0-9a-f-]{36}\/chat$/, { timeout: 30_000 }); + await expect(page.getByTestId("new-chat-error")).toHaveCount(0); + // And the message that created it is in the transcript rather than lost in the + // navigation — it is handed to the chat page and sent there, so the reader sees + // their own words in the conversation they are going to keep reading. + await expect(page.getByTestId("chat-message").first()).toContainText( + "Why is checkout crashlooping?", + { timeout: 30_000 }, + ); + }); + + await test.step("5. and the agent's list is the proof, with one more row", async () => { + // Back through the rail rather than by reloading: the fixture backend keeps writes + // in the page's own memory, so a full page load would start a backend that has + // never heard of this conversation. + await page.getByTestId("agent-nav-agent-conversations").click(); + await expectSettled(page); + // One more, not "at least one more" — and one, not two, which is what a request id + // minted per send rather than per draft would have produced. + await expect(dataRows(page)).toHaveCount(before + 1, { timeout: 30_000 }); + }); + + await test.step("6. an agent with no ready revision cannot start one, and says why", async () => { + await loadPage(page, agentPage(agents.preparing)); + await expectSettled(page); + + // `CreateAgentInstance` answers FailedPrecondition for a pair with no successful + // revision. That used to be a tooltip on a disabled button, which is a reason a + // reader only finds by hovering the thing they were about to give up on; it is an + // alert on the page now, carrying the controller's own words. + const blocked = page.getByTestId("agent-cannot-start"); + await expect(blocked).toBeVisible(); + await expect(blocked).toHaveAttribute("data-blocked-reason", /golden snapshot/); + }); +}); + +test("agents: deleting an agent says what goes with it, and takes both halves", async ({ + page, +}) => { + /* + * There is nothing to delete called "an agent". + * + * An agent is a (template, harness) pair, and the pair is *derived* — the controller + * materialises it from admission and retires it when the labels stop matching. So + * deleting an agent means deleting its template, which retires the pair and stops new + * conversations, plus the conversations already open, which are separate rows that + * outlive it and would otherwise be left running against a retired pair with nothing + * describing them. + * + * The order matters and is asserted by the outcome rather than by spying: the + * conversations go first, because a conversation whose pair is already retired still + * runs and would be stranded. + */ + await loadPage(page, agentPage(agents.k8s)); + await expect(dataRows(page).first()).toBeVisible({ timeout: 30_000 }); + await test.step("1. the prompt counts what will be destroyed", async () => { + await page.getByTestId(`delete-${agents.k8s.template} on ${agents.k8s.harness}`).click(); + const consequence = page.getByTestId("agent-delete-consequence"); + await expect(consequence).toBeVisible(); + // Counted, not "some": a reader deciding this needs to know whether they are + // throwing away one conversation or thirty. + await expect(consequence).toContainText("of your conversations will be deleted"); + // And split, because the two halves have different outcomes. An instance is scoped + // to its creator on write as well as read, so somebody else's cannot be deleted + // from here and keeps running — saying so is what stops "delete agent" reading as + // a promise it cannot keep. + await expect(consequence).toContainText("cannot be deleted from here"); + // And what happens to the agent itself, which depends on whether anything is left. + // These fixtures include conversations started by somebody else, so the mapping + // stays: deleting it would retire the pair and leave those running with nothing + // describing them, which is not ours to do to tidy up an agent they did not ask to + // delete. + await expect(consequence).toContainText("the agent stays"); + // And why it matters beyond tidiness. + await expect(consequence).toContainText("releases the workers"); + }); + + await test.step("2. confirming removes this reader's conversations", async () => { + // Scoped to the open popconfirm: every row carries a delete, so an unscoped match + // answers a prompt nobody is looking at. + await page.locator(".ant-popover:visible").getByRole("button", { name: "Delete" }).click(); + await page.waitForURL(/\/agents(\?|$)/, { timeout: 30_000 }); + await expectSettled(page); + // The agent is still listed, because somebody else's conversations are still under + // it. It goes when nothing is. + await expect(rowNamed(page, agents.k8s.template)).toHaveCount(1, { timeout: 30_000 }); + }); +}); + +test("agents: conversations can be picked and deleted together from the table too", async ({ + page, +}) => { + /* + * The rail offers this, so the table does too: a reader clearing out an agent does it + * from whichever surface they are on, and one that offers it while the other does not + * is a difference they have to learn. + */ + await loadPage(page, agentPage(agents.k8s)); + await expect(dataRows(page).first()).toBeVisible({ timeout: 30_000 }); + + await test.step("1. somebody else's conversation cannot be ticked", async () => { + // An instance is scoped to its creator on write as well as read, so a checkbox + // beside somebody else's would be offering a delete that is refused. + const disabled = page.locator( + "tbody tr td.ant-table-selection-column input:disabled", + ); + await expect(disabled.first()).toBeVisible(); + }); + + await test.step("2. picking one offers the bulk action, counted", async () => { + await page + .locator("tbody tr td.ant-table-selection-column input:not(:disabled)") + .first() + .check(); + await expect(page.getByTestId("conversations-bulk-bar")).toContainText( + "1 conversation selected", + ); + }); + + await test.step("3. and deleting says what goes with it", async () => { + await page.getByTestId("delete-1 selected").click(); + const prompt = page.locator(".ant-popover:visible"); + await expect(prompt).toContainText("can be recovered"); + // The reason it matters here rather than only being tidy. + await expect(prompt).toContainText("workers they hold"); + }); +}); diff --git a/ui/playwright/tests/agents/agents-errors.spec.ts b/ui/playwright/tests/agents/agents-errors.spec.ts index 0091629c9..e0d4e2b68 100644 --- a/ui/playwright/tests/agents/agents-errors.spec.ts +++ b/ui/playwright/tests/agents/agents-errors.spec.ts @@ -1,29 +1,133 @@ import { test, expect } from "../../fixtures/test"; -import { loadPage } from "../../helpers/page"; +import { + agentPage, + agents, + dataRows, + loadPage, + rowNamed, + routes, +} from "../../helpers/app"; +import { operationCalls, rpc } from "../../helpers/mockCalls"; -// Agents — error journey. Required-field validation on both create forms; these -// hold client-side, so no backend resources are created. +/** + * Agents — the error journey. + * + * Two failure modes worth pinning: that the page says so rather than going blank, + * and that it does not quietly report "there are no agents" when the truth is that + * it could not find out. + * + * The list is `AgentTemplateService/ListAgentTemplates` now, because an agent is a + * template paired with a harness and `status.harnesses[]` carries every pair. That + * is asserted below rather than assumed: naming the failing call is what makes the + * message actionable, and it also pins which service this page reads — which changed + * with the model. + * + * **It reads two services, and the order matters when both fail.** Templates are read + * one namespace at a time, because `ListAgentTemplates` validates its namespace first + * and refuses an empty one rather than treating it as a wildcard. So `ListNamespaces` + * is an *input* to the template read, not a nicety beside it: when it fails there are + * no namespaces to iterate, the template read never runs, and a page that reported only + * `templates.error` would sit at "no agents" — an empty state describing a backend that + * was never asked. This scenario fails everything, so the alert correctly names the call + * that actually failed, which is the namespace one. Step 5 covers the other order. + */ -test("agents: create validation", async ({ page }) => { - // region Creating — client-side validation blocks the POST - await test.step("declarative create blocks submit + shows validation errors", async () => { - await loadPage(page, "/agents/new", { heading: "New Agent" }); +test("agents: a failed load is reported, not disguised as an empty list", async ({ + page, +}) => { + await test.step("1. the failure is on screen and names what went wrong", async () => { + await loadPage(page, routes.agents, { scenario: "error", title: "Agents" }); - await page.getByRole("button", { name: "Create Agent" }).click(); + const alert = page.getByTestId("agents-error"); + await expect(alert).toBeVisible(); + await expect(alert).toContainText("Could not load agents"); + // The backend's own account of the failure reaches the reader rather than a + // generic message, and it names the call that failed. Asserted as that property + // rather than as a literal status: a gRPC error is an HTTP 200, so there is no + // status to report and putting one back to satisfy a string match would be + // fitting the product to a stale test. + await expect(alert).toContainText("asked to fail"); + // The call that actually failed, not the one the page is *about*. Reporting + // "could not list templates" when the namespaces read is what broke sends the + // reader to the wrong service. + await expect(alert).toContainText("SystemService/ListNamespaces"); + }); + + await test.step("2. it is not mistaken for an empty list", async () => { + // Every wording the empty state can take, because reporting a failed read as + // "there are none" is the specific bug this step exists for. + await expect(page.getByText(/No agents/)).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(0); + }); + + await test.step("3. no stale data is left on screen from before the failure", async () => { + await expect(rowNamed(page, agents.k8s.template)).toHaveCount(0); + }); + + await test.step("4. retrying asks the backend again", async () => { + // Counted as an operation: a retry issues no HTTP request under the substituted + // transport, so counting requests would report "no retry happened" for a retry + // that demonstrably did. + // + // The namespace call, because that is the one that failed and the one the + // template read is waiting on. Counting `ListAgentTemplates` would assert that a + // retry re-ran a read which never ran in the first place, and would fail for a + // retry that worked. + const before = await operationCalls(page, rpc.listNamespaces); - // Scroll the first error in so it's on screen (in the recorded video). - const descError = page.getByText("Description is required"); - await descError.scrollIntoViewIfNeeded(); - await expect(descError).toBeVisible(); - await expect(page.getByText("Please select a model")).toBeVisible(); - await expect(page).toHaveURL(/\/agents\/new/); + await page + .getByTestId("agents-error") + .getByRole("button", { name: "Try again" }) + .click(); + await expect + .poll(() => operationCalls(page, rpc.listNamespaces), { timeout: 10_000 }) + .toBeGreaterThan(before); + // Still failing, so the message stays put rather than flickering away. + await expect(page.getByTestId("agents-error")).toBeVisible(); }); - await test.step("harness create blocks submit when required fields are empty", async () => { - await loadPage(page, "/agents/new-harness", { heading: "New Agent Harness" }); + await test.step("5. the page recovers once the backend does", async () => { + await loadPage(page, routes.agents, { scenario: "ok", title: "Agents" }); + await expect(page.getByTestId("agents-error")).toHaveCount(0); + await expect(rowNamed(page, agents.k8s.template)).toHaveCount(1); + }); +}); + +/** + * An agent's own page, when its reads fail. + * + * Its two reads answer different questions — what this agent *is*, and what has been + * said to it — and either can fail alone. Reporting one as the other is what this + * covers: an agent whose conversations could not be read is not an agent with no + * conversations. + */ +test("agents: an agent whose conversations cannot be read says so, and is not empty", async ({ + page, +}) => { + await test.step("1. the failure names the read that failed", async () => { + await loadPage(page, agentPage(agents.k8s), { scenario: "error" }); - await page.getByRole("button", { name: "Create harness" }).click(); + const alert = page.getByTestId("conversations-error"); + await expect(alert).toBeVisible(); + await expect(alert).toContainText("AgentInstanceService/ListAgentInstances"); + }); + + await test.step("2. and it is not reported as an agent nobody has talked to", async () => { + // The distinction the whole page turns on: "no conversations yet" invites + // starting one, and would be a lie about an agent with forty. + await expect(page.getByText(/No conversations with this agent yet/)).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(0); + }); - await expect(page).toHaveURL(/\/agents\/new-harness/); + await test.step("3. an address for an agent whose template is gone says which half is missing", async () => { + // A real state rather than a 404: a template can be deleted while the + // conversations cut from it keep running, because an instance runs from the + // prepared revision it was built against. + await loadPage(page, agentPage({ template: "was-deleted", harness: "k8s-agent" }), { + scenario: "ok", + }); + const missing = page.getByTestId("agent-template-missing"); + await expect(missing).toBeVisible(); + await expect(missing).toContainText("keep running"); }); }); diff --git a/ui/playwright/tests/agents/agents.spec.ts b/ui/playwright/tests/agents/agents.spec.ts index 2508c1e72..7e750050a 100644 --- a/ui/playwright/tests/agents/agents.spec.ts +++ b/ui/playwright/tests/agents/agents.spec.ts @@ -1,84 +1,360 @@ import { test, expect } from "../../fixtures/test"; -import { loadPage, expectScrolledIntoView } from "../../helpers/page"; -import { selectOption, selectNamespace } from "../../helpers/select"; -import { firstModelConfig } from "../../helpers/resources"; - -// Agents — full-CRUD lifecycle journey. Creates a uniquely-named declarative -// agent, reads it back on the edit page, updates its description, and deletes it — -// only ever touching the agent it created. The model config it attaches is -// discovered at runtime (firstModelConfig) rather than hard-coded, and an agent is -// only valid in the model config's namespace, so the agent is created there. -// Error journeys live in agents-errors.spec.ts. - -const DESCRIPTION = "e2e declarative agent"; -const UPDATED_DESCRIPTION = "e2e declarative agent (edited)"; - -async function openEdit(page: import("@playwright/test").Page, ref: string) { - await page.getByTestId(`agent-options-${ref}`).first().click(); - await page.getByRole("menuitem", { name: "Edit" }).click(); - await expect(page.getByRole("heading", { level: 1, name: "Edit Agent" })).toBeVisible(); -} - -// This agent's card on the list (default grid view). Scoped by the uniquely-ref'd -// options button so assertions read THIS agent's card — not a neighbour's — and can -// check the description text the card renders (AgentCard.tsx). -function agentCard(page: import("@playwright/test").Page, ref: string) { - return page.locator("div.rounded-xl", { has: page.getByTestId(`agent-options-${ref}`) }); -} - -test("agents: create, read, update, delete", async ({ page }, testInfo) => { - const { ref: modelRef, model, namespace } = await firstModelConfig(); - const modelOption = `${model} (${modelRef})`; - const name = `e2e-agent-${Date.now().toString(36)}-${testInfo.retry}`; - const ref = `${namespace}/${name}`; - - // region Creating — fill the form and POST a new declarative agent - await test.step("creates a declarative agent", async () => { - await loadPage(page, "/agents/new", { heading: "New Agent" }); - - await page.getByLabel("Agent name").fill(name); - await page.getByLabel("Description").fill(DESCRIPTION); - await selectNamespace(page, "#agent-field-namespace", namespace); - await selectOption(page, "#agent-field-model", modelOption); - - await page.getByRole("button", { name: "Create Agent" }).click(); - // Verify the create on the actual agents list: the new card is present (scrolled - // into view) and shows the description we submitted. - await expect(page).toHaveURL(/\/agents(\?|$)/); - const card = agentCard(page, ref); - await expectScrolledIntoView(card); - await expect(card).toContainText(DESCRIPTION); - }); - - // region Reading — reopen the agent on its edit page and read the stored spec - await test.step("reads the agent back on its edit page", async () => { - await openEdit(page, ref); - // The edit form loads the stored spec — proof the create persisted. - await expect(page.getByLabel("Description")).toHaveValue(DESCRIPTION); - }); - - // region Updating — change the description, save (PUT), and confirm it persisted - await test.step("updates the agent description", async () => { - await page.getByLabel("Description").fill(UPDATED_DESCRIPTION); - await page.getByRole("button", { name: "Save Changes" }).click(); - await expect(page).toHaveURL(/\/agents(\?|$)/); - - // Confirm the update on the actual agents list: reload the list and assert the - // card now shows the edited description (scrolled into view). - await loadPage(page, "/agents", { heading: "Agents" }); - const card = agentCard(page, ref); - await expectScrolledIntoView(card); - await expect(card).toContainText(UPDATED_DESCRIPTION); - }); - - // region Deleting — remove the agent and confirm the card is gone - await test.step("deletes the agent", async () => { - await page.getByTestId(`agent-options-${ref}`).first().click(); - await page.getByRole("menuitem", { name: "Delete" }).click(); - const dialog = page.getByRole("alertdialog"); - await expect(dialog).toBeVisible(); - await dialog.getByRole("button", { name: "Delete" }).click(); - // Confirm the delete on the actual agents list: the card for this agent is gone. - await expect(page.getByTestId(`agent-options-${ref}`)).toHaveCount(0); +import { + agentNewChat, + agents, + dataRows, + expectSettled, + loadPage, + rowNamed, + routes, +} from "../../helpers/app"; + +/** + * Agents — what can be run, and what each one is. + * + * An agent is an `AgentTemplate` paired with a `Harness`. An `AgentInstance` is one + * *conversation* with an agent, not an agent — the A2A gateway files every task + * under the instance as the task's `contextId`, so an instance holds a single thread + * of turns. This page used to list those, under a heading that said "Agents". + * + * ## What this covers, and why each thing is here + * + * The properties a page cannot show you it got wrong: + * + * - **A row is a pair.** One template admitted by two harnesses is two agents, and + * the fixtures carry exactly that case. A page keyed on the template would render + * one row, look entirely correct, and merge two agents' conversations. + * - **A template nothing admits is no agent.** It reaches no prepared revision and + * every `CreateAgentInstance` naming it is refused, so listing it as a runnable + * agent would be listing something that cannot run. + * - **Selecting no namespace means all of them**, which is one state rather than two + * controls that could disagree — the toggle-beside-a-single-select this replaced. + * - **The revision state is three answers, not two.** "Preparing" is not a failure + * and "not reported" is not one either. + * + * ## Why the assertions read the state and not the wording + * + * The revision tag carries its state in `data-revision-state` beside its label. The + * wording is a product decision and may change; the state is derived from the + * controller's status and may not. Asserting the value means a rename is a + * deliberate edit here rather than a broken suite, while a row showing the wrong + * state still fails. + */ + +test("agents: the list is agents, and an agent is a template paired with a harness", async ({ + page, +}) => { + await test.step("1. every namespace by default, with no toggle to say so", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expectSettled(page); + + // Five agents across two namespaces, which a page scoped to one could not show. + // The old "all namespaces" switch is gone: nothing selected *is* everything, so + // there is one control rather than two that could contradict each other. + // + // Six rows, because the fixtures also hold conversations whose pair no longer + // exists and those are gathered under a stand-in row — see the dedicated test + // below for why that row is there and when it is not. + await expect(dataRows(page)).toHaveCount(6); + await expect(page.getByTestId("agents-table")).toContainText("analytics"); + await expect(page.getByTestId("instances-all-namespaces")).toHaveCount(0); + }); + + await test.step("2. a template two harnesses admit is two agents, told apart by the harness", async () => { + // The load-bearing assertion of this whole page. `shared-brain` carries one + // label each of two harnesses selects on, so the controller materialises two + // pairs with two revisions — and they share a name, which is exactly why the + // harness is a column rather than a detail. + const shared = rowNamed(page, "shared-brain"); + await expect(shared).toHaveCount(2); + + const harnesses = await shared + .locator("[data-testid^='agent-harness-']") + .allInnerTexts(); + expect(harnesses.sort()).toEqual(["fast-lane", "k8s-agent"]); + }); + + await test.step("3. each row links to its own agent, not to a shared one", async () => { + // Two links to two addresses. A page keyed on the template would produce the same + // href twice and the two rows would be the same page. + // + // The destination is a conversation that does not exist yet, which is what clicking + // an agent's name is for. It creates nothing until a message is sent; the agent's + // own page — `agentPage` — is reached from the Conversations control instead. + await expect( + page.getByTestId("agent-link-kagent-shared-brain-k8s-agent"), + ).toHaveAttribute("href", agentNewChat(agents.sharedOnK8s)); + await expect( + page.getByTestId("agent-link-kagent-shared-brain-fast-lane"), + ).toHaveAttribute("href", agentNewChat(agents.sharedOnFastLane)); + }); + + await test.step("4. a template no harness admits is not listed as an agent", async () => { + // `note-taker` has no labels, so nothing admits it, so it reaches no prepared + // revision and cannot be run. It is a template, and the templates page is where + // that is said — listing it here would offer a "New chat" that cannot succeed. + await expect(rowNamed(page, "note-taker")).toHaveCount(0); + }); + + await test.step("5. the revision state is three answers, and 'preparing' is not a failure", async () => { + await expect( + page.getByTestId(`agent-revision-kagent/${agents.k8s.template}/${agents.k8s.harness}`), + ).toHaveAttribute("data-revision-state", "ready"); + + // Admitted, with a desired revision and none successful yet, because its harness + // has not reported ready. A page that rendered this the same as a failure would + // send a reader looking for a broken template. + const preparing = page.getByTestId( + `agent-revision-kagent/${agents.preparing.template}/${agents.preparing.harness}`, + ); + await expect(preparing).toHaveAttribute("data-revision-state", "preparing"); + await expect(preparing).toHaveText("Preparing"); + }); + + await test.step("6. each agent carries a count of the conversations people have had with it", async () => { + // Counted with `all_creators`, so it is what the agent is doing rather than what + // this reader has done with it. Four for the k8s agent: two of the caller's own + // and two of somebody else's. + await expect( + page.getByTestId( + `agent-conversations-kagent/${agents.k8s.template}/${agents.k8s.harness}`, + ), + ).toHaveText("4 conversations"); + // One each for the two agents `shared-brain` is — which is the count being per + // *pair* rather than per template. Split the other way it would read 2 and 0. + await expect( + page.getByTestId( + `agent-conversations-kagent/${agents.sharedOnK8s.template}/${agents.sharedOnK8s.harness}`, + ), + ).toHaveText("1 conversation"); + await expect( + page.getByTestId( + `agent-conversations-kagent/${agents.sharedOnFastLane.template}/${agents.sharedOnFastLane.harness}`, + ), + ).toHaveText("1 conversation"); + }); + + await test.step("7. a conversation belonging to no pair is said out loud, not dropped", async () => { + // The fixture instance with no harness and no template belongs to no agent, so + // it appears under none of them — which would otherwise be a conversation that + // silently vanished from the product. Deleting a template produces the same + // state on a cluster, and the conversation keeps running. + await expect(page.getByTestId("agents-orphaned-conversations")).toBeVisible(); + }); + +}); + +/** + * The filter bar, on the page whose old controls it replaces. + * + * The shared component is unit-tested and covered on the other three lists; what is + * asserted here is the behaviour that used to be two contradictory controls — a + * single-select namespace beside an "all namespaces" toggle, where choosing a + * namespace and leaving the toggle on left the reader unsure which won. + */ +test("agents: selecting no namespace means every namespace, and a pill undoes one", async ({ + page, +}) => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expectSettled(page); + + await test.step("1. nothing selected is every namespace", async () => { + // Five agents plus the stand-in row for conversations that belong to none of them. + await expect(dataRows(page)).toHaveCount(6); + // No pills, because nothing is narrowing: a pill row over an unfiltered list + // would be a control saying something is hidden when nothing is. + await expect(page.getByTestId("agents-filters-pills")).toHaveCount(0); + }); + + await test.step("2. choosing one narrows the list and shows it as a pill", async () => { + await page.getByTestId("agents-filters-filter-ns").click(); + // Located by `title` on the option element, not by role: rc-select renders a + // second, zero-sized `role=listbox` for screen readers, and Playwright resolves + // it happily and then waits for a visibility that never arrives. + await page.locator('.ant-select-item-option[title="analytics"]').click(); + await page.keyboard.press("Escape"); + + await expect(page.getByTestId("agents-filters-pill-ns-analytics")).toBeVisible(); + // One agent in that namespace, plus the stand-in row — which survives every filter + // deliberately: it belongs to no namespace in the sense the filter means, and + // hiding it because a namespace was chosen would take away the only way to reach + // the conversations it stands for. + await expect(dataRows(page)).toHaveCount(2); + // In the address, so a narrowed view can be linked to and survives a reload. + await expect(page).toHaveURL(/ns=analytics/); + }); + + await test.step("3. the pill removes exactly that filter", async () => { + await page.getByTestId("agents-filters-pill-ns-analytics").click(); + await expect(dataRows(page)).toHaveCount(6); + await expect(page).not.toHaveURL(/ns=analytics/); + }); + + await test.step("4. searching covers every row, not just the page on screen", async () => { + await page.getByTestId("agents-filters-search").fill("fast-lane"); + // Matched on the harness, which is half of what an agent *is* — a search over + // template names alone could never find one of two agents cut from one template. + await expect(dataRows(page)).toHaveCount(1); + await expect(page.getByTestId("agents-summary")).toHaveText("1 of 5 agents"); + + // The search term is a filter like any other, so it has a pill and "clear + // filters" means it. + await expect(page.getByTestId("agents-filters-pill-search")).toBeVisible(); + await page.getByTestId("agents-filters-pill-clear").click(); + // Back to five agents and the stand-in row. + await expect(dataRows(page)).toHaveCount(6); }); }); + +test("agents: conversations with no agent are gathered rather than only counted", async ({ + page, +}) => { + /* + * Deleting a template does not stop the conversations cut from it: an instance runs + * from the prepared revision it was built against, and the collector keeps that + * revision *for it*. So a conversation outlives its agent — and until now the list + * counted those in a sentence and offered nowhere to go, which left them running, + * holding a worker each, and reachable from nothing. + */ + await loadPage(page, routes.agents, { title: "Agents" }); + await expectSettled(page); + + await test.step("1. the notice says where they went, not just that they exist", async () => { + const notice = page.getByTestId("agents-orphaned-conversations"); + await expect(notice).toBeVisible(); + await expect(notice).toContainText("unmapped-agentinstances"); + }); + + await test.step("2. and there is a row for them, last, because it is not an agent", async () => { + /* + * It used to be pinned first, as the exception. Last is better: it is a stand-in for + * conversations whose pair no longer exists, not something anybody came here to + * find, and putting it above every real agent made the list open on the one row + * most readers were not looking for. + */ + await expect(dataRows(page).last()).toContainText("unmapped-agentinstances"); + }); + + await test.step("3. it opens the conversations rather than offering a new one", async () => { + // There is no agent to start a conversation with — that is the condition the row + // describes — so the name goes to the list of what it stands for. + await page.getByTestId("agent-link-kagent-unmapped-agentinstances-—").click(); + await page.waitForURL(/\/agents\/unmapped$/, { timeout: 30_000 }); + await expect(page.getByTestId("unmapped-table")).toBeVisible(); + // Each row says which pair it *was* built from, which is the only clue to why it + // is here — a reader recognising a template they deleted has their answer. + await expect(page.getByTestId("unmapped-table")).toContainText("on"); + }); +}); + +/** + * Agents, templates and harnesses are three tabs of one surface. + * + * They were separate destinations with their own sidebar entries, which put the three + * halves of one idea in three places and left the relationship between them implicit — + * a reader looking at templates had no way to see which harness would run them, and a + * reader looking for "New agent" was looking for something that does not exist. + * + * The tab lives in the URL, so it can be linked to and survives a reload. That is the + * part worth asserting: a tab held in state looks identical until somebody shares the + * address of what they are looking at. + */ +test("agents: the landing page is three tabs, and the tab is in the address", async ({ + page, +}) => { + await loadPage(page, routes.agents, { title: "Agents" }); + + await test.step("1. the concepts are stated before the list", async () => { + // The model is not guessable from the nouns: "Agents" reads like a list of things + // somebody made, and there is no Agent CRD at all. + const concepts = page.getByTestId("agent-concepts"); + await expect(concepts).toBeVisible(); + await expect(concepts).toContainText("AgentTemplate"); + await expect(concepts).toContainText("Harness"); + await expect(concepts, "the derived one has to say that it is").toContainText("derived"); + }); + + await test.step("2. each tab is reachable and shows its own list", async () => { + await page.getByRole("tab", { name: "Templates" }).click(); + await expect(page).toHaveURL(/tab=templates/); + // The tab itself carries no controls: creating and refreshing act on the whole + // page, so they live in its header rather than three times over. + await expect(page.getByTestId("templates-filters")).toBeVisible(); + + await page.getByRole("tab", { name: /Harnesses/ }).click(); + await expect(page).toHaveURL(/tab=harnesses/); + await expect(page.getByTestId("harnesses-table")).toBeVisible({ timeout: 30_000 }); + }); + + await test.step("3. a tab survives a reload, because it is in the address", async () => { + await page.reload(); + await expect(page.getByTestId("harnesses-table")).toBeVisible({ timeout: 30_000 }); + }); + + await test.step("4. there is no way to create an agent, because there is no such thing", async () => { + await page.getByRole("tab", { name: "Agents" }).click(); + await expect(page.getByTestId("agents-new")).toHaveCount(0); + // What it offers instead is the thing that actually makes one. + await expect(page.getByTestId("agents-new-template")).toBeVisible(); + }); +}); + +/** + * A pressed row looks different from a hovered one. + * + * There was a rule for this and it did nothing: it set the pressed background to the + * border token, which is the same colour antd already uses for the row hover — measured + * at rgb(50, 44, 61) for both. So pressing a row looked exactly like pointing at it, + * and on a slow route a click still looked like it had not registered, which is the + * whole thing the rule was added for. + * + * Compared rather than asserted against a value: the point is that the two differ, and + * pinning either to a literal would make this a test of the palette instead. + */ +test("agents: pressing a row looks different from hovering it", async ({ page }) => { + await loadPage(page, routes.agents, { title: "Agents" }); + const row = page.locator("tbody tr.clickable-table-row").first(); + await expect(row).toBeVisible({ timeout: 30_000 }); + + const background = () => + row.locator("td").first().evaluate((cell) => getComputedStyle(cell).backgroundColor); + + await row.hover(); + const hovered = await background(); + + const box = await row.boundingBox(); + await page.mouse.move(box!.x + 30, box!.y + 10); + await page.mouse.down(); + const pressed = await background(); + await page.mouse.up(); + + expect(pressed, "a press must not look like a hover").not.toBe(hovered); +}); + +/** + * The list has an order of its own, and the stand-in row is not part of it. + * + * It arrived in whatever order the namespaces were read in — stable within a read and + * meaningless to a reader, so an agent moved when an unrelated namespace answered more + * slowly. And `Unmapped conversations` is not an agent: it stands in for conversations + * whose pair no longer exists, so sorting it among real agents by name would drop it + * into the middle of the list on a "U". + */ +test("agents: the list is ordered by name, with the stand-in row last", async ({ page }) => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expect(page.locator("tbody tr").first()).toBeVisible({ timeout: 30_000 }); + + const names = await page + .locator('tbody tr [data-testid="agent-name"], tbody tr td:first-child') + .allTextContents(); + const cleaned = names.map((name) => name.trim()).filter(Boolean); + const stranded = cleaned.findIndex((name) => name.includes("Unmapped")); + + if (stranded !== -1) { + expect(stranded, "the stand-in row belongs at the bottom").toBe(cleaned.length - 1); + } + + const real = stranded === -1 ? cleaned : cleaned.slice(0, stranded); + const sorted = [...real].sort((left, right) => left.localeCompare(right)); + expect(real, "agents should be listed by name").toEqual(sorted); +}); diff --git a/ui/playwright/tests/agents/harnesses.spec.ts b/ui/playwright/tests/agents/harnesses.spec.ts new file mode 100644 index 000000000..1a7b797e3 --- /dev/null +++ b/ui/playwright/tests/agents/harnesses.spec.ts @@ -0,0 +1,140 @@ +import { test, expect } from "../../fixtures/test"; +import { loadPage, routes } from "../../helpers/app"; + +/** + * The harnesses tab, which replaced the create-an-agent form. + * + * That form is gone because what it created does not exist: there is no Agent CRD, and + * an agent is what you get once a harness admits a template. But the form carried two + * things a reader still needs, and they moved here rather than being lost with it. + * + * **The admission selector has to be visible.** A harness admits templates through a + * label selector, and that selector is what decides whether a template ever becomes an + * agent at all. A template carrying no label it matches saves happily and then does + * nothing, with nothing on screen explaining why — so the selector is on the page + * rather than behind an expander. + * + * **A harness must not be called broken.** `ready: false` also covers one the + * controller has not observed yet, which is a different thing from one that failed — + * and the `kagent` harness on the development cluster is exactly that: it runs agents + * and carries `status: null`. Calling that "broken" sends somebody debugging a harness + * that works. + */ +test("harnesses: the tab says what admits a template, and does not call a new harness broken", async ({ + page, +}) => { + await loadPage(page, routes.harnesses, { title: "Agents" }); + + await test.step("1. the harnesses are listed", async () => { + await expect(page.getByTestId("harnesses-table")).toBeVisible({ timeout: 30_000 }); + await expect(page.locator("tbody tr").first()).toBeVisible(); + }); + + await test.step("2. the admission selector is on the page, not behind anything", async () => { + // The whole reason a template does or does not become an agent, so it is read + // without expanding a row. + await expect(page.getByTestId("harness-selector").first()).toBeVisible(); + }); + + await test.step("3. an unobserved harness is 'not ready yet', never 'broken'", async () => { + const states = await page.getByTestId("harness-ready").allTextContents(); + expect(states.length).toBeGreaterThan(0); + for (const state of states) { + expect( + state.toLowerCase(), + "a harness the controller has not observed is not a broken one", + ).not.toContain("broken"); + expect(state).toMatch(/Ready|Not ready yet/); + } + }); + + await test.step("4. a harness can be made and removed from here", async () => { + /* + * This tab was read-only, on a note in the codebase saying `HarnessService` was + * read-only in this build. It was not: the service implements create, update and + * delete and always did — what was read-only was the application, which only ever + * called `list`. + */ + await expect(page.getByTestId("agents-new-harness")).toBeVisible(); + await expect( + page.getByTestId("harnesses-table").locator('[data-testid^="delete-"]').first(), + "each harness offers a delete, because one can be removed", + ).toBeVisible(); + }); + + await test.step("5. the list narrows like every other table", async () => { + await expect(page.getByTestId("harnesses-filters")).toContainText("All namespaces"); + const before = await page.getByTestId("harnesses-table").locator("tbody tr").count(); + await page.getByTestId("harnesses-filters").getByRole("textbox").fill("no-such-harness"); + await expect + .poll(() => page.getByTestId("harnesses-table").locator("tbody tr").count()) + .toBeLessThan(before); + }); +}); + +/** + * Creating a harness, and being refused the two ways a cluster would refuse it. + * + * The form is short because the CRD is strict, and the constraints it enforces are the + * cluster's rather than this page's: exactly one runtime adapter, an image pinned by + * digest, and a worker pool for the Substrate Actors to be scheduled onto. A form that + * accepted a tag would build a resource the cluster rejects — the failure that is + * invisible until somebody tries it for real, which is why the fixture refuses it too. + */ +test("harnesses: one can be created, and a tag is refused the way a cluster refuses it", async ({ + page, +}) => { + await loadPage(page, routes.harnessNew, { title: "New harness" }); + + await test.step("1. a harness with no selector says it will run nothing", async () => { + // Legal, and almost never intended: the CRD admits no templates when the selector + // is omitted, so the harness is created and does nothing with no sign of why. + await expect(page.getByTestId("harness-admits-nothing")).toBeVisible(); + }); + + await test.step("2. an image that is not pinned cannot be submitted", async () => { + await page.getByTestId("harness-namespace").click(); + await page.locator(".ant-select-item-option").first().click(); + await page.getByTestId("harness-name").fill("made-here"); + await page.getByTestId("harness-worker-pool").fill("kagent-default"); + + await page.getByTestId("harness-image").fill("ghcr.io/example/runtime:latest"); + await expect( + page.getByTestId("harness-create"), + "a tag can move under a running agent, and the CRD refuses one", + ).toBeDisabled(); + }); + + await test.step("3. nor can one with no snapshot location", async () => { + // The CRD requires it. This form used to treat it as optional, so a harness + // could be submitted without one and the controller answered "Invalid Harness" + // -- naming neither the field nor what was wrong with it. + await page + .getByTestId("harness-image") + .fill(`ghcr.io/example/runtime@sha256:${"a".repeat(64)}`); + await expect(page.getByTestId("harness-create")).toBeDisabled(); + }); + + await test.step("4. pinned by digest and told where snapshots go, it can be created", async () => { + await page.getByTestId("harness-snapshot").fill("s3://ate-snapshots/kagent"); + await page.getByTestId("harness-selector-key").fill("runtime"); + await page.getByTestId("harness-selector-value").fill("made-here"); + await expect(page.getByTestId("harness-admits-nothing")).toHaveCount(0); + + await expect(page.getByTestId("harness-create")).toBeEnabled(); + await page.getByTestId("harness-create").click(); + + // Back to the tab it came from, with the new harness in the list. + await page.waitForURL(/tab=harnesses/); + await expect(page.getByTestId("harnesses-table")).toContainText("made-here", { + timeout: 30_000, + }); + }); + + await test.step("5. and it is not ready yet, which is what a cluster reports", async () => { + // The controller has not observed it. A fixture that answered "ready" would hide + // the one state a newly created harness is actually in. + const row = page.getByTestId("harnesses-table").locator("tr", { hasText: "made-here" }); + await expect(row.getByTestId("harness-ready")).toContainText("Not ready yet"); + }); +}); diff --git a/ui/playwright/tests/app-shell.spec.ts b/ui/playwright/tests/app-shell.spec.ts index 7dd3b2aff..7b29d88b1 100644 --- a/ui/playwright/tests/app-shell.spec.ts +++ b/ui/playwright/tests/app-shell.spec.ts @@ -1,54 +1,159 @@ import { test, expect } from "../fixtures/test"; -import { loadPage, expectNoErrors } from "../helpers/page"; -import { gotoView, gotoCreate } from "../helpers/nav"; - -// App-shell journey — one test that walks the persistent shell end to end: the -// Agents list renders, then dropdown-based navigation to every listing and create -// page. -// -// The agents list shows the helm-seeded sample agents (k8s-agent, etc.) in the -// kagent namespace. We assert one of them is present rather than an exact count, -// so the test doesn't break as the seeded set evolves. -const SEEDED_AGENT = "k8s-agent"; - -test("app shell: list and navigation", async ({ page }) => { - // region Reading — the agents list renders from the backend - await test.step("renders the agents list from the real backend", async () => { - const fatalErrors: string[] = []; - page.on("pageerror", (err) => fatalErrors.push(err.message)); - - await loadPage(page, "/", { heading: "Agents" }); - await expect(page.getByText(SEEDED_AGENT).first()).toBeVisible(); - await expectNoErrors(page); - expect(fatalErrors, `uncaught page errors: ${fatalErrors.join("; ")}`).toEqual([]); - }); +import { loadPage, expectPageTitle, routes } from "../helpers/app"; +import { expectShell, navLabels } from "../helpers/nav"; - // region Navigating — reach every listing and create page via the header menus - await test.step("navigates between listing pages via the View menu", async () => { - await gotoView(page, "Models", "**/models"); - await expect(page.getByRole("heading", { level: 1, name: "Models" })).toBeVisible(); +/** + * App shell — the chrome that every in-app route renders inside. + * + * One journey: the shell renders, it advertises every destination the app has, it + * survives a route change rather than remounting the page whole, and creation is reachable + * from the lists rather than from the chrome. + * + * That last part used to be a Create menu in the header, and the assertion that it is + * *gone* matters as much as the ones that replace it. The header belongs to the default + * shell, so a distribution supplying its own layout inherited none of it — a create route + * reachable only from the header was, there, reachable only by typing the URL. + */ - await gotoView(page, "MCP & tools", "**/mcp"); - await expect(page.getByRole("heading", { level: 1, name: "MCP & tools" })).toBeVisible(); - await expect(page.getByLabel("Loading apps")).toHaveCount(0); +test("app shell: chrome, navigation entries, and where creation lives", async ({ + page, +}) => { + await test.step("1. the shell renders around the agents list", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expectShell(page); + // The logo is the wordmark, so there is no text to read — its accessible name + // is what identifies the product now, and asserting that is also the check + // that the SVG did not arrive unlabelled. + const logo = page.getByTestId("app-logo"); + await expect(logo).toHaveAttribute("aria-label", /kagent/i); + await expect(logo.locator("svg")).toBeVisible(); }); - await test.step("navigates to create pages via the Create menu", async () => { - // The Create menu lives in the persistent header, so we navigate client-side - // from wherever the View step left us (no extra full reload of "/"). - await gotoCreate(page, "New Agent", "**/agents/new"); - await expect(page.getByRole("heading", { level: 1, name: "New Agent", exact: true })).toBeVisible(); + await test.step("2. every destination the app ships is in the sidebar", async () => { + for (const [key, label] of Object.entries(navLabels)) { + const entry = page.getByTestId(`nav-${key}`); + await expect(entry, `sidebar is missing "${label}"`).toBeVisible(); + await expect(entry).toContainText(label); + } + }); - await gotoCreate(page, "New Agent Harness", "**/agents/new-harness"); - await expect(page.getByRole("heading", { level: 1, name: "New Agent Harness" })).toBeVisible(); + await test.step("3. the chrome persists across a route change", async () => { + const headerId = await page + .getByTestId("app-header") + .evaluate((node) => { + // Tag the live node so we can tell "still the same element" from + // "re-rendered from scratch" after navigating. + node.setAttribute("data-shell-probe", "1"); + return node.getAttribute("data-shell-probe"); + }); + expect(headerId).toBe("1"); - await gotoCreate(page, "New Model", "**/models/new"); - await expect(page.getByRole("heading", { level: 1, name: "New Model" })).toBeVisible(); + await page.getByTestId("nav-models").click(); + await page.waitForURL(/\/models(\?|$)/); + await expectPageTitle(page, "Models"); - await gotoCreate(page, "New MCP Server", "**/mcp/new"); - await expect(page.getByRole("heading", { level: 1, name: "New MCP server" })).toBeVisible(); + // The header element was never torn down, so the shell is genuinely + // persistent rather than re-mounted per route. + await expect(page.locator('[data-testid="app-header"][data-shell-probe="1"]')).toHaveCount(1); + }); - await gotoCreate(page, "New prompt library", "**/prompts/new"); - await expect(page.getByRole("heading", { level: 1, name: "New Prompt Library" })).toBeVisible(); + await test.step("4. the chrome offers no create menu of its own", async () => { + await expect(page.getByTestId("create-menu-trigger")).toHaveCount(0); }); + + await test.step("5. every list that can create one says so, and reaches its form", async () => { + // Each list page, its own control, and the form it lands on. Driven page by page + // because that is the claim: not "a create route exists" but "the list you are + // looking at offers it". + const creates = [ + /* The agents list creates a *template*, not an agent. There is no agent to + create — it is what exists once a harness admits a template — so the control + that used to say "New agent" now leads to the template form. */ + { + route: routes.agents, + title: "Agents", + testId: "agents-new-template", + form: "New agent template", + }, + { route: routes.models, title: "Models", testId: "models-new", form: "New model" }, + { + route: routes.mcpServers, + title: "MCP servers", + testId: "mcp-servers-new", + form: "New MCP server", + }, + { + route: routes.prompts, + title: "Prompts", + testId: "prompts-new", + form: "New prompt library", + }, + ] as const; + + for (const { route, title, testId, form } of creates) { + await loadPage(page, route, { title }); + const create = page.getByTestId(testId); + await expect(create, `${title} has no create control`).toBeVisible(); + await create.click(); + await expectPageTitle(page, form); + await expectShell(page); + } + }); +}); + +/** + * Navigation is made of links, so it can be opened the way links can. + * + * These were menu items with a click handler, which gave a reader nothing to + * cmd-click: no `href`, so no "open in new tab", no middle-click, no copy-link. That + * is a reasonable thing to want from navigation — comparing two pages side by side — + * and the fix is that each entry is a router `Link` rather than a handler. + * + * Both halves are asserted, because either alone is a plausible mistake: a plain + * `` would open a new tab and also reload the whole app on an ordinary click, and a + * handler with no anchor keeps the app fast while making a new tab impossible. So this + * checks the anchor is real *and* that an ordinary click never reloads the document. + */ +test("app shell: nav entries are links, not click handlers", async ({ page }) => { + await loadPage(page, routes.dashboard); + + // Waited for explicitly: `evaluateAll` has no auto-wait, so without this it reads an + // empty list before the shell has rendered and passes or fails on timing. + await expect(page.getByTestId("nav-models").locator("a")).toBeVisible(); + + // Every entry carries a real destination. + const hrefs = await page + .locator('[data-testid^="nav-"] a') + .evaluateAll((anchors) => anchors.map((a) => a.getAttribute("href"))); + expect(hrefs.length).toBeGreaterThan(3); + expect(hrefs.every((href) => typeof href === "string" && href.startsWith("/"))).toBe(true); + + // An ordinary click is handled by the router: the document is never reloaded, which a + // value set on `window` before the click is enough to prove. + let documentLoads = 0; + page.on("load", () => { documentLoads += 1; }); + await page.evaluate(() => { (window as unknown as Record).spaMarker = "alive"; }); + + await page.getByTestId("nav-models").locator("a").click(); + await expect(page).toHaveURL(/\/models$/); + expect(documentLoads).toBe(0); + expect( + await page.evaluate(() => (window as unknown as Record).spaMarker), + ).toBe("alive"); + + /* + * And the entry is a link the browser can act on, rather than a handler dressed as + * one. Asserted through the anchor, not by modifier-clicking it. + * + * Driving a real cmd/ctrl-click turned out to assert the *browser*, not this app: + * the modifier differs by platform, and headless Chromium on Linux does not raise a + * new page for it at all, so the same correct markup passed on one engine and timed + * out on the other. What this app owns is that the destination is a genuine `href` + * on an `` that is not target-hijacked — given that, opening a new tab is the + * browser's business and it does it. + */ + const anchor = page.getByTestId("nav-prompts").locator("a"); + await expect(anchor).toHaveAttribute("href", "/prompts"); + expect(await anchor.evaluate((a) => (a as HTMLAnchorElement).target)).toBe(""); + await expect(page).toHaveURL(/\/models$/); }); diff --git a/ui/playwright/tests/auth/auth-modes.spec.ts b/ui/playwright/tests/auth/auth-modes.spec.ts new file mode 100644 index 000000000..6a0aa126a --- /dev/null +++ b/ui/playwright/tests/auth/auth-modes.spec.ts @@ -0,0 +1,110 @@ +import { expect, test } from "../../fixtures/test"; + +/** + * The three authentication states, each driven end to end. + * + * **This is the spec to run when the question is "does authentication still work".** It + * needs no proxy, no identity provider and no cluster: the mock backend answers + * oauth2-proxy's `/oauth2/userinfo`, which is the only thing the app asks about who is + * signed in, and `?auth=` chooses what it answers. + * + * - **unsecured** — nothing is fronting the app. It must work exactly as in development + * and must *never* redirect: there is no `/oauth2` endpoint to redirect to, so the + * browser would bounce between the app and a 404 forever. + * - **authenticated** — a proxy is in front and the session is good; the reader's identity + * appears in the header. + * - **expired** — a proxy is in front and its session has lapsed. The app leaves for the + * proxy on its own, carrying `rd` so the reader returns to the page they were reading. + * + * The last is what regressed in the rewrite. The header offered a button to a page that + * offered another button, where the UI this replaced recovered without being asked — + * while `AuthStatus`'s own doc comment said the UI "should re-run OIDC" the whole time. + */ + +/** The proxy's start endpoint. Not served by anything, so the SPA fallback answers it. */ +const START = "/oauth2/start"; + +test.describe("authentication", () => { + test("unsecured: the app works, and never redirects", async ({ page }) => { + // No `?auth=`, which is the default and what mock mode should say: there is no + // backend to have signed in to. + const navigations: string[] = []; + page.on("framenavigated", (frame) => { + if (frame === page.mainFrame()) navigations.push(frame.url()); + }); + + await page.goto("/agents"); + await expect(page.getByTestId("agents-table")).toBeVisible({ timeout: 30_000 }); + + // Usable, and silent about sessions. + await expect(page.getByTestId("header-reauth")).toHaveCount(0); + await expect(page.getByTestId("header-user")).toHaveCount(0); + + await page.waitForTimeout(2_000); + expect( + navigations.filter((url) => url.includes(START)), + "an unsecured deployment must never be sent to a proxy that is not there", + ).toEqual([]); + }); + + test("authenticated: the header names the reader", async ({ page }) => { + await page.goto("/agents?auth=authenticated"); + + await expect(page.getByTestId("header-user")).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId("header-user")).toContainText("alice"); + // Nothing asks them to sign in, because they are signed in. + await expect(page.getByTestId("header-reauth")).toHaveCount(0); + }); + + test.describe("with a lapsed session", () => { + // The 401 from `/oauth2/userinfo` *is* the state under test, so the console fixture + // has to be told to expect it. Scoped to these two tests and to that one status: a + // blanket allowance would stop the fixture doing its job for everything else here. + test.use({ + expectedNoise: [ + /Failed to load resource: the server responded with a status of 401/, + ], + }); + + test("expired: the app re-authenticates itself, and comes back here", async ({ + page, + }) => { + await page.goto("/agents?auth=expired&filter=k8s"); + + // Nothing is clicked. The app notices and leaves. + await page.waitForURL((url) => url.pathname === START, { timeout: 30_000 }); + + // Carrying where the reader was, so signing in does not cost them the page. + const rd = new URL(page.url()).searchParams.get("rd"); + expect(rd).toContain("/agents"); + expect(rd).toContain("filter=k8s"); + }); + + test("an attempt already spent: it stays put and offers the way out", async ({ + page, + }) => { + // The guard is seeded directly rather than earned by driving the browser through the + // proxy's path. `/oauth2/start` is not a route this app owns — the SPA fallback serves + // it, so the app boots there and its first API calls race the mock worker's + // registration, which surfaced as CORS noise the console fixture rightly failed on. + // What is under test is what the app does when an attempt is *already* spent, and that + // needs no round trip to set up. + await page.addInitScript(() => { + window.sessionStorage.setItem("kagent_reauth_attempt", String(Date.now())); + }); + + await page.goto("/agents?auth=expired"); + + // Offered the way out rather than bounced: a proxy that keeps handing back a token it + // will not refresh must not become a redirect loop, because a reader cannot read an + // error on a page that keeps leaving. + await expect(page.getByTestId("header-reauth")).toBeVisible({ timeout: 30_000 }); + + await page.waitForTimeout(2_000); + expect( + new URL(page.url()).pathname, + "a spent attempt must not send the reader away again", + ).toBe("/agents"); + }); + }); +}); diff --git a/ui/playwright/tests/chat/agent-rail.spec.ts b/ui/playwright/tests/chat/agent-rail.spec.ts new file mode 100644 index 000000000..0ce891590 --- /dev/null +++ b/ui/playwright/tests/chat/agent-rail.spec.ts @@ -0,0 +1,509 @@ +import { test, expect } from "../../fixtures/test"; +import { agentChat, agentDetail, agentPage, agents, instances } from "../../helpers/app"; + +/** + * The agent rail — the navigation for when you are inside one agent. + * + * Narrowed to a single agent: which agent you are in, the things you can do to it, + * and every conversation you have had with it. The last of those is the sibling + * instances of the same `(Harness, AgentTemplate)` pair, because an `AgentInstance` + * *is* one conversation — so a second conversation with an agent is a second + * instance of the same pair, and "New chat" creates rather than navigates. + * + * ## What is no longer here + * + * The capabilities panel — an agent's tools and skills beside its conversation. It + * read them off a `SandboxAgent`, and an instance has neither: what an agent can + * reach is described by its `AgentTemplate`, which has no surface in this build yet. + * Recorded in `playwright/DEFERRED.md` rather than left as a passing test of + * something that is gone. + */ + +const AGENT_CHAT = agentChat(instances.ready); +const AGENT_DETAILS = agentDetail(instances.ready); + +test("chat: a conversation's record is read without leaving the conversation", async ({ + page, +}) => { + /* + * This was an entry in the rail, and reading four facts about a conversation meant + * leaving it and then finding the way back. Reference that costs a navigation is + * reference nobody consults, so it is a modal over the conversation now — in the + * gutter under Share, which is where the conversation's other controls live. + */ + await page.goto(AGENT_CHAT); + await page.getByTestId("chat-details").click(); + + const fields = page.getByTestId("conversation-details-fields"); + await expect(fields).toBeVisible({ timeout: 30_000 }); + // The record, not a summary: the id is what a reader copies into a CLI. + await expect(fields).toContainText(instances.ready); + + // Still on the conversation behind it — the point of not making this a page. + await expect(page).toHaveURL(new RegExp(`/agents/kagent/${instances.ready}/chat$`)); + await expect(page.getByTestId("chat-input")).toBeVisible(); + + // There is no Edit anywhere on it: an instance has no spec to change. What the agent + // *is* lives on its AgentTemplate and how it *runs* on its Harness, so a control here + // would offer something that does not exist. + await expect(page.getByTestId("agent-details-edit")).toHaveCount(0); +}); + +test("agent rail: a conversation is deleted from a menu, on every surface", async ({ + page, +}) => { + /* + * The control used to be a trash can on every row, always visible, inches from the + * conversation being read in a rail where every row looks alike — a slip cost the + * whole thing with nothing to undo it. It is behind a per-row menu now, revealed on + * hover, so deleting takes two deliberate actions and the list reads as names. + * + * It also only existed where a caller passed a handler, which meant the chat page and + * nowhere else: the same row behaved differently depending on which surface had + * mounted the rail. The rail owns the delete now, which is what step 3 checks. + */ + await page.goto(AGENT_CHAT); + const rail = page.getByTestId("chat-sessions"); + // The row links themselves. Several controls share the `chat-session-` prefix now — + // the menu, the checkbox, the confirmation — so a prefix match counts each row + // several times. + const rows = rail.locator('a[data-testid^="chat-session-"]'); + // Counted after the list has arrived: counting during the read gives zero, and a + // later assertion of "one fewer" then expects minus one. + await expect(rows.first()).toBeVisible({ timeout: 30_000 }); + const before = await rows.count(); + + const item = page.getByRole("menuitem", { name: "Delete chat" }); + + await test.step("1. the menu offers it, and the row is otherwise quiet", async () => { + const menu = rail.locator('[data-testid^="chat-session-menu-"]').first(); + // Present for a pointer to find, but not drawn until the row is hovered. + await expect(menu).toHaveCSS("opacity", "0"); + await menu.click({ force: true }); + await expect(item).toBeVisible(); + // The dropdown animates in, and a click landing mid-transition is refused as + // unstable rather than missing the element. + await page.waitForTimeout(400); + }); + + await test.step("2. it still asks, and the question names the conversation", async () => { + // The menu makes deleting deliberate; it does not make it recoverable. A + // conversation is gone with its whole transcript and there is no undo. + await item.click(); + const confirm = page.locator(".ant-modal:visible"); + await expect(confirm).toContainText("cannot be recovered"); + await confirm.getByRole("button", { name: "Keep" }).click(); + await expect(rows).toHaveCount(before); + }); + + await test.step("3. and Delete removes exactly one", async () => { + // The dialog animates out, and a click while it is still there lands on its mask. + await expect(page.locator(".ant-modal:visible")).toHaveCount(0); + await page.locator('[data-testid^="chat-session-menu-"]').first().click({ force: true }); + await page.waitForTimeout(400); + await page.getByRole("menuitem", { name: "Delete chat" }).click(); + await page.locator(".ant-modal:visible").getByRole("button", { name: "Delete" }).click(); + await expect(rows).toHaveCount(before - 1, { timeout: 20_000 }); + await expect(page.getByTestId("chat-sessions-error")).toHaveCount(0); + }); + + await test.step("4. and the same control is there off the chat page", async () => { + // The agent's own page mounts the same rail and passes no delete handler. That used + // to mean no control at all. + await page.getByTestId("agent-nav-agent-conversations").click(); + await expect(page.getByTestId("agent-rail")).toBeVisible({ timeout: 30_000 }); + await expect( + page.locator('[data-testid^="chat-session-menu-"]').first(), + ).toHaveCount(1); + }); +}); + +/** + * Changing which agent the rail is scoped to, without leaving the rail. + * + * The identity card wears a chevron, so it has to open something: an affordance that + * looked like "change agent" and went somewhere else was the bug this replaced. + */ +/** + * The agent you are on stays out of its own switcher, wherever you opened it from. + * + * The exclusion matched on namespace, template *and* harness, and the harness reaches + * the rail from the open conversation's record — so it was undefined until that record + * loaded, and on the surfaces with no conversation at all it never arrived. Requiring + * it to match meant nothing matched, and the current agent listed itself: not always, + * which is what made it look intermittent, but exactly whenever the record was not + * there. + * + * Opened from the agent's own page, which is one of the surfaces that has no + * conversation to read a harness from — the case the chat page's version of this test + * cannot reach. + */ +test("agent rail: the current agent is absent from the switcher on a surface with no conversation", async ({ + page, +}) => { + // The agent's own page, which has no conversation open and so no record to read a + // harness from — the state where this actually broke. + await page.goto(agentPage(agents.k8s)); + await expect(page.getByTestId("agent-rail-identity")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("agent-rail-identity").click(); + + const switcher = page.getByTestId("agent-switcher"); + await expect(switcher).toBeVisible(); + // Once the list has actually arrived: asserting a row is absent while nothing has + // loaded passes for the wrong reason. + await expect( + switcher.locator('[data-testid^="agent-switcher-option-"]').first(), + ).toBeVisible({ timeout: 30_000 }); + + await expect( + page.getByTestId(`agent-switcher-option-${agents.k8s.template}-${agents.k8s.harness}`), + "the agent whose page is open should not be offered as somewhere to go", + ).toHaveCount(0); +}); + +test("agent rail: the identity card switches agent", async ({ page }) => { + await page.goto(AGENT_CHAT); + + // Not mounted until asked for — the switcher reads every namespace, one request + // each, and a rail that did that before anybody wanted a menu would be paying for + // one most readers never open. + await expect(page.getByTestId("agent-switcher")).toHaveCount(0); + + await page.getByTestId("agent-rail-identity").click(); + const switcher = page.getByTestId("agent-switcher"); + await expect(switcher).toBeVisible(); + + const options = switcher.locator('[data-testid^="agent-switcher-option-"]'); + /* + * Agents, not conversations. + * + * This listed `AgentInstance`s, so a switcher labelled "agent" moved between + * *conversations* — and one agent with nine of them filled it nine times over with + * rows nothing distinguished but a UUID. An agent is a (template, harness) pair, + * which is what the agents page lists and what somebody opening this is looking for. + * + * The agent the reader is already on is **not** listed. Every row here is somewhere + * to go, and that one goes nowhere: listing it made them read past their own agent to + * find another, and offered a click that did nothing — which is worse than absent, + * because it looks like a destination. The card that opens this menu names the + * current agent directly above it. + */ + await expect(options.first()).toBeVisible({ timeout: 30_000 }); + await expect( + page.getByTestId(`agent-switcher-option-${agents.k8s.template}-${agents.k8s.harness}`), + ).toHaveCount(0); + + // Filtering narrows it, and matches what the reader can see — the template and the + // harness, which is how the agent is named everywhere else. + const before = await options.count(); + await switcher.getByTestId("agent-switcher-filter").fill("reporting"); + await expect(options).toHaveCount(1); + expect(before).toBeGreaterThan(1); + + // Picking one starts a conversation with it — the call to action for that agent, + // where nothing is created until a message is sent. Not the conversation of the + // agent left behind, and not a new instance either. + await switcher.getByTestId("agent-switcher-filter").fill(""); + await options.first().click(); + await expect(page).toHaveURL(/\/agents\/[^/]+\/[^/]+\/on\/[^/]+\/new$/); +}); + +/** + * The rail is sticky and so is the header, and the header draws on top. + * + * Stuck any higher than the header is tall, the rail slid underneath it — and the + * switcher, which opens from the card at the very top of the rail, came out of a card + * that was itself half-hidden. Asserted as geometry rather than as a screenshot, + * because the failure is an overlap of two rectangles and that is what to measure. + */ +test("agent rail: stays clear of the header when the page scrolls", async ({ page }) => { + // A short viewport on a tall page, so there is something to scroll. + await page.setViewportSize({ width: 1400, height: 700 }); + await page.goto(AGENT_DETAILS); + await expect(page.getByTestId("agent-rail-identity")).toBeVisible(); + + await page.evaluate(() => window.scrollTo(0, 900)); + await page.getByTestId("agent-rail-identity").click(); + await expect(page.getByTestId("agent-switcher")).toBeVisible(); + + const edges = await page.evaluate(() => { + const rect = (id: string) => + document.querySelector(`[data-testid="${id}"]`)?.getBoundingClientRect(); + return { + headerBottom: rect("app-header")?.bottom ?? 0, + cardTop: rect("agent-rail-identity")?.top ?? 0, + switcherTop: rect("agent-switcher")?.top ?? 0, + }; + }); + + expect(edges.headerBottom).toBeGreaterThan(0); + expect(edges.cardTop).toBeGreaterThanOrEqual(edges.headerBottom); + expect(edges.switcherTop).toBeGreaterThanOrEqual(edges.headerBottom); +}); + +/** + * The conversations are the only part of the rail that scrolls. + * + * It used to scroll as one box, so a reader with thirty conversations scrolled the + * agent's name, the switcher and the search field away in order to reach them — and + * the search field is the thing you reach for *because* the list is long. + * + * Asserted structurally rather than by scrolling a long fixture: what makes this true + * is the rail being bounded with its overflow hidden while the list owns an `auto` + * one, and that holds at any length. + */ +test("agent rail: the conversation list scrolls without taking the rest with it", async ({ + page, +}) => { + await page.setViewportSize({ width: 1400, height: 700 }); + await page.goto(AGENT_DETAILS); + await expect(page.getByTestId("chat-sessions-list")).toBeVisible(); + + const shape = await page.evaluate(() => { + const at = (id: string) => document.querySelector(`[data-testid="${id}"]`); + const rail = at("agent-rail"); + const list = at("chat-sessions-list"); + const search = at("chat-search"); + return { + railOverflow: rail ? getComputedStyle(rail).overflowY : null, + listOverflow: list ? getComputedStyle(list).overflowY : null, + railHeight: rail?.getBoundingClientRect().height ?? 0, + searchBottom: search?.getBoundingClientRect().bottom ?? 0, + listTop: list?.getBoundingClientRect().top ?? 0, + searchIsInsideList: list && search ? list.contains(search) : true, + }; + }); + + // The rail cannot scroll; the list can. + expect(shape.railOverflow).toBe("hidden"); + expect(shape.listOverflow).toBe("auto"); + + // The rail is bounded by the window, which is what gives the list something to + // scroll inside rather than growing the page. + expect(shape.railHeight).toBeLessThanOrEqual(700); + + // And the search field is above the scrolling part, not inside it. + expect(shape.searchIsInsideList).toBe(false); + expect(shape.searchBottom).toBeLessThanOrEqual(shape.listTop); +}); + +test("agent rail: it can be got out of the way, and stays that way", async ({ page }) => { + await page.goto(AGENT_CHAT); + await expect(page.getByTestId("agent-rail")).toBeVisible({ timeout: 30_000 }); + + await test.step("1. collapsing leaves a way back, not a dead edge", async () => { + await page.getByTestId("agent-rail-collapse").click(); + // Hidden rather than unmounted: it slides shut, and animating a width needs the + // element to still be there. Asserted as not-visible so a rail that stopped + // collapsing would still fail. + await expect(page.getByTestId("agent-rail")).toBeHidden(); + // The control that undoes it has to be where the thing used to be. A collapse with + // no visible way back is a feature people use once. + await expect(page.getByTestId("agent-rail-expand")).toBeVisible(); + }); + + await test.step("2. the preference is the reader's, not the page's", async () => { + // Collapsing on one conversation and finding it back on the next is what makes + // people stop using the control, so it is remembered rather than reset per page. + await page.goto(AGENT_DETAILS); + await expect(page.getByTestId("agent-rail-expand")).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId("agent-rail")).toBeHidden(); + }); + + await test.step("3. and expanding brings the navigation back", async () => { + await page.getByTestId("agent-rail-expand").click(); + await expect(page.getByTestId("agent-rail")).toBeVisible(); + await expect(page.getByTestId("chat-sessions")).toBeVisible(); + }); +}); + +test("chat: the agent panel says what the conversation cannot", async ({ page }) => { + /* + * A conversation is an `AgentInstance`, and an instance holds no configuration — + * what model is answering, what it was told to do and what tools it can reach all + * live on the `AgentTemplate` it was cut from. So this panel reads the template, + * which is also a thing the reader can open and change. + */ + await page.goto(AGENT_CHAT); + const panel = page.getByTestId("chat-agent-context"); + await expect(panel).toBeVisible({ timeout: 30_000 }); + + await test.step("1. it names the template, and the template is a link", async () => { + // Not a dead label: every conversation with this agent reads the same template, and + // the page behind this link is where that is said before anybody edits it. + await expect(page.getByTestId("chat-agent-context-template")).toBeVisible(); + }); + + await test.step("2. the model and the tools, read from that template", async () => { + await expect(panel).toContainText("Model"); + await expect(panel).toContainText("Tools"); + }); + + await test.step("3. and it can be put away, and stays away", async () => { + await page.getByTestId("chat-context-collapse").click(); + await expect(panel).toBeHidden(); + await expect(page.getByTestId("chat-context-expand")).toBeVisible(); + + // Remembered per reader, like the rail: closing it on one conversation and finding + // it back on the next is what makes people stop using the control. + await page.reload(); + await expect(page.getByTestId("chat-context-expand")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("chat-context-expand").click(); + await expect(page.getByTestId("chat-agent-context")).toBeVisible(); + }); +}); + +/** + * A rail you can read: every row named, and every row's state visible. + * + * Both of these were missing for the same reason and are fixed by the same change. + * Deriving a conversation's title needs its first message, which needs its task list — + * and the A2A gateway refused a task read for any conversation that was not ready. With + * conversations giving their workers back at the end of every turn, that is most of + * them, so the rail fell back to `Untitled · 50b46891` for everything except the one + * already open: the only row a reader could identify was the one they were looking at. + * + * The gateway now answers a task read from the store whatever state the instance is in, + * because that is where the transcript lives. Resuming to read one would have claimed a + * worker every time somebody glanced at a conversation. + */ +test("agent rail: conversations are named and show their state", async ({ page }) => { + await page.goto(AGENT_CHAT); + const rail = page.getByTestId("chat-sessions"); + const rows = rail.locator('a[data-testid^="chat-session-"]'); + await expect(rows.first()).toBeVisible({ timeout: 30_000 }); + + await test.step("1. an ordinary conversation is not marked at all", async () => { + /* + * The dot marks the exceptions, not everything. + * + * It used to appear on every row, `ready` included, which was right while `ready` + * meant something: a conversation held a worker until the page suspended it. The + * server quiesces a runtime after every turn now and leaves the record `ready`, so + * `ready` is what every conversation says, permanently — and a dot on every row + * repeating it is decoration implying a distinction the API cannot make. + * + * So a fixture of ordinary conversations carries no dots, and one that is creating, + * failed or being deleted carries one worth looking at. + */ + const dots = rail.locator('[data-testid^="chat-session-state-"]'); + await expect(dots.locator('[data-testid="chat-session-state-ready"]')).toHaveCount(0); + }); + + await test.step("2. a row is named by what was said in it, not only by its id", async () => { + /* + * Asserted on a row other than the open one, which is the whole point: the open + * conversation always had a title, because the page rendering its transcript could + * derive one. Everything else fell back to the id. + */ + const others = rows.filter({ hasNotText: "Untitled" }); + await expect( + others, + "at least one conversation should be named by its first message", + ).not.toHaveCount(0, { timeout: 30_000 }); + }); +}); + +test("agent rail: several conversations can be picked and deleted together", async ({ + page, +}) => { + /* + * Deleting twenty conversations one confirmation at a time is a chore, and on a + * cluster where each holds a worker it is the chore standing between a reader and a + * working pool. So they can be picked as a set. + * + * Scoped to what the filter is showing throughout: selecting all after a search means + * the ones searched for, and a range extends over the visible list. Extending over the + * unfiltered one would tick conversations that are not on screen, and the count would + * then not match what the reader can see. + */ + await page.goto(AGENT_CHAT); + const rail = page.getByTestId("chat-sessions"); + const rows = rail.locator('a[data-testid^="chat-session-"]'); + await expect(rows.first()).toBeVisible({ timeout: 30_000 }); + const before = await rows.count(); + const boxes = rail.locator('[data-testid^="chat-session-select-"]'); + + await test.step("1. the row is there before a selection, but the actions are not", async () => { + /* + * The row stays and only the actions button comes and goes. + * + * It used to be the whole bar that appeared, which meant ticking the first + * conversation inserted a line and pushed the list down under the reader's pointer + * — a jump at the exact moment they were aiming at something. So what this asserts + * is a layout that does not move: select-all is present with nothing selected, and + * the button that has nothing to act on yet is the only part missing. + */ + await expect(page.getByTestId("chat-bulk-bar")).toBeVisible(); + await expect(page.getByTestId("chat-selection-count")).toContainText("Select all"); + await expect(page.getByTestId("chat-bulk-menu")).toHaveCount(0); + + // The property the row exists for: the list does not move when one is ticked. + const listTop = await rail + .locator('a[data-testid^="chat-session-"]') + .first() + .evaluate((node) => node.getBoundingClientRect().top); + await rows.first().hover(); + await boxes.first().click(); + await expect(page.getByTestId("chat-bulk-menu")).toBeVisible(); + const listTopAfter = await rail + .locator('a[data-testid^="chat-session-"]') + .first() + .evaluate((node) => node.getBoundingClientRect().top); + expect( + Math.abs(listTopAfter - listTop), + "ticking a conversation should not move the list under the pointer", + ).toBeLessThanOrEqual(1); + + // Left as it was found, so the step below starts from nothing selected. + await boxes.first().click(); + }); + + await test.step("2. shift extends the selection over a run", async () => { + // Hovered first, as a reader does: the boxes are hidden until the row is under the + // pointer, so the list reads as names rather than as a form. + await rows.first().hover(); + await boxes.first().click(); + await rows.nth(before - 1).hover(); + await boxes.nth(before - 1).click({ modifiers: ["Shift"] }); + // Everything between the two ends, not just the two clicked — which is the whole + // difference between shift-selecting and clicking twice. + await expect(page.getByTestId("chat-selection-count")).toContainText( + `${before} selected`, + ); + }); + + await test.step("3. the same control clears, then selects everything again", async () => { + // Everything is selected after the range above, so the box is checked and clicking + // it clears — a select-all that only ever adds gives no way back out of a big + // selection. + await page.getByTestId("chat-select-all").click(); + // The row stays put; what goes is the actions button and the count, because there + // is nothing left to act on. The row remaining is the whole point of it. + await expect(page.getByTestId("chat-bulk-bar")).toBeVisible(); + await expect(page.getByTestId("chat-bulk-menu")).toHaveCount(0); + await expect(page.getByTestId("chat-selection-count")).toContainText("Select all"); + }); + + await test.step("4. deleting the set asks once, and takes all of them", async () => { + await rows.first().hover(); + await boxes.first().click(); + await rows.nth(1).hover(); + await boxes.nth(1).click(); + await page.getByTestId("chat-bulk-menu").click(); + await page.waitForTimeout(400); + await page.getByRole("menuitem", { name: /Delete all selected/ }).click(); + + const confirm = page.getByTestId("chat-bulk-confirm"); + // One question for the set, naming how many — not one per conversation, which is + // the thing that makes clearing a rail unbearable. + await expect(page.locator(".ant-modal:visible")).toContainText("Delete 2 conversations?"); + await expect(page.locator(".ant-modal:visible")).toContainText("can be recovered"); + // And why it is worth doing on a cluster that keeps running out of workers. + await expect(page.locator(".ant-modal:visible")).toContainText("workers they hold"); + await page.locator(".ant-modal:visible").getByRole("button", { name: "Delete" }).click(); + + await expect(rows).toHaveCount(before - 2, { timeout: 20_000 }); + await expect(confirm).toHaveCount(0); + }); +}); diff --git a/ui/playwright/tests/chat/agent-sharing.spec.ts b/ui/playwright/tests/chat/agent-sharing.spec.ts new file mode 100644 index 000000000..05221c63a --- /dev/null +++ b/ui/playwright/tests/chat/agent-sharing.spec.ts @@ -0,0 +1,152 @@ +import { expect, test } from "../../fixtures/test"; +import { agentChat, instances } from "../../helpers/app"; + +/** + * Sharing a conversation: create a link, see it listed, revoke it, open one. + * + * ## What changed to make this possible + * + * A share is over an `AgentInstance`, because the instance *is* the conversation. + * `AgentInstanceService` always carried the three share RPCs, but nothing on the + * read path honoured the token they minted: the gRPC interceptor resolved + * `X-Share-Token` through `GetSessionShareByToken` and produced a context naming a + * *session*, while the A2A gateway authorises on the instance. A dialog built on + * those RPCs would have handed out links that could not be opened — which is why + * this was deferred rather than shipped. + * + * The interceptor now tries both kinds of share, and the gateway reads the instance + * as the share's *owner* when the token names it — which it must, since an instance + * is scoped to its creator and reading it as the visitor finds nothing. + * + * ## What a fixture can and cannot prove here + * + * It can prove the page spends a token and reports a refusal. It **cannot** prove + * the header reaches a backend: chat in mock mode is served by a client-side fake + * that builds no request, so it reads the registration directly rather than seeing + * what travelled. That gap is real and recorded in `playwright/DEFERRED.md`; only + * the live suite can close it. + */ + +const CONVERSATION = agentChat(instances.ready); +/** A link issued before this tab opened — see `SEEDED_INSTANCE_SHARE` in the mock. */ +const SEEDED_LINK = `/shared/agent/kagent/${instances.ready}/mock-instance-token-seed`; + +test("sharing: a link is created, shown once, listed and revoked", async ({ page }) => { + await page.goto(CONVERSATION); + + await test.step("1. sharing is offered on a conversation", async () => { + await expect(page.getByTestId("chat-share")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("chat-share").click(); + await expect(page.getByTestId("share-dialog")).toBeVisible(); + await expect(page.getByTestId("share-create")).toBeVisible(); + }); + + await test.step("2. read-only is the default, because giving away access should be deliberate", async () => { + // `READ_WRITE` lets a visitor send *as the owner*, so it is an opt-in. + await expect(page.getByTestId("share-allow-writes")).toHaveAttribute( + "aria-checked", + "false", + ); + }); + + await test.step("3. a created link is shown once, and says so", async () => { + await page.getByTestId("share-create").click(); + + const fresh = page.getByTestId("share-fresh-link"); + await expect(fresh).toBeVisible({ timeout: 15_000 }); + // The controller stores only a digest, so this is the one moment the token + // exists to be shown — and the page has to say that rather than imply it can be + // fetched again. + await expect(fresh).toContainText("cannot be shown again"); + // A whole link, not a bare token: that is the form a person actually sends. + await expect(fresh).toContainText("/shared/agent/kagent/"); + }); + + await test.step("4. the list shows the share, and never the token", async () => { + const list = page.getByTestId("share-list"); + // Two: the one just created and the one seeded as "issued before this tab + // opened". Waited for rather than counted immediately — the list reloads after a + // create, and counting mid-reload reads a number that is about to change. + await expect(list.locator("tbody tr")).toHaveCount(2, { timeout: 15_000 }); + await expect(list).toContainText("Read only"); + await expect( + list, + "the list cannot show a token — only its digest is stored", + ).not.toContainText("mock-instance-token"); + }); + + await test.step("5. revoking removes it", async () => { + await page.locator('[data-testid^="revoke-share-"]').first().click(); + await expect(page.getByTestId("share-list").locator("tbody tr")).toHaveCount(1, { + timeout: 15_000, + }); + await expect(page.getByTestId("share-error")).toHaveCount(0); + // The link on screen may be the one just revoked, and a copy button for a dead + // link is worse than none. + await expect(page.getByTestId("share-fresh-link")).toHaveCount(0); + }); +}); + +test("sharing: a link issued earlier opens the conversation, read-only", async ({ + page, +}) => { + await test.step("1. it opens and says what it is", async () => { + await page.goto(SEEDED_LINK); + + // Said on the page, not only in the URL: a reader who was sent a link has no + // other way to know this is somebody else's conversation. + await expect(page.getByTestId("shared-agent-notice")).toContainText("read-only", { + timeout: 30_000, + }); + await expect(page.getByTestId("shared-agent-error")).toHaveCount(0); + }); + + await test.step("2. the conversation is there", async () => { + await expect(page.getByTestId("shared-agent-transcript")).toBeVisible(); + await expect(page.getByTestId("chat-message").first()).toBeVisible({ + timeout: 30_000, + }); + }); + + await test.step("3. a read-only link offers no composer", async () => { + // An input that could not send is worse than none: the send would be refused by + // the controller and the reader would have typed for nothing. + await expect(page.getByTestId("chat-input")).toHaveCount(0); + }); +}); + +test("sharing: a token the backend never issued is refused", async ({ page }) => { + // The assertion that makes the one above mean something: the fixture refuses a + // token it cannot resolve, exactly as the controller does. Without it, a build + // that mangled the token would serve the conversation anyway and the miss would + // read on screen as success. + await page.goto(`/shared/agent/kagent/${instances.ready}/not-a-real-token`); + + await expect(page.getByTestId("shared-agent-error")).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId("shared-agent-transcript")).toHaveCount(0); +}); + +test("sharing: a link that allows replies offers a way to reply", async ({ page }) => { + /* + * The permission was grantable and had no effect. + * + * An owner could tick "Also allow replies", hand the link over, and the person + * opening it had no composer — a permission granted and silently ignored. The page + * withheld one deliberately, on the reasoning that a composer working for some links + * and not others fails invisibly; the cost of that reasoning was worse than the + * problem it avoided. + * + * The link carries `?reply`, which is a hint about what to draw and never a + * permission: the controller resolves the token and refuses a send the share does + * not allow, so a hand-edited address gets a refusal in the controller's own words. + */ + await page.goto(`${SEEDED_LINK}?reply`); + + await expect(page.getByTestId("shared-agent-notice")).toBeVisible({ timeout: 30_000 }); + // And it says what replying means here, because it is not obvious: a share answers + // as its owner, so anything sent is recorded as theirs. + await expect(page.getByTestId("shared-agent-notice")).toContainText("owner"); + await expect(page.getByTestId("chat-input")).toBeVisible(); + +}); + diff --git a/ui/playwright/tests/chat/chat-errors.spec.ts b/ui/playwright/tests/chat/chat-errors.spec.ts index a134e7a7e..58cb0a033 100644 --- a/ui/playwright/tests/chat/chat-errors.spec.ts +++ b/ui/playwright/tests/chat/chat-errors.spec.ts @@ -1,37 +1,274 @@ import { test, expect } from "../../fixtures/test"; -import { waitForAppReady } from "../../helpers/page"; -import { mockAgentStreamError } from "../../helpers/a2a"; -import { firstReadyAgent } from "../../helpers/resources"; +import { agentChat, instances, loadPage, withScenario } from "../../helpers/app"; -// Chat — error journeys. A broken A2A stream (aborted in the browser before it -// reaches the proxy) surfaces an error toast; a missing session id shows the -// not-found screen. The agent is discovered at runtime (firstReadyAgent). +/** + * Chat — the failure journeys. + * + * Three ways a conversation goes wrong, each of which the user has to be able to get + * out of: the turn fails mid-flight, the user stops a turn themselves, and the list + * of the agent's other conversations cannot be loaded at all. The thing they have in + * common is that none of them may leave the page stuck with no way forward. + * + * The conversation and that list fail independently, which is why they are separate + * journeys: `?chat=…` drives the turn's outcome and `?mock=…` drives the API. + */ -const USER_MESSAGE = "List the pods please"; -const AGENT_REPLY = "Hello from the agent"; +const AGENT_CHAT = agentChat(instances.ready); -test("chat: stream error and missing session", async ({ page }) => { - const agent = await firstReadyAgent(); +test("chat: a failed turn is reported and can be retried", async ({ page }) => { + await test.step("1. the turn starts normally", async () => { + await page.goto(`${AGENT_CHAT}?chat=error`); + await page.getByTestId("chat-input").fill("This one fails"); + await page.getByTestId("chat-send").click(); + + // The user's message is theirs; a failing agent does not erase it. + await expect( + page.locator('[data-testid="chat-message"][data-role="user"]') + .filter({ hasText: "This one fails" }), + ).toHaveCount(1); + }); - // region Sending — a broken stream surfaces an error toast, no reply - await test.step("surfaces an error when the stream fails", async () => { - await mockAgentStreamError(page); + await test.step("2. the failure is reported, with what went wrong", async () => { + const error = page.getByTestId("chat-turn-error"); + await expect(error).toBeVisible(); + await expect(error).toContainText("could not finish this turn"); + await expect(error).toContainText("stopped responding"); + }); - await page.goto(`/agents/${agent}/chat`); - await waitForAppReady(page); - const input = page.getByTestId("chat-input"); - await expect(input).toBeEnabled(); + await test.step("3. the composer is usable again rather than stuck streaming", async () => { + await expect(page.getByTestId("chat-send")).toBeVisible(); + await expect(page.getByTestId("chat-cancel")).toHaveCount(0); + }); + + await test.step("4. retrying resends and clears the previous failure", async () => { + /* + * This step also covers a turn crossing a suspend, which is not obvious from + * reading it. + * + * A suspended conversation is one a reader can meet at any point — they may have + * suspended it themselves, or left it and come back. Retry begins its turn inside + * `useChat` and never passes through the composer, so a page that resumed only + * around `send` would leave this one button broken while every other path worked. + */ + await page.getByRole("button", { name: "Retry" }).click(); + // Still the failing scenario, so it fails again — the point is that the + // retry ran at all, and that the page did not double up the user's message. + await expect(page.getByTestId("chat-turn-error")).toBeVisible(); + await expect( + page.locator('[data-testid="chat-message"][data-role="user"]') + .filter({ hasText: "This one fails" }), + "a retry should resend the message, not duplicate the original", + ).toHaveCount(2); + }); - await input.fill(USER_MESSAGE); + await test.step("5. a healthy turn afterwards works", async () => { + await page.goto(AGENT_CHAT); + await page.getByTestId("chat-input").fill("Now it works"); await page.getByTestId("chat-send").click(); + await expect(page.getByTestId("chat-turn-error")).toHaveCount(0); + await expect(page.getByTestId("chat-send")).toBeVisible({ timeout: 20_000 }); + }); +}); + +test("chat: a turn can be stopped while it is streaming", async ({ page }) => { + await test.step("1. a long turn starts", async () => { + // The slow scenario exists so this is deterministic rather than a race + // against a stream that might already have finished. + await page.goto(`${AGENT_CHAT}?chat=slow`); + await page.getByTestId("chat-input").fill("This one gets stopped"); + await page.getByTestId("chat-send").click(); + await expect(page.getByTestId("chat-cancel")).toBeVisible(); + }); + + await test.step("2. stopping it reports a cancelled turn", async () => { + await page.getByTestId("chat-cancel").click(); + await expect(page.getByTestId("chat-status")).toHaveAttribute( + "data-state", + "canceled", + ); + }); + + await test.step("3. the composer returns and nothing further streams in", async () => { + await expect(page.getByTestId("chat-send")).toBeVisible(); + + const settled = await page.getByTestId("chat-message").count(); + await page.waitForTimeout(2_000); + await expect( + page.getByTestId("chat-message"), + "a stopped turn should stop producing messages", + ).toHaveCount(settled); + }); - await expect(page.locator('[data-sonner-toast][data-type="error"]')).toBeVisible(); - await expect(page.getByText(AGENT_REPLY)).toHaveCount(0); + await test.step("4. the conversation is still usable", async () => { + await expect(page.getByTestId("chat-input")).toBeEditable(); }); +}); + +test("chat: the list of other conversations reports its own failure", async ({ + page, +}) => { + await test.step("1. the list says it failed", async () => { + // The API scenario, not the chat one: the conversation itself and the list of the + // agent's other conversations fail independently, and this is the list. + await loadPage(page, AGENT_CHAT, { scenario: "error" }); + + const error = page.getByTestId("chat-sessions-error"); + await expect(error).toBeVisible(); + await expect(error).toContainText("Could not load conversations"); + }); + + await test.step("2. it is not mistaken for having no conversations", async () => { + await expect(page.getByTestId("chat-sessions-empty")).toHaveCount(0); + }); + + await test.step("3. it recovers", async () => { + await page.goto(withScenario(AGENT_CHAT, "ok")); + await expect(page.getByTestId("chat-sessions-error")).toHaveCount(0); + // This conversation is in its own rail, marked as the one that is open. + await expect( + page.getByTestId(`chat-session-${instances.ready}`), + ).toBeVisible(); + }); +}); - // region Reading — a missing session id shows the not-found screen - await test.step("shows session-not-found for a missing session", async () => { - await page.goto(`/agents/${agent}/chat/missing`); - await expect(page.getByRole("heading", { name: "Session not found" })).toBeVisible(); +/** + * A conversation the agent has parked on a question. + * + * This is the reported "the agent worked and then suddenly stopped": the agent called + * `ask_user`, its turn parked in `input_required`, and because that state is + * non-terminal it holds the instance's one active-task slot — so the controller + * refuses every further message with `FailedPrecondition`. Nothing on screen said any + * of that, because the question renders as ordinary agent prose and the conversation + * looks finished. + * + * There are two ways out and the journey drives both: answering, which is a message + * that *names* the parked turn and resumes it, and giving the question up, which + * cancels the task. A message that does neither is refused — and that refusal is the + * reported symptom. + * + * The fixture is the controller's behaviour, copied — the parked turn survives a + * reload, a send while it stands is refused in the controller's own words, and + * cancelling its task frees the conversation. + */ +test("chat: a question the agent is waiting on is said, and can be given up", async ({ + page, +}) => { + await test.step("1. a turn ends by asking rather than by finishing", async () => { + await page.goto(`${AGENT_CHAT}?chat=asks`); + await page.getByTestId("chat-input").fill("What should I order?"); + await page.getByTestId("chat-send").click(); + + // The question itself arrives as prose, which is exactly why it is not enough. + await expect( + page.getByTestId("chat-message").last(), + ).toContainText("What size pizza would you like?", { timeout: 20_000 }); + }); + + await test.step("2. the page says the agent is waiting, and does not call it a failure", async () => { + await expect(page.getByTestId("chat-awaiting-reply")).toBeVisible(); + // Nothing went wrong. A red alert over a turn that worked correctly would be a + // visible lie, and it is the reason this state was read as a broken agent. + await expect(page.getByTestId("chat-turn-error")).toHaveCount(0); + // And the lifecycle indicator agrees, rather than reporting a ready agent. + }); + + await test.step("3. its choices are offered as choices, and honour `multiple`", async () => { + // The payload carries two questions, one single-choice and one multi. Which + // control each gets is read from the question's own `multiple` flag — a + // single-choice question rendered as a multi-select sends an array the agent + // never asked for, and the runtime has no way to complain about it. + await expect(page.getByTestId("chat-question")).toHaveCount(2); + await expect( + page.getByTestId("chat-choices-0").locator(".ant-radio-input"), + "a single-choice question takes one answer", + ).toHaveCount(3); + await expect( + page.getByTestId("chat-choices-1").locator(".ant-checkbox-input"), + "a question marked `multiple` takes several", + ).toHaveCount(3); + + // And nothing can be sent until every question has an answer: the runtime pairs + // them positionally, so a gap answers the wrong question. + await expect(page.getByTestId("chat-answer-send")).toBeDisabled(); + }); + + await test.step("4. the question, its choices and all, survive a reload", async () => { + // The state belongs to the *task*, not to anything on screen — so a reader who + // comes back tomorrow meets the same question with the same options, rather than + // meeting a refusal. The payload is persisted with the task, which is what makes + // that possible; the choices are not re-derivable from the prose. + await page.reload(); + await expect(page.getByTestId("chat-awaiting-reply")).toBeVisible(); + await expect(page.getByTestId("chat-question")).toHaveCount(2); + await expect(page.getByTestId("chat-choices-0").locator(".ant-radio-input")).toHaveCount(3); + }); + + await test.step("5. answering it resumes the turn that asked, and the agent uses the answer", async () => { + await page.getByTestId("chat-choices-0").getByText("Large", { exact: true }).click(); + await page.getByTestId("chat-choices-1").getByText("Pineapple", { exact: true }).click(); + await expect(page.getByTestId("chat-answer-send")).toBeEnabled(); + await page.getByTestId("chat-answer-send").click(); + + // The choices are in the transcript as prose, so the conversation reads as what + // happened rather than as an empty message the agent somehow understood. + await expect( + page.locator('[data-testid="chat-message"][data-role="user"]').filter({ + hasText: "Large", + }), + ).toHaveCount(1); + + // And the *structured* answer arrived, which the prose alone cannot show. The + // fixture answers "I did not catch a choice in that" when the metadata is + // missing or its correlation id is wrong — the exact silent failure this whole + // path is at risk of. + await expect(page.getByTestId("chat-message").last()).toContainText( + "Noted: Large; Pineapple", + ); + await expect(page.getByTestId("chat-awaiting-reply")).toHaveCount(0); + }); + + await test.step("6. and the conversation takes ordinary messages again", async () => { + // The proof that the turn really closed is a *new* turn running, not an alert + // that disappeared: a task still parked would refuse this. + await page.goto(`${AGENT_CHAT}?chat=ok`); + await page.getByTestId("chat-input").fill("How many pods are running?"); + await page.getByTestId("chat-send").click(); + await expect(page.getByTestId("chat-cancel")).toHaveCount(0, { timeout: 30_000 }); + await expect(page.getByTestId("chat-turn-error")).toHaveCount(0); + }); +}); + +/** + * The other way out of a question: giving it up. + * + * Its own journey because it is a different ending, and because the state it starts + * from has to be reached from a clean session — the parked turn is remembered for the + * browsing session, so driving both endings in one test would have the second start + * from whatever the first left behind. + */ +test("chat: a question can be discarded instead of answered", async ({ page }) => { + await test.step("1. a turn parks on a question", async () => { + await page.goto(`${AGENT_CHAT}?chat=asks`); + await page.getByTestId("chat-input").fill("What should I order?"); + await page.getByTestId("chat-send").click(); + await expect(page.getByTestId("chat-awaiting-reply")).toBeVisible({ + timeout: 20_000, + }); + }); + + await test.step("2. discarding it frees the conversation", async () => { + await page.getByTestId("chat-dismiss-question").click(); + await expect(page.getByTestId("chat-awaiting-reply")).toHaveCount(0); + // Either reading is correct here. Giving up the question ends the turn; whether the + // conversation is still logically ready depends on what has been asked of it since, + // and this step is about the question being gone rather than about the state. + }); + + await test.step("3. and an unrelated message is accepted again", async () => { + await page.goto(`${AGENT_CHAT}?chat=ok`); + await page.getByTestId("chat-input").fill("How many pods are running?"); + await page.getByTestId("chat-send").click(); + await expect(page.getByTestId("chat-cancel")).toHaveCount(0, { timeout: 30_000 }); + await expect(page.getByTestId("chat-turn-error")).toHaveCount(0); }); }); diff --git a/ui/playwright/tests/chat/chat.spec.ts b/ui/playwright/tests/chat/chat.spec.ts index 78431aeed..82fdb5ead 100644 --- a/ui/playwright/tests/chat/chat.spec.ts +++ b/ui/playwright/tests/chat/chat.spec.ts @@ -1,40 +1,397 @@ import { test, expect } from "../../fixtures/test"; -import { waitForAppReady } from "../../helpers/page"; -import { firstReadyAgent } from "../../helpers/resources"; - -// Chat / session — success journey. Agent resolve, session create, and session -// lookups hit the real backend; only the A2A chat stream is mocked — by the proxy -// (mocks/server.mjs), which answers /a2a with a canned agent reply so the suite -// never needs a live LLM. The proxy echoes the request's contextId, so the reply -// lines up with the session the backend created. -// -// The agent under test is discovered at runtime (firstReadyAgent) rather than -// hard-coded, so the suite isn't tied to a specific seeded agent — it just needs -// one deployment-ready agent to exist. Error journeys live in chat-errors.spec.ts. - -const USER_MESSAGE = "List the pods please"; -const AGENT_REPLY = "Hello from the agent"; // the proxy's canned reply text - -test("chat: send and receive a reply", async ({ page }) => { - const chatUrl = `/agents/${await firstReadyAgent()}/chat`; - - // region Reading — the empty state before any message - await test.step("opens on the empty state before any message", async () => { - await page.goto(chatUrl); - await waitForAppReady(page); - await expect(page.getByRole("heading", { name: "Start a conversation" })).toBeVisible(); +import { SIBLING_OF_READY, agentChat, instances, loadPage } from "../../helpers/app"; + +/** + * Chat — the conversation journey. + * + * One continuous story, because that is what a conversation is: open an agent that + * already has history, add a turn to it, watch the turn stream in, add another, and + * see the whole thing survive a reload. Splitting these would lose the only thing + * worth checking — that the message you sent is the one that ended up in the + * transcript you reload. + * + * ## The fixture is silent about the reader's message, because the gateway is + * + * `MockChatClient` records what the reader sent and never announces it, which is + * what the A2A gateway does: measured on 2026-08-24, a completed turn emits a + * `WORKING` frame and a `COMPLETED` frame and no message frame at all, while + * `ListTasks` afterwards holds the user's message and nothing else. The fixture used + * to echo it, and that one generosity kept this file green while the reader's own + * words were invisible on a cluster until they reloaded. Every "the message is on + * screen" assertion below therefore checks the client put it there. + * + * ## There is no session to open first + * + * An `AgentInstance` *is* the conversation: the A2A gateway files every task under + * the instance as its `contextId`, and `ListTasks` for the instance is the + * transcript. So arriving at an agent is arriving at its conversation, and the rail + * beside it lists the *other* conversations with the same agent — which are the + * sibling instances of the same harness and template. + */ + +/** The instance with a seeded conversation behind it. */ +const AGENT_CHAT = agentChat(instances.ready); + +const FIRST_QUESTION = "How many pods are running?"; +const SECOND_QUESTION = "And which of them is the reconciler?"; + +/** + * The reply to a given question, as it *renders* rather than as it is written. + * + * The fixture answers in Markdown, so the bold and the backticks contribute their text + * and the list items become their own lines. Asserting the rendered text keeps this a + * check on the transcript rather than on the renderer, which the Markdown test below + * covers separately. + * + * Taking the question as an argument is not tidiness: the fixture quotes it back, so + * the two turns below have *different* replies — and an assertion that could not tell + * them apart would pass on a second turn that re-rendered the first one's answer. + */ +const replyTo = (question: string) => + [ + "There are 3 pods running in the kagent namespace:", + "", + "kagent-controller — the reconciler", + "kagent-ui — this page", + "kagent-tools — the tool server", + "", + `You asked: "${question}".`, + ].join("\n"); + +test("chat: history, sending, streaming, and tool rendering", async ({ page }) => { + const messages = page.getByTestId("chat-message"); + // Scoped by role: the agent quotes the question back in its reply, so a plain + // text filter matches the answer as well as the question that prompted it. + const userMessages = page.locator('[data-testid="chat-message"][data-role="user"]'); + + await test.step("1. the page opens on the conversation, ready to be typed into", async () => { + await loadPage(page, AGENT_CHAT); + // Nothing has to be picked or clicked first: the agent is the conversation. await expect(page.getByTestId("chat-input")).toBeVisible(); + await expect(page.getByTestId("chat-sessions")).toBeVisible(); + }); + + await test.step("3. its history is already loaded", async () => { + // The seeded conversation: a question, a tool call, its result, an answer. + await expect(messages).toHaveCount(4); + // The rail marks which conversation is open — this one. + await expect( + page.getByTestId(`chat-session-${instances.ready}`), + ).toHaveAttribute("data-active", "true"); }); - // region Sending — send a message and render the (proxy-mocked) agent reply - await test.step("sends a message and renders the agent reply", async () => { - const input = page.getByTestId("chat-input"); - await expect(input).toBeEnabled(); + await test.step("4. a tool call and its result render as themselves, not as JSON blobs", async () => { + const call = page.getByTestId("chat-tool-call"); + await expect(call).toHaveCount(1); + await expect(call).toHaveAttribute("data-tool-name", "k8s_get_events"); - await input.fill(USER_MESSAGE); + const result = page.getByTestId("chat-tool-result"); + await expect(result).toHaveCount(1); + await expect(result).toContainText("liveness probe failed"); + }); + + await test.step("5. messages are attributed to who said them", async () => { + await expect(messages.first()).toHaveAttribute("data-role", "user"); + await expect(messages.last()).toHaveAttribute("data-role", "agent"); + }); + + await test.step("6. nothing rules the conversation off from the box it is typed into", async () => { + // Reported as a defect: a border above the composer with a band of empty space + // over it, riding across the transcript as the page scrolled underneath. The + // computed style is the assertion because a rule of the page's own background + // colour is invisible in a screenshot and still wrong. + const borderTop = await page + .getByTestId("chat-composer") + .evaluate((row) => getComputedStyle(row).borderTopWidth); + expect(borderTop, "the composer should carry no rule above it").toBe("0px"); + }); + + await test.step("7. sending a message adds it to the transcript immediately", async () => { + await page.getByTestId("chat-input").fill(FIRST_QUESTION); await page.getByTestId("chat-send").click(); - await expect(page.getByText(USER_MESSAGE)).toBeVisible(); - await expect(page.getByText(AGENT_REPLY)).toBeVisible(); + // The reader's own words, before the server has said anything. The fixture + // does not echo them back and neither does the gateway, so this can only pass + // if the client put them there itself. + await expect(userMessages.filter({ hasText: FIRST_QUESTION })).toHaveCount(1); + // Composer clears, so the next message does not start with the last one. + await expect(page.getByTestId("chat-input")).toHaveValue(""); + }); + + await test.step("8. the turn reports itself as running, and offers a way out", async () => { + await expect(page.getByTestId("chat-cancel")).toBeVisible(); + // The transcript's own line, which is now the only place a turn is reported: the + // separate lifecycle bar under the composer said the same thing twice and went. + await expect(page.getByTestId("chat-status")).toBeVisible(); }); + + await test.step("9. the turn's tool call and result arrive", async () => { + await expect(page.getByTestId("chat-tool-call")).toHaveCount(2); + await expect( + page.getByTestId("chat-tool-result").last(), + ).toContainText("3 pods running in kagent"); + }); + + await test.step("10. the streamed reply lands exactly once", async () => { + // Exact text, not a substring. Streaming appends, and the failure mode that + // actually happened here was a doubled first chunk ("There There are 3 + // pods…") — which every `toContainText` in this file would have passed. + await expect( + messages.last().getByTestId("chat-message-text"), + "the streamed reply should assemble exactly once", + ).toHaveText(replyTo(FIRST_QUESTION)); + }); + + await test.step("11. the turn finishes, the composer comes back, and the indicator settles", async () => { + await expect(page.getByTestId("chat-send")).toBeVisible(); + await expect(page.getByTestId("chat-cancel")).toHaveCount(0); + // Nothing reported once the turn is over: the status line belongs to a turn in + // flight, so a finished one leaves it with nothing to say. + await expect(page.getByTestId("chat-status")).toHaveCount(0); + }); + + await test.step("12. a second question behaves exactly like the first", async () => { + // The report was that further questions behaved the same way — only the agent's + // replies appeared. So the second turn is driven, not assumed from the first. + await page.getByTestId("chat-input").fill(SECOND_QUESTION); + await page.getByTestId("chat-send").click(); + + await expect( + userMessages.filter({ hasText: SECOND_QUESTION }), + "the second question should be on screen as soon as it is sent, like the first", + ).toHaveCount(1); + + await expect(page.getByTestId("chat-cancel")).toHaveCount(0); + await expect( + messages.last().getByTestId("chat-message-text"), + "the second reply should assemble exactly once", + ).toHaveText(replyTo(SECOND_QUESTION)); + }); + + await test.step("13. a reload shows the identical conversation, not merely a similar one", async () => { + // The acceptance the report asked for, and the strongest form of it available: + // the same messages, saying the same things, in the same order. A count would + // pass on a transcript that had lost the questions and gained two replies. + const before = await messages.allInnerTexts(); + // Twelve: the seeded four, plus four for each turn driven above — the + // question, the tool call, its result and the reply. Stated as a number rather + // than read from the page, so a transcript that quietly lost the questions + // cannot define its own expectation. + expect(before.length, "the seeded four plus four for each of two turns").toBe(12); + + await page.reload(); + await expect(page.getByTestId("chat-input")).toBeVisible(); + await expect(messages).toHaveCount(before.length); + expect( + await messages.allInnerTexts(), + "the reloaded conversation should be the one that was on screen", + ).toEqual(before); + + // And the reader's own words are among them — which before this fix was the + // only moment they ever appeared. + await expect(userMessages.filter({ hasText: FIRST_QUESTION })).toHaveCount(1); + await expect(userMessages.filter({ hasText: SECOND_QUESTION })).toHaveCount(1); + }); + + await test.step("14. and the composer does not sit on top of the conversation", async () => { + /* + * The other half of the same report — "a border above the prompt input which + * overlaps the rendered conversation". Removing the rule stopped a line being + * drawn across the transcript; this is about the box itself. + * + * The composer is `position: sticky; bottom: 0`, and the transcript scrolls its + * own sentinel to the foot of the viewport after every turn — so the last + * message lands directly under the box. Measured at 23px of overlap on a 420px + * viewport, which is a line of text. + * + * The state driven here is the one the page puts *itself* in: a turn, and then + * whatever the auto-scroll does. Scrolling manually to the very bottom would + * measure a different position, one where the composer is at its natural place + * and nothing overlaps whatever the transcript does — which is how an earlier + * version of this assertion came to pass on the broken build. + * + * Boxes, not a screenshot: a few pixels of overlap cover a line of text and are + * not something a glance at a still will catch. + */ + await page.setViewportSize({ width: 1440, height: 420 }); + await page.getByTestId("chat-input").fill("one more, to make the page scroll"); + await page.getByTestId("chat-send").click(); + await expect(page.getByTestId("chat-cancel")).toHaveCount(0); + + /* + * The box that scrolls must end above the box you type in. + * + * This used to measure the last message against the composer, which was the right + * question while the transcript sat in the page's flow and could run underneath it. + * The transcript owns a scroll box of its own now, so the last message is often + * legitimately outside the visible area and its position says nothing — the + * property that survives is that the two regions do not overlap. + */ + const gap = await page.evaluate(() => { + const transcript = document.querySelector('[data-testid="chat-transcript"]'); + const composer = document.querySelector('[data-testid="chat-composer"]'); + if (!transcript || !composer) return null; + const box = transcript.parentElement; + if (!box) return null; + return composer.getBoundingClientRect().top - box.getBoundingClientRect().bottom; + }); + + expect(gap, "the transcript and the composer should both be on the page").not.toBeNull(); + expect( + gap as number, + "the foot of the conversation should be clear of the composer, not under it", + ).toBeGreaterThanOrEqual(0); + + await page.setViewportSize({ width: 1440, height: 900 }); + }); + + await test.step("15. switching conversations does not leak the previous one", async () => { + // A sibling instance: the same harness and template, so the rail lists it as + // another conversation with this agent. + await page.getByTestId(`chat-session-${SIBLING_OF_READY}`).click(); + await page.waitForURL(new RegExp(`/agents/kagent/${SIBLING_OF_READY}/chat$`)); + + await expect(page.getByTestId("chat-empty")).toBeVisible(); + await expect( + page.getByTestId("chat-message"), + "the previous conversation's messages should not carry over", + ).toHaveCount(0); + }); + + await test.step("16. a suspended agent still takes a message, and resuming is the page's job", async () => { + /* + * Suspended is not "cannot answer" — it is "not resumed yet". + * + * The gateway does refuse a message for a suspended instance, and the composer used + * to be disabled because of it. But the reader's intention is unambiguous: they + * typed something and pressed send. Making them find a Resume control first — + * having just been told the agent gave its worker back at the end of the last turn + * — is a step the page can take for them rather than a detour on every message. + */ + await expect(page.getByTestId("chat-input")).toBeEnabled(); + // And no alert claiming it cannot answer, because it can. + await expect(page.getByTestId("chat-not-ready")).toHaveCount(0); + }); +}); + +/** + * Scrolling away from the foot offers a way back, and sending returns there. + * + * Both were broken by the same thing and neither showed up as a failure. The + * transcript used to be scrolled by the page, so the sentinel that decides whether the + * reader is at the bottom was observed against the viewport. Once the transcript owned + * its own scroll box that question was about the wrong element: the sentinel stayed + * inside the window whether or not the reader had scrolled away from it, so the + * observer went on reporting "at the bottom", the button never appeared, and following + * a turn scrolled whatever ancestor `scrollIntoView` happened to pick. + * + * Asserted by scrolling the box rather than by looking at it, because the failure is + * invisible in a screenshot — the transcript looks the same either way. + */ +test("chat: leaving the foot of a conversation offers a way back to it", async ({ page }) => { + // A short window, so the conversation genuinely overflows its box rather than being + // forced to by a style this test applied — the second measures the test's own hack. + await page.setViewportSize({ width: 1280, height: 460 }); + await loadPage(page, AGENT_CHAT); + await expect(page.getByTestId("chat-message").first()).toBeVisible({ timeout: 30_000 }); + + const box = page.getByTestId("chat-transcript").locator("xpath=.."); + const button = page.getByTestId("chat-scroll-bottom"); + + await test.step("1. at the foot, there is nothing to offer", async () => { + await expect(button).toHaveCount(0); + }); + + await test.step("2. scrolling up offers the way back", async () => { + await box.evaluate((node) => node.scrollTo({ top: 0 })); + await expect(button).toBeVisible({ timeout: 10_000 }); + }); + + await test.step("3. and it takes them there", async () => { + await button.click(); + /* + * The button going is the assertion, not a scroll offset. + * + * It is set by the same observer that decides whether the reader is at the foot, + * so it going away *is* the transcript reporting that they are — and it is the + * thing a reader sees. Measuring the offset instead reads a moving target: a + * transcript that re-reads itself on a timer changes height under the assertion. + */ + await expect(button).toHaveCount(0, { timeout: 10_000 }); + }); +}); + +/** + * The composer does not move when the conversation changes under it. + * + * The panel was `grid-template-rows: auto 1fr auto`, which assumes exactly three + * children — and it has a varying number, because the notices above the transcript come + * and go with the conversation's state. A fourth child pushed the `1fr` onto a + * different row, so the transcript stopped being the part that grows and the composer + * moved instead: a visible flicker when clicking between conversations, worst on short + * ones where the composer is not pinned to the foot of the viewport anyway. + * + * Measured rather than eyeballed, because a couple of pixels is exactly the size of the + * problem and is invisible in a screenshot. + */ +test("chat: the composer stays put when switching conversations", async ({ page }) => { + await loadPage(page, AGENT_CHAT); + await expect(page.getByTestId("chat-composer")).toBeVisible({ timeout: 30_000 }); + // After the transcript has arrived: measuring mid-read compares a loading state with a + // loaded one, which is a different question from the one this asks. + await expect(page.getByTestId("chat-message").first()).toBeVisible({ timeout: 30_000 }); + + const composerTop = () => + page.getByTestId("chat-composer").evaluate((node) => node.getBoundingClientRect().top); + const before = await composerTop(); + + const rail = page.getByTestId("chat-sessions"); + await rail.locator(`a[data-testid="chat-session-${SIBLING_OF_READY}"]`).click(); + await page.waitForURL(new RegExp(`/agents/kagent/${SIBLING_OF_READY}/chat$`)); + await expect(page.getByTestId("chat-composer")).toBeVisible(); + // Settled, not mid-transition — the assertion is about where it ends up. + await page.waitForTimeout(1000); + + expect( + Math.abs((await composerTop()) - before), + "switching conversations should not move the message box", + ).toBeLessThanOrEqual(1); +}); + +/** + * An agent's answer is Markdown, and this page renders it. + * + * Worth its own test because the failure is silent in both directions: a broken + * renderer shows `**3 pods**` with the asterisks intact, which still reads as an + * answer, and a renderer given raw HTML would run it. So this asserts the elements + * exist — not the text, which is identical either way. + */ +test("chat: an agent's Markdown renders as elements, not as characters", async ({ page }) => { + await loadPage(page, AGENT_CHAT); + // The seeded history, before typing: sending early would race the load rather than + // the render, and the assertion below is about the render. + await expect(page.getByTestId("chat-message")).toHaveCount(4); + + await page.getByTestId("chat-input").fill("How many pods are running?"); + await expect(page.getByTestId("chat-send")).toBeEnabled(); + await page.getByTestId("chat-send").click(); + + // The turn runs, then finishes. Both waits matter: without the first this can read the + // transcript before the reply exists, and without the second it can read a half-streamed + // list whose item count is whatever had arrived. + await expect(page.getByTestId("chat-cancel")).toBeVisible(); + await expect(page.getByTestId("chat-cancel")).toHaveCount(0); + + const answer = page.getByTestId("chat-message").last().getByTestId("chat-message-text"); + + // Emphasis and code became elements rather than surviving as punctuation. + await expect(answer.locator("strong")).toHaveText("3 pods"); + await expect(answer.locator("code").first()).toHaveText("kagent"); + + // The list is a list, so it reads as one to anything that is not a pair of eyes. + await expect(answer.locator("ul li")).toHaveCount(3); + + // And the source characters are gone: this is what fails when the renderer is + // bypassed and the raw Markdown is printed instead. + await expect(answer).not.toContainText("**"); }); diff --git a/ui/playwright/tests/chat/shared-conversation.spec.ts b/ui/playwright/tests/chat/shared-conversation.spec.ts new file mode 100644 index 000000000..dc57b1c9a --- /dev/null +++ b/ui/playwright/tests/chat/shared-conversation.spec.ts @@ -0,0 +1,72 @@ +import { expect, test } from "../../fixtures/test"; + +/** + * Opening a conversation somebody shared. + * + * ## What is left of sharing, and why + * + * Only this half. The controller's share RPCs identify a *session*, and the gRPC + * interceptor that honours `X-Share-Token` resolves it through + * `GetSessionShareByToken` — so a share is a capability over a session and nothing + * else. Chat is addressed by `AgentInstance` now, and while + * `AgentInstanceService` does carry `CreateAgentInstanceShare`, nothing on the read + * path validates the token it mints: the A2A gateway authorises on the instance + * instead. A Share button would therefore hand somebody a link that cannot be + * opened, which is worse than no button. + * + * So the UI mints no tokens, and this spec covers what remains true: a link issued + * before still opens, still says what it is, and still stops working when revoked. + * `playwright/DEFERRED.md` records what it would take to share a conversation again. + * + * The token is seeded in `src/mocks/state.ts` rather than created through the UI, + * because there is no longer a control that creates one — the fixture equivalent of + * a link somebody was sent last week. + */ + +const TOKEN = "mock-share-token-1"; +const SESSION = "session-8f31"; +const SHARED = `/shared/${SESSION}/${TOKEN}`; + +test("shared conversation: a link issued earlier opens, read-only", async ({ page }) => { + await test.step("1. it shows the conversation and says what it is", async () => { + await page.goto(SHARED); + + // Said on the page, not only in the URL. A reader who was sent a link has no + // other way to know that this is somebody else's conversation, or why there is + // nowhere to reply. + await expect(page.getByTestId("shared-session-notice")).toContainText("read-only", { + timeout: 30_000, + }); + await expect(page.getByTestId("shared-session-transcript")).toBeVisible(); + await expect(page.getByTestId("shared-session-error")).toHaveCount(0); + }); + + await test.step("2. the transcript is the shared conversation's own", async () => { + // The seeded session's turns, read through `sessions.tasks` — not the A2A + // gateway, which knows nothing about sessions. A page that rendered an empty + // transcript would look like a working share of an empty conversation. + await expect(page.getByTestId("chat-message").first()).toBeVisible({ + timeout: 30_000, + }); + }); + + await test.step("3. there is no composer, because a share is for reading", async () => { + // An input that could not send would be worse than none. + await expect(page.getByTestId("chat-input")).toHaveCount(0); + }); +}); + +test("shared conversation: a token the backend never issued is refused", async ({ + page, +}) => { + // The claim this makes is about the header being sent, not about the page + // rendering: the fixture backend refuses a token it cannot resolve, exactly as the + // controller does, so a build that stopped sending `X-Share-Token` would serve an + // unauthenticated read and the miss would read on screen as success. + await page.goto(`/shared/${SESSION}/not-a-real-token`); + + await expect(page.getByTestId("shared-session-error")).toBeVisible({ + timeout: 30_000, + }); + await expect(page.getByTestId("shared-session-transcript")).toHaveCount(0); +}); diff --git a/ui/playwright/tests/cleanup.spec.ts b/ui/playwright/tests/cleanup.spec.ts deleted file mode 100644 index a64aedd4d..000000000 --- a/ui/playwright/tests/cleanup.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { test, expect } from "../fixtures/test"; -import { - deleteAgent, - deleteModelConfig, - deletePromptTemplate, - deleteToolServer, - listAgents, - listModelConfigs, - listPromptTemplateRefs, - listToolServerRefs, -} from "../helpers/grpc"; - -// Housekeeping — delete any leftover e2e-* resources. Every other suite already -// deletes what it creates on a green run, so this only matters when a run crashed -// mid-way (leaving a uniquely-named resource behind). It sweeps the resource CRDs -// by name prefix via the controller's gRPC API; it asserts nothing about product -// behaviour. -// -// Only names starting with this prefix are touched, so seeded resources -// (k8s-agent, default-model-config, …) are never at risk. -const PREFIX = "e2e-"; - -const isTestRef = (ref: string | null): ref is string => !!ref && (ref.split("/")[1] ?? "").startsWith(PREFIX); - -test("cleanup: remove leftover e2e resources", async () => { - // region Reading — collect leftover e2e-* refs across resource types - const [agents, toolServers, modelConfigs, prompts] = await Promise.all([ - listAgents(), - listToolServerRefs(), - listModelConfigs(), - listPromptTemplateRefs("kagent"), - ]); - - // region Deleting — delete each leftover - for (const agent of agents.filter((item) => isTestRef(`${item.namespace}/${item.name}`))) { - await deleteAgent(agent); - } - for (const ref of toolServers.filter(isTestRef)) { - await deleteToolServer(ref); - } - for (const config of modelConfigs.filter((item) => isTestRef(item.ref))) { - await deleteModelConfig(config.ref); - } - for (const ref of prompts.filter(isTestRef)) { - await deletePromptTemplate(ref); - } - - // Best-effort housekeeping — the exact leftovers vary run to run, so there's - // nothing meaningful to assert beyond "the sweep ran". - expect(test.info().errors).toEqual([]); -}); diff --git a/ui/playwright/tests/extensions/extension-points-absent.spec.ts b/ui/playwright/tests/extensions/extension-points-absent.spec.ts new file mode 100644 index 000000000..d66bf5281 --- /dev/null +++ b/ui/playwright/tests/extensions/extension-points-absent.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from "../../fixtures/test"; +import { agents, loadPage, routes } from "../../helpers/app"; +import { expectShell } from "../../helpers/nav"; +import { CORE_NAV_ORDER, allSlots, navOrder } from "../../helpers/extensions"; + +/** + * Extension points — with nothing installed. + * + * This is the shape a default build takes, and it is the case most likely to rot + * unnoticed, because every other spec and every screenshot is taken with the + * example switched on. A framework that only works when something is plugged + * into it is a framework that breaks the day a deployment ships bare. + */ + +test("extension points: a bare build renders the application and nothing else", async ({ + page, +}) => { + await test.step("1. no point mounts anything anywhere", async () => { + for (const [path, title] of [ + [routes.dashboard, "Dashboard"], + [routes.agents, "Agents"], + [routes.models, "Models"], + ] as const) { + // Wait for the page to have actually rendered before asserting an absence. + // "Nothing is here" is trivially true of a page that has not mounted yet, + // so without this anchor the whole step would pass on a blank screen. + await loadPage(page, path, { title }); + await expect( + allSlots(page), + `${path} rendered a vendor slot with no extension installed`, + ).toHaveCount(0); + } + }); + + await test.step("2. the sidebar shows exactly what the application ships", async () => { + expect(await navOrder(page)).toEqual(CORE_NAV_ORDER); + }); + + await test.step("3. the app is otherwise whole", async () => { + // The absence of contributions must not take any of the app with it. + await loadPage(page, routes.agents, { title: "Agents" }); + await expectShell(page); + // By the harness as well as the template, because an agent is the pair: the + // template alone appears on two rows, so matching it would pass on a build that + // had lost the harness column entirely. + await expect( + page + .getByRole("row") + .filter({ hasText: agents.k8s.template }) + .filter({ hasText: agents.k8s.harness }), + ).toHaveCount(1); + }); + + await test.step("4. a route only an extension would contribute is a 404", async () => { + // The example contributes this path; with nothing installed the router must + // not have quietly kept a slot for it. + await loadPage(page, "/example/insights"); + await expect(page.getByTestId("not-found")).toBeVisible(); + await expect(allSlots(page)).toHaveCount(0); + }); +}); diff --git a/ui/playwright/tests/extensions/extension-points.vendor.spec.ts b/ui/playwright/tests/extensions/extension-points.vendor.spec.ts new file mode 100644 index 000000000..ab013b1bb --- /dev/null +++ b/ui/playwright/tests/extensions/extension-points.vendor.spec.ts @@ -0,0 +1,179 @@ +import { test, expect } from "../../fixtures/test"; +import { + agentChat, + agentPage, + agents, + dataRows, + expectSettled, + instances, + loadPage, + rowNamed, + routes, +} from "../../helpers/app"; +import { expectShell } from "../../helpers/nav"; +import { + CORE_NAV_ORDER, + expectInline, + expectPortalled, + navOrder, + slot, +} from "../../helpers/extensions"; + +/** + * Extension points — the framework's contract, with an extension installed. + * + * Runs against the vendor server (see `playwright.config.ts`). Every assertion + * here is about the *mechanism*: that a component configured at a point mounts + * at that point, in the DOM position the point promises, carrying the context + * the point declares. None of it asserts what the bundled example renders, so + * reshaping the example does not churn this file. + */ + +test("extension points: configured components mount where the point promises", async ({ + page, +}) => { + await test.step("1. shell points mount on every in-app page", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + await expectShell(page); + + await expect(slot(page, "app_shell_appLayout_contentArea_leadingBanner")).toHaveCount(1); + await expect(slot(page, "app_shell_appLayout_appSidebar_footer")).toHaveCount(1); + }); + + await test.step("2. an inline point renders where the slot sits", async () => { + await expectInline( + page, + "app_shell_appLayout_contentArea_leadingBanner", + page.getByTestId("app-content"), + ); + await expectInline( + page, + "app_shell_appLayout_appSidebar_footer", + page.getByTestId("app-sidebar"), + ); + }); + + await test.step("3. the portal point escapes the scrolling content area", async () => { + // The one point whose render mode is observable only from where it landed. + await expectPortalled(page, "app_shell_appLayout_contentArea_globalOverlay"); + }); + + await test.step("4. page-level points mount on the page that declares them", async () => { + await expect(slot(page, "app_agents_agentsList_pageHeader_actions")).toHaveCount(1); + }); + + await test.step("5. a per-row point mounts once per row", async () => { + const badge = "app_agents_agentsList_agentListItem_badge"; + // On one agent's page, because that is where `AgentInstance` rows are listed + // now: an instance is a conversation, so the agents list holds pairs and the + // conversations sit inside one. The point's id and its context are unchanged — + // it was always a point about an instance — which is the property this step is + // really protecting: a contribution written against it keeps working. + await loadPage(page, agentPage(agents.k8s)); + // One per row, whatever the list holds — counted from the table rather than + // written down, so a fixture gaining a conversation is not a false failure while + // a slot that mounted twice or not at all still fails. Waited for, because + // counting too early compares a slot count against nought rows and passes for a + // slot that never mounted. + await expect(dataRows(page).first()).toBeVisible({ timeout: 30_000 }); + await expectSettled(page); + const rows = await dataRows(page).count(); + expect(rows).toBeGreaterThan(1); + await expect(slot(page, badge)).toHaveCount(rows); + + // And each row carries its own, located by the short id the table shows beside + // the conversation's name. + for (const id of [instances.ready, instances.suspended]) { + await expect( + rowNamed(page, id.slice(0, 8)).locator(`[data-testid="vendor-slot-${badge}"]`), + `row "${id}" should carry its own slot`, + ).toHaveCount(1); + } + }); + + await test.step("6. each row's slot receives that row's context, not a shared one", async () => { + // The trap: every badge renders identical copy, so a component that ignored + // its context entirely would satisfy any text assertion. What proves context + // is per-row is that what each one rendered is *distinguishable* — so this + // asserts distinctness and deliberately says nothing about the values. + const rendered = await slot(page, "app_agents_agentsList_agentListItem_badge") + .evaluateAll((nodes) => + nodes.map((node) => node.firstElementChild?.getAttribute("data-testid") ?? ""), + ); + + // Counted against the list rather than against a written-down number, so a + // fixture gaining an agent is not a false failure — while a contribution that + // rendered nothing, or the same thing for every row, still fails. + expect( + rendered.length, + "every row should have rendered something", + ).toBeGreaterThan(1); + expect( + rendered.filter((value) => value !== ""), + "each contribution should identify itself from its context", + ).toHaveLength(rendered.length); + expect( + new Set(rendered).size, + `contributions were indistinguishable, so context is not per-row: ${rendered.join(", ")}`, + ).toBe(rendered.length); + }); + + await test.step("7. a contributed nav entry composes into the core list by order", async () => { + // Asserted positionally rather than by name: the invariant is that a + // contribution can land *between* core entries instead of being appended, + // which is what "composes" means here. + const order = await navOrder(page); + const extras = order.filter((id) => !CORE_NAV_ORDER.includes(id)); + + expect(extras, "expected exactly one contributed nav entry").toHaveLength(1); + expect(order.indexOf(extras[0])).toBeGreaterThan(order.indexOf("nav-agents")); + expect(order.indexOf(extras[0])).toBeLessThan(order.indexOf("nav-models")); + + // The core entries keep their own relative order around the insertion. + expect(order.filter((id) => CORE_NAV_ORDER.includes(id))).toEqual(CORE_NAV_ORDER); + }); + + await test.step("8. a per-message point mounts once per message, with its own context", async () => { + // The agent with a seeded conversation behind it. There is no session segment: + // an AgentInstance *is* the conversation. + await loadPage(page, agentChat(instances.ready)); + const point = "app_agents_agentChat_agentChatMessage_additionalActionsButton"; + + const messages = page.getByTestId("chat-message"); + await expect(messages).toHaveCount(4); + await expect(slot(page, point)).toHaveCount(4); + + // Same trap as the per-row badge: every contribution renders the same label, + // so only distinctness proves each one received its own message. + const rendered = await slot(page, point).evaluateAll((nodes) => + nodes.map((node) => node.firstElementChild?.getAttribute("data-testid") ?? ""), + ); + expect(rendered.filter(Boolean)).toHaveLength(4); + expect( + new Set(rendered).size, + `contributions were indistinguishable, so context is not per-message: ${rendered.join(", ")}`, + ).toBe(4); + }); + + await test.step("9. a point on another page mounts there and only there", async () => { + // The dashboard point is the one place a contribution appears that is not + // the agents area, so it also checks that page-scoped points stay scoped. + const dashboardPoint = "app_dashboard_dashboardOverview_summaryGrid_leadingCard"; + await expect(slot(page, dashboardPoint)).toHaveCount(0); + + await loadPage(page, routes.dashboard, { title: "Dashboard" }); + await expect(slot(page, dashboardPoint)).toHaveCount(1); + await expectInline(page, dashboardPoint, page.getByTestId("app-content")); + }); + + await test.step("10. a contributed route renders inside the shell", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + const order = await navOrder(page); + const contributed = order.find((id) => !CORE_NAV_ORDER.includes(id))!; + + await page.getByTestId(contributed).click(); + await expect(page.getByTestId("page-title")).toBeVisible(); + // A contributed page is a page of this app, not a separate site. + await expectShell(page); + }); +}); diff --git a/ui/playwright/tests/lists/list-filters.spec.ts b/ui/playwright/tests/lists/list-filters.spec.ts new file mode 100644 index 000000000..bd81ef432 --- /dev/null +++ b/ui/playwright/tests/lists/list-filters.spec.ts @@ -0,0 +1,324 @@ +import type { Page } from "@playwright/test"; +import { test, expect } from "../../fixtures/test"; +import { dataRows, expectSettled, loadPage, rowNamed, routes } from "../../helpers/app"; + +/** + * The filter bar, the URL state behind it, and what the three list pages claim about + * where their narrowing happens. + * + * These are the properties the shared machinery exists for, and each of them replaces + * something a page was previously doing worse: + * + * - **Selecting no namespaces means every namespace.** The old pattern was a separate + * "all namespaces" toggle beside a single-select — two controls answering one + * question, able to disagree. An empty multi-select has one state. + * - **The choices are visible without opening the control.** A trigger reading + * "2 selected" hides which two, and "why can I not see the row I am looking for" is + * the question a filtered list most often provokes. + * - **The view is in the address**, so it can be linked to and survives a reload. + * - **The pages say where the narrowing happens.** All three of these RPCs return the + * whole list — `ListModelConfigs` and `ListToolServers` take an empty request, and + * `ListPromptTemplates` takes only a namespace — so a search in the browser searches + * every row. That is the fact the note on each page states, and it is what makes a + * client-side control honest here where it would not be on the substrate page's + * paged tables. + * + * Driving the antd multi-select needs one piece of local knowledge, which is why it + * has a helper: rc-select renders a *second*, invisible `role="listbox"` for screen + * readers, and `getByRole("option")` resolves to that one and then waits forever for a + * visibility that never arrives — reporting the option as absent while it is on screen + * the whole time. Locating `.ant-select-item-option` by its title is what actually + * points at the row a person clicks. + */ + +/** Opens a filter's popup and ticks one option by the label the reader sees. */ +async function chooseFilter(page: Page, filterTestId: string, label: string) { + await page.getByTestId(filterTestId).click(); + await page.locator(`.ant-select-item-option[title="${label}"]`).click(); + // Otherwise the popup covers the pill row the next step asserts on. + await page.keyboard.press("Escape"); +} + +test("lists: no namespaces chosen means every namespace, and each choice becomes a pill", async ({ + page, +}) => { + await test.step("1. the page opens unnarrowed, with no pills at all", async () => { + await loadPage(page, routes.models, { title: "Models" }); + await expectSettled(page); + + // Four configurations across three namespaces — the whole fixture set, which is + // what "nothing selected" has to mean. A control that read an empty selection as + // "narrow to nothing" would show an empty table here. + await expect(dataRows(page)).toHaveCount(4); + await expect(page.getByTestId("models-filters-pills")).toHaveCount(0); + await expect(page.getByTestId("models-filters-pill-clear")).toHaveCount(0); + }); + + await test.step("2. choosing one namespace narrows the list and raises one pill", async () => { + await chooseFilter(page, "models-filters-filter-ns", "kagent"); + + await expect(page.getByTestId("models-filters-pill-ns-kagent")).toContainText( + "Namespace: kagent", + ); + await expect(rowNamed(page, "default-model-config")).toHaveCount(1); + await expect(rowNamed(page, "ollama-local")).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(2); + }); + + await test.step("3. a second namespace adds to the first rather than replacing it", async () => { + // The whole reason for a multi-select. A single-select answers "which one + // namespace"; a reader comparing two namespaces has to be able to ask for both. + await chooseFilter(page, "models-filters-filter-ns", "platform"); + + await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); + await expect(page.getByTestId("models-filters-pill-ns-platform")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(3); + await expect(page.getByTestId("models-summary")).toContainText("3 of 4"); + }); + + await test.step("4. a second filter narrows further and keeps its own pill", async () => { + // Two different filters at once is what the bar takes definitions for. A + // component built around namespaces could not do this at all. + await chooseFilter(page, "models-filters-filter-provider", "OpenAI"); + + await expect(page.getByTestId("models-filters-pill-provider-OpenAI")).toContainText( + "Provider: OpenAI", + ); + await expect(dataRows(page)).toHaveCount(1); + await expect(rowNamed(page, "default-model-config")).toHaveCount(1); + }); + + await test.step("5. clicking a pill removes that filter and leaves the rest alone", async () => { + await page.getByTestId("models-filters-pill-provider-OpenAI").click(); + + await expect(page.getByTestId("models-filters-pill-provider-OpenAI")).toHaveCount(0); + // The two namespace pills are untouched: removing one choice is not a reset. + await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); + await expect(page.getByTestId("models-filters-pill-ns-platform")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(3); + }); + + await test.step("6. the last pill of a filter takes the parameter with it", async () => { + /* + * Removed back to back, with no wait between them. + * + * Each removal used to compute the remainder from the render it was drawn in, so + * two clicks landing in the same frame both worked from the same list: the second + * wrote back the namespace the first had just taken out, and a pill survived being + * clicked. `no wait` is the assertion — pausing here would pass either way. + */ + await page.getByTestId("models-filters-pill-ns-platform").click({ noWaitAfter: true }); + await page.getByTestId("models-filters-pill-ns-kagent").click({ noWaitAfter: true }); + + // Not `?ns=`, which would read as "narrowed to nothing" — the address has to + // become the address of the unfiltered list again, or a link to "everything" and a + // link to "nothing" would look the same. + await expect(page).not.toHaveURL(/ns=/); + await expect(page.getByTestId("models-filters-pills")).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(4); + }); +}); + +test("lists: the search term is a filter too, and clearing means everything", async ({ + page, +}) => { + await test.step("1. models has a search box, which it did not before", async () => { + // The page's only way to find a configuration used to be reading the table. + await loadPage(page, routes.models, { title: "Models" }); + await expectSettled(page); + await expect(page.getByTestId("models-filters-search")).toBeVisible(); + }); + + await test.step("2. a term narrows the list and appears as its own pill", async () => { + await page.getByTestId("models-filters-search").fill("haiku"); + + // Matched on the model rather than the name, which is the point of searching + // every column the row displays. + await expect(rowNamed(page, "bedrock-haiku")).toHaveCount(1); + await expect(dataRows(page)).toHaveCount(1); + await expect(page.getByTestId("models-filters-pill-search")).toContainText( + "Search: haiku", + ); + }); + + await test.step("3. clear filters drops the term and every filter at once", async () => { + await chooseFilter(page, "models-filters-filter-ns", "analytics"); + await expect(page.getByTestId("models-filters-pill-ns-analytics")).toBeVisible(); + + await page.getByTestId("models-filters-pill-clear").click(); + + // Everything: the term as well as the namespace. A term left in the box is the + // filter a reader is most likely to have forgotten, so a control that cleared the + // pills and left it would still be hiding rows. + await expect(page.getByTestId("models-filters-pills")).toHaveCount(0); + await expect(page.getByTestId("models-filters-search")).toHaveValue(""); + await expect(dataRows(page)).toHaveCount(4); + await expect(page).toHaveURL(/\/models\?mock=ok$/); + }); + + await test.step("4. a term and a filter chosen in quick succession both survive", async () => { + /* + * A regression this build actually had. The two writes landed before React had + * re-rendered, so the second read the address as it was before the first and put + * the cleared filter straight back — a filter that would not clear, and a search + * reporting no matches for a row plainly on the page. Driven without waiting in + * between, because waiting is what hid it. + */ + await page.getByTestId("models-filters-search").fill("model"); + await chooseFilter(page, "models-filters-filter-ns", "kagent"); + + await expect(page.getByTestId("models-filters-pill-search")).toBeVisible(); + await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(2); + }); +}); + +test("lists: a narrowed view is an address, so it survives a reload", async ({ page }) => { + await test.step("1. narrowing writes what was chosen into the address", async () => { + await loadPage(page, routes.models, { title: "Models" }); + await expectSettled(page); + + await page.getByTestId("models-filters-search").fill("config"); + await chooseFilter(page, "models-filters-filter-ns", "kagent"); + + const url = new URL(page.url()); + expect(url.searchParams.get("q")).toBe("config"); + expect(url.searchParams.getAll("ns")).toEqual(["kagent"]); + }); + + await test.step("2. sorting is in the address too, and the header shows it", async () => { + await page.getByRole("columnheader", { name: "Name", exact: true }).click(); + + await expect(page).toHaveURL(/sort=name/); + // Ascending is the default direction and is deliberately not written, so the + // address carries a direction only where one was chosen. + await expect(page).not.toHaveURL(/dir=/); + }); + + await test.step("3. reloading restores every part of it", async () => { + await page.reload(); + await expectSettled(page); + + // The controls, not just the parameters: a page that kept the URL and rendered + // the unfiltered list would pass a URL-only assertion and be broken. + await expect(page.getByTestId("models-filters-search")).toHaveValue("config"); + await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(2); + await expect( + page.locator("th.ant-table-column-sort").filter({ hasText: "Name" }), + ).toHaveCount(1); + }); + + await test.step("4. the same address typed fresh gives the same view", async () => { + // What a link is. Opened cold rather than reloaded, so nothing in memory can be + // carrying the state. + await page.goto("/models?mock=ok&q=config&ns=kagent&sort=namespace&dir=desc"); + await expectSettled(page); + + await expect(page.getByTestId("models-filters-search")).toHaveValue("config"); + await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(2); + }); +}); + +test("lists: each page says where its narrowing happens, and names the RPC", async ({ + page, +}) => { + /* + * The honesty requirement, asserted rather than trusted. A search box and a sort + * arrow look identical whether the server did the work or the browser did, and the + * difference decides whether "no matches" is true. These three reads return the + * whole list, so the browser can answer completely — and the page says so, naming + * the RPC, so the claim can be checked against the proto rather than believed. + */ + await test.step("1. models names ListModelConfigs", async () => { + await loadPage(page, routes.models, { title: "Models" }); + await expect(page.getByTestId("models-read-note")).toContainText( + "ListModelConfigs", + ); + await expect(page.getByTestId("models-read-note")).toContainText( + "takes no page, sort or search parameter", + ); + }); + + await test.step("2. MCP servers names ListToolServers", async () => { + await loadPage(page, routes.mcpServers, { title: "MCP servers" }); + await expect(page.getByTestId("mcp-servers-read-note")).toContainText( + "ListToolServers", + ); + }); + + await test.step("3. prompts names its one genuinely server-side filter", async () => { + // Not the same claim as the other two. `ListPromptTemplates` takes a namespace and + // rejects a request without one, so the namespace filter here really is sent to + // the server — one read per namespace chosen — while the search and sort are not. + // Saying "everything is client-side" would be as wrong as saying the opposite. + await loadPage(page, routes.prompts, { title: "Prompts" }); + const note = page.getByTestId("prompts-read-note"); + await expect(note).toContainText("ListPromptTemplates"); + await expect(note).toContainText("the request carries a namespace"); + }); + + await test.step("4. the substrate page still makes the opposite claim, correctly", async () => { + // The contrast is the point, and it is worth pinning that this work did not blur + // it: those tables are paged by the server, so they offer no sort at all and say + // what order the server applied. If a later change gave them a client-side sorter + // to match these pages, this step is what would object. + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectSettled(page); + + const headers = page.getByTestId("substrate-actors-table").locator("th"); + await expect(headers.first()).toBeVisible(); + const sortable = await headers.evaluateAll( + (cells) => + cells.filter((cell) => cell.className.includes("column-has-sorters")).length, + ); + expect(sortable, "a server-paged table must not offer a sort it cannot honour").toBe( + 0, + ); + }); +}); + +test("lists: prompts asks the server for exactly the namespaces chosen", async ({ + page, +}) => { + /* + * The one filter on these three pages that is not client-side, asserted as what it + * is rather than as what it looks like. `ListPromptTemplates` takes a namespace, so + * `usePrompts` fans out one call per namespace — choosing two reads those two, and + * nothing else is fetched and thrown away. + */ + await test.step("1. unfiltered, every library is listed", async () => { + await loadPage(page, routes.prompts, { title: "Prompts" }); + await expectSettled(page); + await expect(rowNamed(page, "shared-fragments")).toHaveCount(1, { timeout: 30_000 }); + await expect(rowNamed(page, "incident-playbooks")).toHaveCount(1); + }); + + await test.step("2. choosing a namespace leaves only that namespace's libraries", async () => { + await chooseFilter(page, "prompts-filters-filter-ns", "platform"); + await expectSettled(page); + + await expect(rowNamed(page, "incident-playbooks")).toHaveCount(1); + await expect(rowNamed(page, "shared-fragments")).toHaveCount(0); + }); + + await test.step("3. the count says what was read, not what the cluster holds", async () => { + // With the read scoped, the page has not asked about the other namespaces — so it + // cannot claim a total, and says "read" rather than implying one. + await expect(page.getByTestId("prompts-summary")).toContainText("1 of 1 library read"); + }); + + await test.step("4. an empty namespace says the filter matched nothing, not that none exist", async () => { + // The distinction the scoped read forces. "No prompt libraries yet" would be a + // claim about the cluster that this page, having asked about one namespace, is in + // no position to make. + await page.goto("/prompts?mock=ok&ns=analytics"); + await expectSettled(page); + + await expect( + page.getByText("No prompt libraries match those filters."), + ).toBeVisible(); + await expect(page.getByText("No prompt libraries yet.")).toHaveCount(0); + }); +}); diff --git a/ui/playwright/tests/mcp-servers/mcp-servers-errors.spec.ts b/ui/playwright/tests/mcp-servers/mcp-servers-errors.spec.ts index 0ff64221f..b34024865 100644 --- a/ui/playwright/tests/mcp-servers/mcp-servers-errors.spec.ts +++ b/ui/playwright/tests/mcp-servers/mcp-servers-errors.spec.ts @@ -1,17 +1,56 @@ import { test, expect } from "../../fixtures/test"; -import { loadPage } from "../../helpers/page"; +import { dataRows, loadPage, rowNamed, routes } from "../../helpers/app"; +import { operationCalls, rpc } from "../../helpers/mockCalls"; -// MCP servers — error journey. Client-side url-required validation on create. +/** + * MCP servers — the error journey. + * + * The same contract the agents and models error journeys assert, on purpose: a + * failed list load should look and behave identically wherever it happens, and + * several pages pinning one contract is what catches the first page to drift from + * it. + */ -test("mcp servers: url validation", async ({ page }) => { - // region Creating — client-side validation blocks the POST - await test.step("blocks create when the URL is empty", async () => { - await loadPage(page, "/mcp/new", { heading: "New MCP server" }); +test("mcp servers: a failed load is reported, not disguised as an empty list", async ({ + page, +}) => { + await test.step("1. the failure is on screen and names what went wrong", async () => { + await loadPage(page, routes.mcpServers, { scenario: "error", title: "MCP servers" }); - await page.getByLabel("Server Name").fill("e2e-url-validation"); - await page.getByRole("button", { name: "Create server" }).click(); + const alert = page.getByTestId("mcp-servers-error"); + await expect(alert).toBeVisible(); + await expect(alert).toContainText("Could not load MCP servers"); + // The backend's own account of the failure reaches the user rather than a + // generic message, and it names the call that failed — which the HTTP status + // this used to assert never did. Asserted as that property, not as a literal + // status: there is no status to report now, and putting one back in the + // message to satisfy a string match would be fitting the product to a stale + // test. + await expect(alert).toContainText("asked to fail"); + await expect(alert).toContainText("ToolService/ListToolServers"); + }); + + await test.step("2. it is not mistaken for an empty list", async () => { + // The distinction this whole spec exists for: "there are none" and "we could not + // find out" lead a reader to opposite conclusions, and only one of them is true. + await expect(page.getByText("No MCP servers yet.")).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(0); + }); + + await test.step("3. retrying asks the backend again", async () => { + const before = await operationCalls(page, rpc.listToolServers); + + await page.getByRole("button", { name: "Try again" }).click(); + await expect + .poll(() => operationCalls(page, rpc.listToolServers), { timeout: 10_000 }) + .toBeGreaterThan(before); + }); - await expect(page.getByText("URL is required")).toBeVisible(); - await expect(page).toHaveURL(/\/mcp\/new/); + await test.step("4. the page recovers when the backend does", async () => { + // A failure that cannot clear is indistinguishable from a broken page, so the + // recovery is as much a part of the contract as the message. + await loadPage(page, routes.mcpServers, { title: "MCP servers" }); + await expect(page.getByTestId("mcp-servers-error")).toHaveCount(0); + await expect(rowNamed(page, "kagent-tool-server")).toHaveCount(1); }); }); diff --git a/ui/playwright/tests/mcp-servers/mcp-servers.spec.ts b/ui/playwright/tests/mcp-servers/mcp-servers.spec.ts index 7b1ddd308..9be98937d 100644 --- a/ui/playwright/tests/mcp-servers/mcp-servers.spec.ts +++ b/ui/playwright/tests/mcp-servers/mcp-servers.spec.ts @@ -1,57 +1,126 @@ import { test, expect } from "../../fixtures/test"; -import { loadPage } from "../../helpers/page"; +import { + dataRows, + expectSettled, + loadPage, + rowNamed, + routes, + withScenario, +} from "../../helpers/app"; -// MCP servers & tools — create/read/delete lifecycle journey. The UI has no edit -// surface for a tool server, so there's no Update stage. Creates a uniquely-named -// RemoteMCPServer, finds it via search and expands it, then deletes it — only ever -// touching the server it created. The form's namespace combobox auto-selects -// "kagent", so the server is kagent/. -// -// Only the remote transport is asserted: an MCPServer (stdio) becomes listable -// only once its backing deployment is ready (tens of seconds), too slow for an -// e2e assertion, and the remote path already gives full tool-server coverage. -// Error journeys live in mcp-servers-errors.spec.ts. +/** + * MCP servers — the reading journey. + * + * `DEFERRED.md` listed this as blocked on a page that did not exist. The page does + * exist, and did before this spec was written; the entry was simply stale. Which is + * the argument for porting it now rather than trusting the note. + * + * The thing worth pinning here is the tool count per server, because it is derived + * rather than read: the API returns a server with its discovered tools nested, and + * the list has to count them. A server that discovered none is the case most likely + * to be got wrong — an earlier listing of these dropped such a server from the page + * entirely — so it is asserted explicitly. + * + * The row's own expander is covered too: a server's tools are only reachable by + * opening it, and the row opens anywhere along its length rather than on the chevron + * alone. + */ -const NAMESPACE = "kagent"; -const SERVER_URL = "https://example.com/mcp"; +const SERVERS = ["kagent-tool-server", "grafana-mcp", "warehouse-mcp"]; -test("mcp servers: create, read, delete", async ({ page }, testInfo) => { - // Generated per attempt (Date.now differs on retry) so re-runs never collide. - const ref = `${NAMESPACE}/e2e-remote-${Date.now().toString(36)}-${testInfo.retry}`; - const name = ref.split("/")[1]; +test("mcp servers: the list loads and counts each server's tools", async ({ + page, +}) => { + await test.step("1. a loading state precedes the data", async () => { + await page.goto(withScenario(routes.mcpServers, "slow")); + await expect(page.locator(".ant-spin-spinning")).toBeVisible(); + }); + + await test.step("2. every server is listed", async () => { + for (const name of SERVERS) { + await expect(rowNamed(page, name), `"${name}" is missing`).toHaveCount(1); + } + await expect(dataRows(page)).toHaveCount(SERVERS.length); + await expectSettled(page); + }); + + await test.step("2b. interactive rows opt in to clickable styling", async () => { + // Row hover/pointer styles are now opt-in through this class so static tables + // do not imply click behavior. + for (const name of SERVERS) { + await expect(rowNamed(page, name)).toHaveClass(/clickable-table-row/); + } + }); - // region Creating — fill the form and POST a new RemoteMCPServer - await test.step("creates a remote MCP server", async () => { - await loadPage(page, "/mcp/new", { heading: "New MCP server" }); + await test.step("3. each row shows its namespace, kind and tool count", async () => { + const local = rowNamed(page, "kagent-tool-server"); + await expect(local).toContainText("kagent"); + await expect(local).toContainText("MCPServer"); + await expect(local).toContainText("3"); - await page.getByLabel("Server Name").fill(name); - await page.locator("#url").fill(SERVER_URL); - await page.getByRole("button", { name: "Create server" }).click(); + const remote = rowNamed(page, "grafana-mcp"); + await expect(remote).toContainText("platform"); + await expect(remote).toContainText("RemoteMCPServer"); + await expect(remote).toContainText("2"); + }); - await expect(page).toHaveURL(/\/mcp(\?|$)/); - await expect(page.getByText(ref)).toBeVisible(); + await test.step("4. a server that discovered no tools is still listed, as zero", async () => { + // Not filtered out and not blank. A registered server reporting nothing is more + // likely to be misconfigured than a busy one, so it is the row a reader most + // needs to see. + const empty = rowNamed(page, "warehouse-mcp"); + await expect(empty).toHaveCount(1); + await expect(empty).toContainText("0"); }); - // region Reading — filter the list to the new server and expand its row - await test.step("finds the server via search and expands it", async () => { - await loadPage(page, "/mcp", { heading: "MCP & tools" }); + await test.step("5. the summary counts servers and tools together", async () => { + // 3 + 2 + 0 across three servers — a total the page computes, so worth pinning + // against the rows above rather than restating a constant. + await expect(page.getByTestId("mcp-servers-summary")).toContainText( + "3 servers", + ); + await expect(page.getByTestId("mcp-servers-summary")).toContainText( + "5 tools", + ); + }); - await page.locator("#mcp-search").fill("zzz-no-such-server"); - await expect(page.getByText("No servers or tools match that filter.")).toBeVisible(); + await test.step("6. filtering narrows by server, tool name or description", async () => { + await page.getByTestId("mcp-servers-filters-search").fill("grafana"); + await expect(rowNamed(page, "grafana-mcp")).toHaveCount(1); + await expect(rowNamed(page, "kagent-tool-server")).toHaveCount(0); - await page.getByRole("button", { name: "Clear search" }).click(); - await expect(page.getByText(ref)).toBeVisible(); - await page.getByRole("button", { name: new RegExp(`Expand server ${ref}`) }).click(); + await page.getByTestId("mcp-servers-filters-search").fill(""); + await expect(dataRows(page)).toHaveCount(SERVERS.length); }); - // region Deleting — remove the server and confirm the row is gone - await test.step("deletes the server", async () => { - await page.getByRole("button", { name: `Actions for server ${ref}` }).click(); - await page.getByRole("menuitem", { name: "Remove server" }).click(); - const dialog = page.getByRole("dialog"); - await expect(dialog.getByText("Delete MCP server")).toBeVisible(); - await dialog.getByRole("button", { name: "Confirm" }).click(); + await test.step("7. a row opens anywhere along it, and closes the same way", async () => { + // `dataRows` rather than `rowNamed`: once the row is open, the panel below it holds + // tools whose names also contain "grafana", so a by-text row locator matches two + // rows and the second click lands inside the panel instead of on the row. + const server = dataRows(page).filter({ hasText: "grafana-mcp" }); + // Deliberately not the chevron: the point of the assertion is the rest of the row. + // The namespace cell is as far from the expander as a cell gets. + await server.getByRole("cell").nth(2).click(); + + // The panel lists the tools the server discovered, which no column does. + await expect(page.getByTestId("tool-server-tools")).toBeVisible(); + + // And it closes again, so the row is a toggle rather than a one-way reveal. + // + // Hidden rather than absent, and the distinction is the table library's: once a row + // has been expanded it keeps its panel mounted and collapses it with `display: none` + // (`ExpandedRow.js` — `display: expanded ? null : 'none'`). Asserting on count here + // fails against a panel the reader cannot see. + await server.getByRole("cell").nth(2).click(); + await expect(page.getByTestId("tool-server-tools")).toBeHidden(); + }); - await expect(page.getByText(ref)).toHaveCount(0); + await test.step("8. an empty result says so instead of showing a bare table", async () => { + await loadPage(page, routes.mcpServers, { + scenario: "empty", + title: "MCP servers", + }); + await expect(page.getByText("No MCP servers yet.")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(0); }); }); diff --git a/ui/playwright/tests/mcp-servers/row-interaction.spec.ts b/ui/playwright/tests/mcp-servers/row-interaction.spec.ts new file mode 100644 index 000000000..8572f3285 --- /dev/null +++ b/ui/playwright/tests/mcp-servers/row-interaction.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from "../../fixtures/test"; + +/** + * One row, one affordance. + * + * The whole row expands a server's tools, and the plus/minus is inside that row — so the + * two must not respond differently. They did: the control brought antd's own hover and + * press treatment, which meant the smaller of two overlapping targets for the same action + * was the one that lit up under the mouse. + * + * The press state is asserted with a real mouse-down rather than a class, because `:active` + * cannot be faked and it is the state that was missing altogether — antd ships a row hover + * and nothing for the click, so on a slow route a click looked like it had not registered. + */ + +const MCP = "/mcp"; + +test("mcp servers: the row and its expand control behave as one", async ({ page }) => { + await page.goto(MCP); + await expect(page.getByTestId("mcp-servers-table")).toBeVisible(); + + const row = page.locator("tr.clickable-table-row").first(); + const icon = row.locator(".ant-table-row-expand-icon"); + await expect(icon).toBeVisible(); + + const styles = (locator: typeof icon) => + locator.evaluate((el) => { + const cs = getComputedStyle(el); + return { background: cs.backgroundColor, shadow: cs.boxShadow, colour: cs.color }; + }); + + await test.step("1. hovering the control adds nothing of its own", async () => { + const atRest = await styles(icon); + await icon.hover(); + expect(await styles(icon)).toEqual(atRest); + }); + + await test.step("2. pressing it adds nothing of its own either", async () => { + const atRest = await styles(icon); + const box = await icon.boundingBox(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2); + await page.mouse.down(); + const pressed = await styles(icon); + await page.mouse.up(); + + expect(pressed.background).toBe(atRest.background); + expect(pressed.shadow).toBe(atRest.shadow); + }); + + await test.step("3. the row itself does respond to being pressed", async () => { + const cell = row.locator("td").first(); + const before = await cell.evaluate((el) => getComputedStyle(el).backgroundColor); + + const box = await cell.boundingBox(); + await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2); + await page.mouse.down(); + const during = await cell.evaluate((el) => getComputedStyle(el).backgroundColor); + await page.mouse.up(); + + expect(during, "a pressed row must look pressed").not.toBe(before); + }); + + await test.step("4. and clicking anywhere on it still expands the server", async () => { + await row.locator("td").first().click(); + await expect(page.locator(".ant-table-expanded-row")).toBeVisible(); + }); +}); diff --git a/ui/playwright/tests/models/models-errors.spec.ts b/ui/playwright/tests/models/models-errors.spec.ts index 79d0233a1..aab81ee26 100644 --- a/ui/playwright/tests/models/models-errors.spec.ts +++ b/ui/playwright/tests/models/models-errors.spec.ts @@ -1,21 +1,52 @@ import { test, expect } from "../../fixtures/test"; -import { loadPage } from "../../helpers/page"; +import { dataRows, loadPage, rowNamed, routes } from "../../helpers/app"; +import { operationCalls, rpc } from "../../helpers/mockCalls"; -// Models — error journey. Client-side model-selection validation on create; holds -// without a backend round trip. +/** + * Models — the error journey. + * + * Same shape as the agents error journey, and deliberately so: a failed list + * load should look and behave the same wherever it happens, and two pages + * asserting the same contract is what will catch the first one that drifts. + */ -test("models: create validation", async ({ page }) => { - // region Creating — client-side validation blocks the POST - await test.step("blocks create when no model is selected", async () => { - await loadPage(page, "/models/new", { heading: "New Model" }); +test("models: a failed load is reported, not disguised as an empty list", async ({ + page, +}) => { + await test.step("1. the failure is on screen and names what went wrong", async () => { + await loadPage(page, routes.models, { scenario: "error", title: "Models" }); - await page.getByRole("button", { name: "Create Model" }).click(); + const alert = page.getByTestId("models-error"); + await expect(alert).toBeVisible(); + await expect(alert).toContainText("Could not load model configurations"); + // The backend's own account of the failure reaches the user rather than a + // generic message, and it names the call that failed — which the HTTP status + // this used to assert never did. Asserted as that property, not as a literal + // status: there is no status to report now, and putting one back in the + // message to satisfy a string match would be fitting the product to a stale + // test. + await expect(alert).toContainText("asked to fail"); + await expect(alert).toContainText("ModelService/ListModelConfigs"); + }); + + await test.step("2. it is not mistaken for an empty list", async () => { + await expect(page.getByText("No model configurations yet.")).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(0); + }); + + await test.step("3. retrying asks the backend again", async () => { + const before = await operationCalls(page, rpc.listModelConfigs); + + await page.getByRole("button", { name: "Try again" }).click(); + await expect + .poll(() => operationCalls(page, rpc.listModelConfigs), { timeout: 10_000 }) + .toBeGreaterThan(before); + await expect(page.getByTestId("models-error")).toBeVisible(); + }); - // The error renders up by the model field; scroll it in so it's on screen - // (in the recorded video) rather than above the fold. - const error = page.getByText("Provider and Model selection is required"); - await error.scrollIntoViewIfNeeded(); - await expect(error).toBeVisible(); - await expect(page).toHaveURL(/\/models\/new/); + await test.step("4. the page recovers once the backend does", async () => { + await loadPage(page, routes.models, { scenario: "ok", title: "Models" }); + await expect(page.getByTestId("models-error")).toHaveCount(0); + await expect(rowNamed(page, "default-model-config")).toHaveCount(1); }); }); diff --git a/ui/playwright/tests/models/models.spec.ts b/ui/playwright/tests/models/models.spec.ts index de1c0288f..1ebb17c2c 100644 --- a/ui/playwright/tests/models/models.spec.ts +++ b/ui/playwright/tests/models/models.spec.ts @@ -1,77 +1,88 @@ import { test, expect } from "../../fixtures/test"; -import { loadPage, expectScrolledIntoView } from "../../helpers/page"; +import { + dataRows, + expectSettled, + loadPage, + rowNamed, + routes, + withScenario, +} from "../../helpers/app"; +import { operationCalls, rpc } from "../../helpers/mockCalls"; -// Models / providers — full-CRUD lifecycle journey. Creates a uniquely-named -// throwaway config (OpenAI + a real catalog model + dummy key), reads it back on -// the edit page, updates it, then deletes it — never touching a seeded config. -// Per-item edit/delete controls are scoped by the config ref, so only the config -// this test created is acted on. Error journeys live in models-errors.spec.ts. +/** + * Models — the reading journey, and the way in to the create form. + * + * As with agents, the old spec was a create → read → update → delete lifecycle. The + * create half runs against a real cluster instead (`live/write/models-create.spec.ts`), + * because a form that only ever posts to a fixture proves the fixture. What is covered + * here is the list, and that the page offers a way to reach the form — the header's + * create menu belongs to the default shell, so a distribution supplying its own layout + * does not inherit it, and this button is then the only way in. + */ -const NAMESPACE = "kagent"; -// A model that exists in the real OpenAI catalog served by /api/models. -const MODEL_NAME = "gpt-5.4-mini"; +const CONFIGS = [ + "default-model-config", + "anthropic-model-config", + "ollama-local", + "bedrock-haiku", +]; -test("models: create, read, update, delete", async ({ page }, testInfo) => { - const name = `e2e-model-${Date.now().toString(36)}-${testInfo.retry}`; - const ref = `${NAMESPACE}/${name}`; - - // region Creating — fill the form and POST a new model config - await test.step("creates a model config", async () => { - await loadPage(page, "/models/new", { heading: "New Model" }); +test("models: the list loads and renders each configuration", async ({ page }) => { + await test.step("1. a loading state precedes the data", async () => { + await page.goto(withScenario(routes.models, "slow")); + await expect(page.locator(".ant-spin-spinning")).toBeVisible(); + }); - // Provider + model are searchable cmdk comboboxes; type to filter, then click. - // Option accessible names include icon alt text (e.g. "OpenAI icon OpenAI"), so - // anchor the provider match to avoid "AzureOpenAI" and take the first model hit. - await page.getByTestId("model-provider-select").click(); - await page.getByPlaceholder("Search providers...").fill("OpenAI"); - await page.getByRole("option", { name: /^OpenAI\b/ }).first().click(); + await test.step("2. every configuration is listed", async () => { + for (const name of CONFIGS) { + await expect(rowNamed(page, name), `"${name}" is missing`).toHaveCount(1); + } + await expect(dataRows(page)).toHaveCount(CONFIGS.length); + await expectSettled(page); + }); - await page.getByTestId("model-select").click(); - await page.getByPlaceholder("Search models...").fill(MODEL_NAME); - await page.getByRole("option").first().click(); + await test.step("3. each row splits the ref and shows its provider", async () => { + // The API returns one `namespace/name` string; the list has to take it apart + // to fill two columns, which is the part worth pinning. + const openai = rowNamed(page, "default-model-config"); + await expect(openai).toContainText("kagent"); + await expect(openai).toContainText("OpenAI"); + await expect(openai).toContainText("gpt-4.1"); - // Override the auto-generated name so it's unique and scoped to this run. - await page.locator('[data-test="edit-model-name-button"]').click(); - await page.getByPlaceholder("Enter model name...").fill(name); + const ollama = rowNamed(page, "ollama-local"); + await expect(ollama).toContainText("platform"); + await expect(ollama).toContainText("Ollama"); + // No API key secret on a local provider — the column shows a dash, not blank. + await expect(ollama).toContainText("—"); + }); - await page.getByTestId("model-api-key-input").fill("sk-e2e-test-key"); - await page.getByRole("button", { name: "Create Model" }).click(); + await test.step("4. refreshing re-reads the list without disturbing it", async () => { + // Operations, not requests: under the substituted transport a working refresh + // makes no HTTP request for `page.on("request")` to see. + const before = await operationCalls(page, rpc.listModelConfigs); - // Verify the create on the actual models list: the new config's row is present - // (scrolled into view). - await expect(page).toHaveURL(/\/models(\?|$)/); - await expectScrolledIntoView(page.getByRole("button", { name: `Edit model ${ref}` })); - }); + await page.getByRole("button", { name: "Refresh" }).click(); + await expect + .poll(() => operationCalls(page, rpc.listModelConfigs), { timeout: 10_000 }) + .toBeGreaterThan(before); - // region Reading — open the edit page and load the stored config - await test.step("reads the config back on its edit page", async () => { - await page.getByRole("button", { name: `Edit model ${ref}` }).click(); - await expect(page.getByRole("heading", { level: 1, name: "Edit Model" })).toBeVisible(); + await expectSettled(page); + await expect(dataRows(page)).toHaveCount(CONFIGS.length); }); - // region Updating — rotate the API key and save (PUT) - await test.step("updates the config's API key", async () => { - // In edit mode only the API key is editable (provider/model/name are locked). - // The key is write-only, so it can't be read back; a successful PUT is confirmed - // by the redirect to the list with the config still present. - await page.getByTestId("model-api-key-input").fill("sk-e2e-rotated-key"); - await page.getByRole("button", { name: "Save Changes" }).click(); - // The rotated API key is write-only and never rendered on the list, so a model - // update produces no list-visible change. The list-level check is that the - // config's row survives the save (scrolled into view); a failed PUT keeps you on - // the edit page. - await expect(page).toHaveURL(/\/models(\?|$)/); - await expectScrolledIntoView(page.getByRole("button", { name: `Edit model ${ref}` })); + await test.step("5. an empty result says so instead of showing a bare table", async () => { + await loadPage(page, routes.models, { scenario: "empty", title: "Models" }); + await expect(page.getByText("No model configurations yet.")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(0); }); - // region Deleting — remove the config and confirm the row is gone - await test.step("deletes the config", async () => { - await page.getByRole("button", { name: `Delete model ${ref}` }).click(); - const dialog = page.getByRole("dialog"); - await expect(dialog.getByText("Delete Model")).toBeVisible(); - await dialog.getByRole("button", { name: "Delete" }).click(); - - // The config's row disappearing from the list is the durable delete signal. - await expect(page.getByRole("button", { name: `Delete model ${ref}` })).toHaveCount(0); + await test.step("6. the list offers a way to create one", async () => { + // From the empty list, which is where somebody most needs it. + await page.getByTestId("models-new").click(); + await page.waitForURL(/\/models\/new(\?|$)/); + // A field of the real form, not just the route: `ModelForm` is the same component the + // edit page uses, so reaching its provider picker is reaching the thing that can + // actually create a model. + await expect(page.getByTestId("model-provider")).toBeVisible(); }); }); diff --git a/ui/playwright/tests/onboarding/onboarding.spec.ts b/ui/playwright/tests/onboarding/onboarding.spec.ts deleted file mode 100644 index 3f49e0d70..000000000 --- a/ui/playwright/tests/onboarding/onboarding.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { test, expect } from "../../fixtures/test"; - -// Onboarding wizard — completion journey + skip, against the real backend. The -// wizard shows when localStorage['kagent-onboarding'] !== "true"; the shared -// fixture sets it to "true", so each test overrides it to "false" (init scripts -// run in registration order, so this wins). With the seeded model config present, -// step 1 defaults to "select existing", avoiding the create-a-provider path. -// -// Completing the wizard creates a real agent, so we give it a unique name and -// delete it afterward. Creating a model in the wizard is covered by the models -// suite, so this walks the "select existing model" path. - -const NAMESPACE = "kagent"; - -test("onboarding: complete the wizard", async ({ page }, testInfo) => { - const agentName = `e2e-onboard-${Date.now().toString(36)}-${testInfo.retry}`; - const ref = `${NAMESPACE}/${agentName}`; - - await page.addInitScript(() => window.localStorage.setItem("kagent-onboarding", "false")); - await page.goto("/"); - - // region Creating — walk the wizard end to end to create an agent - await test.step("welcome → get started", async () => { - await expect(page.getByText("Bringing Agentic AI to Cloud Native")).toBeVisible(); - await page.getByRole("button", { name: /Let's Get Started/ }).click(); - }); - - await test.step("step 1 — select the seeded existing model config", async () => { - await expect(page.getByText("Step 1: Configure AI Model")).toBeVisible(); - await page.getByRole("combobox").first().click(); - await page.getByRole("option").first().click(); - await page.getByRole("button", { name: "Next: Agent Setup" }).click(); - }); - - await test.step("step 2 — agent setup with a unique name", async () => { - await expect(page.getByText("Step 2: Set Up The AI Agent")).toBeVisible(); - await page.getByLabel("Agent Name", { exact: true }).fill(agentName); - await page.getByRole("button", { name: "Next: Select Tools" }).click(); - }); - - await test.step("step 3 — tools are optional", async () => { - await expect(page.getByText("Step 3: Select Tools")).toBeVisible(); - await page.getByRole("button", { name: "Next: Review" }).click(); - }); - - await test.step("step 4 — review + finalize (creates the agent)", async () => { - await expect(page.getByText("Step 4: Review Agent Configuration")).toBeVisible(); - await page.getByRole("button", { name: /Finish/ }).click(); - }); - - await test.step("step 5 — lands on the agents list with the new agent", async () => { - await expect(page.getByText("Setup Complete!")).toBeVisible(); - await page.getByRole("button", { name: /Go to Agent/ }).click(); - - await expect(page.getByText("Setup Complete!")).toHaveCount(0); - await expect(page.getByRole("heading", { level: 1, name: "Agents" })).toBeVisible(); - // The agent the wizard created is present in the list it lands on. - await expect(page.getByText(agentName).first()).toBeVisible(); - expect(await page.evaluate(() => window.localStorage.getItem("kagent-onboarding"))).toBe("true"); - }); - - // region Deleting — remove the agent the wizard created - await test.step("cleans up the agent the wizard created", async () => { - await page.getByTestId(`agent-options-${ref}`).first().click(); - await page.getByRole("menuitem", { name: "Delete" }).click(); - const dialog = page.getByRole("alertdialog"); - await expect(dialog).toBeVisible(); - await dialog.getByRole("button", { name: "Delete" }).click(); - await expect(page.getByText(agentName)).toHaveCount(0); - }); -}); - -test("onboarding: skip the wizard", async ({ page }) => { - // region Skipping — dismiss the wizard without creating anything - await page.addInitScript(() => window.localStorage.setItem("kagent-onboarding", "false")); - await page.goto("/"); - - await expect(page.getByText("Bringing Agentic AI to Cloud Native")).toBeVisible(); - await page.getByRole("button", { name: /Skip wizard/ }).click(); - - await expect(page.getByText("Bringing Agentic AI to Cloud Native")).toHaveCount(0); - expect(await page.evaluate(() => window.localStorage.getItem("kagent-onboarding"))).toBe("true"); -}); diff --git a/ui/playwright/tests/prompt-libraries/prompt-libraries-errors.spec.ts b/ui/playwright/tests/prompt-libraries/prompt-libraries-errors.spec.ts deleted file mode 100644 index 232dfa8a4..000000000 --- a/ui/playwright/tests/prompt-libraries/prompt-libraries-errors.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test } from "../../fixtures/test"; -import { expectToast, waitForAppReady } from "../../helpers/page"; - -const NAMESPACE = "kagent"; - -// Prompt libraries — error journey. Client-side name-required validation on create. - -test("prompt libraries: name validation", async ({ page }) => { - // region Creating — client-side validation blocks the POST - await page.goto(`/prompts/new?ns=${NAMESPACE}`); - await waitForAppReady(page); - - await page.getByRole("button", { name: "Create Library" }).click(); - - await expectToast(page, /Library name is required/i, { type: "error" }); -}); diff --git a/ui/playwright/tests/prompt-libraries/prompt-libraries.spec.ts b/ui/playwright/tests/prompt-libraries/prompt-libraries.spec.ts deleted file mode 100644 index dfd81a2b6..000000000 --- a/ui/playwright/tests/prompt-libraries/prompt-libraries.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { test, expect } from "../../fixtures/test"; -import { loadPage, waitForAppReady, expectScrolledIntoView } from "../../helpers/page"; - -// Prompt libraries — full-CRUD lifecycle journey. /prompts lists via GET -// /api/prompttemplates?namespace=; create is a dedicated route that POSTs then -// redirects to the detail page; edit PUTs, delete DELETEs. Each mutation is -// verified back on the list page: create adds the row, an edit that adds a fragment -// bumps the row's key count, and delete removes the row. Only the library this test -// creates is touched. Error journeys live in prompt-libraries-errors.spec.ts. - -const NAMESPACE = "kagent"; - -// The list renders each library as a link whose text includes the name and " keys". -function libraryRow(page: import("@playwright/test").Page, name: string) { - return page.getByRole("link", { name: new RegExp(name) }); -} - -test("prompt libraries: create, read, update, delete", async ({ page }, testInfo) => { - const name = `e2e-prompts-${Date.now().toString(36)}-${testInfo.retry}`; - - // region Creating — POST a new library, then confirm the row on the prompts list - await test.step("creates a library and sees it on the list", async () => { - await page.goto(`/prompts/new?ns=${NAMESPACE}`); - await waitForAppReady(page); - - await page.getByLabel("Name", { exact: true }).fill(name); - await page.getByLabel("Key 1").fill("safety-rules"); - // Target the textbox role, not getByLabel("Content"): the "Open … in editor" - // button's aria-label also contains "Content", so a label match is ambiguous. - await page.getByRole("textbox", { name: "Content" }).fill("Always be safe."); - await page.getByRole("button", { name: "Create Library" }).click(); - - // Success is confirmed by durable state, not the auto-dismissing toast: the - // create redirects to the library's detail page, then the row appears on the list. - await expect(page).toHaveURL(new RegExp(`/prompts/${NAMESPACE}/${name}`)); - - await loadPage(page, "/prompts", { heading: "Prompt Libraries" }); - const createdRow = libraryRow(page, name); - await expectScrolledIntoView(createdRow); - await expect(createdRow).toContainText("1 keys"); - }); - - // region Reading — open the library's detail page from the list - await test.step("opens the library detail page", async () => { - await libraryRow(page, name).click(); - await expect(page.getByRole("heading", { level: 1, name })).toBeVisible(); - await expect(page.getByRole("button", { name: "Save changes" })).toBeVisible(); - }); - - // region Updating — add a fragment, save (PUT), then confirm the key count on the list - await test.step("adds a fragment and sees the updated count on the list", async () => { - await page.getByRole("button", { name: "Add fragment" }).click(); - await page.getByLabel("Key 2").fill("tone"); - await page.getByRole("textbox", { name: "Content" }).nth(1).fill("Be kind."); - await page.getByRole("button", { name: "Save changes" }).click(); - await expect(page.locator('[data-sonner-toast][data-type="success"]')).toContainText("Saved"); - - // The saved fragment shows up as an updated key count on the list — a durable - // signal, unlike the auto-dismissing "saved" toast. - await loadPage(page, "/prompts", { heading: "Prompt Libraries" }); - const updatedRow = libraryRow(page, name); - await expectScrolledIntoView(updatedRow); - await expect(updatedRow).toContainText("2 keys"); - }); - - // region Deleting — delete from the detail page, then confirm the row is gone - await test.step("deletes the library and sees it removed from the list", async () => { - await libraryRow(page, name).click(); - await page.getByRole("button", { name: "Delete", exact: true }).click(); - const dialog = page.getByRole("dialog"); - await expect(dialog.getByText("Delete this prompt library?")).toBeVisible(); - await dialog.getByRole("button", { name: "Delete library" }).click(); - - // Deletion is confirmed by the redirect to the list and the row disappearing, - // rather than the transient "deleted" toast. - await expect(page).toHaveURL(new RegExp(`/prompts\\?namespace=${NAMESPACE}`)); - await expect(libraryRow(page, name)).toHaveCount(0); - }); -}); diff --git a/ui/playwright/tests/prompts/prompt-libraries-errors.spec.ts b/ui/playwright/tests/prompts/prompt-libraries-errors.spec.ts new file mode 100644 index 000000000..7e72442c5 --- /dev/null +++ b/ui/playwright/tests/prompts/prompt-libraries-errors.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from "../../fixtures/test"; +import { dataRows, loadPage, rowNamed, routes } from "../../helpers/app"; +import { operationCallCounts, rpc } from "../../helpers/mockCalls"; + +// No console allowance any more. This spec asks for a library that does not exist, and +// that used to be an HTTP 404 the browser logged; the API is gRPC over a substituted +// transport now, so nothing is fetched and nothing is logged. The allowance is left off +// deliberately rather than kept "just in case": one that forgives noise that can no +// longer occur reads as evidence that it still does. + +/** + * Prompt libraries — the error journeys, list and detail. + * + * Two failures rather than one, because they are different: a list that cannot load + * and a *single* library that cannot be found lead a reader to different actions, and + * a page that answered both the same way would be wrong about one of them. + */ + +test("prompts: a failed load is reported, not disguised as an empty list", async ({ + page, +}) => { + await test.step("1. the failure is on screen and names what went wrong", async () => { + await loadPage(page, routes.prompts, { scenario: "error", title: "Prompts" }); + + const alert = page.getByTestId("prompts-error"); + await expect(alert).toBeVisible(); + await expect(alert).toContainText("Could not load prompt libraries"); + // The backend's own account of the failure reaches the user rather than a + // generic message, and it names the call that failed — which the HTTP status + // this used to assert never did. Asserted as that property, not as a literal + // status: there is no status to report now, and putting one back in the + // message to satisfy a string match would be fitting the product to a stale + // test. + await expect(alert).toContainText("asked to fail"); + }); + + await test.step("2. it is not mistaken for an empty list", async () => { + await expect(page.getByText("No prompt libraries yet.")).toHaveCount(0); + await expect(dataRows(page)).toHaveCount(0); + }); + + await test.step("3. retrying asks the backend again", async () => { + // Either read counts as the retry. Listing across namespaces starts with the + // namespaces call and fans out from there, so under a failing backend the retry + // does not reach `ListPromptTemplates` at all — it fails at the first hop. + // Watching only that one would report "no retry happened" when a retry + // demonstrably did. + const WATCHED = [rpc.listPromptTemplates, rpc.listNamespaces] as const; + const total = async () => { + const counts = await operationCallCounts(page, WATCHED); + return WATCHED.reduce((sum, read) => sum + counts[read], 0); + }; + + const before = await total(); + await page.getByRole("button", { name: "Try again" }).click(); + await expect.poll(total, { timeout: 10_000 }).toBeGreaterThan(before); + }); + + await test.step("4. the page recovers when the backend does", async () => { + await loadPage(page, routes.prompts, { title: "Prompts" }); + await expect(page.getByTestId("prompts-error")).toHaveCount(0); + await expect(rowNamed(page, "shared-fragments")).toHaveCount(1); + }); +}); + +test("prompts: a library that fails to load says so on its own page", async ({ page }) => { + await test.step("1. the detail page reports its own failure", async () => { + await page.goto("/prompts/kagent/shared-fragments?mock=error"); + + const alert = page.getByTestId("prompt-detail-error"); + await expect(alert).toBeVisible(); + await expect(alert).toContainText("Could not load this prompt library"); + }); + + await test.step("2. no fragments are shown alongside the failure", async () => { + // Showing a partial fragment list under an error would invite copying an include + // for something that may not exist. + await expect(page.getByTestId("prompt-fragments")).toHaveCount(0); + }); + + await test.step("3. a library that does not exist is told apart from one that failed", async () => { + // `?mock=ok` explicitly: the scenario persists across navigation, so without it + // this step inherits the failure from step 1 and gets a 500 where it needs a 404 + // — which would make a passing page look broken. + await page.goto("/prompts/kagent/no-such-library?mock=ok"); + // Different state, different message: nothing is wrong with the backend here, so + // reporting a failure would send a reader looking for a fault that is not there. + await expect(page.getByTestId("prompt-detail-not-found")).toBeVisible(); + await expect(page.getByTestId("prompt-detail-error")).toHaveCount(0); + }); +}); diff --git a/ui/playwright/tests/prompts/prompt-libraries.spec.ts b/ui/playwright/tests/prompts/prompt-libraries.spec.ts new file mode 100644 index 000000000..a503a3c8a --- /dev/null +++ b/ui/playwright/tests/prompts/prompt-libraries.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from "../../fixtures/test"; +import { + dataRows, + expectSettled, + loadPage, + rowNamed, + routes, + withScenario, +} from "../../helpers/app"; + +/** + * Prompt libraries — the reading journey, list through to detail. + * + * Listed in `DEFERRED.md` as blocked on pages that did not exist. They do, and did + * before this spec; the entry was stale. + * + * Both pages are covered in one journey because the interesting part is the step + * between them: a library's fragments are not on the list at all, so the detail page + * is the only place the include syntax a reader has to copy is ever shown. + */ + +const LIBRARIES = ["shared-fragments", "incident-playbooks"]; + +test("prompt libraries: the list loads and each library opens its fragments", async ({ + page, +}) => { + await test.step("1. a loading state precedes the data", async () => { + await page.goto(withScenario(routes.prompts, "slow")); + await expect(page.locator(".ant-spin-spinning")).toBeVisible(); + }); + + await test.step("2. every library is listed with its key count", async () => { + // A longer wait than the default, and for a real reason rather than flake: + // listing across namespaces is a fan-out, because the API requires a namespace + // and offers no wildcard (see `usePrompts`). Under the slow scenario that is one + // delayed namespaces call followed by one per namespace — about twice the default + // expect timeout on this fixture set, where a single call used to be well inside it. + for (const name of LIBRARIES) { + await expect(rowNamed(page, name), `"${name}" is missing`).toHaveCount(1, { + timeout: 30_000, + }); + } + await expect(dataRows(page)).toHaveCount(LIBRARIES.length); + + const shared = rowNamed(page, "shared-fragments"); + await expect(shared).toContainText("kagent"); + await expect(shared).toContainText("3 keys"); + await expectSettled(page); + }); + + await test.step("3. opening a library shows each fragment and how to include it", async () => { + await rowNamed(page, "shared-fragments").getByRole("link").first().click(); + await expect(page).toHaveURL(/\/prompts\/kagent\/shared-fragments$/); + + const fragments = page.getByTestId("prompt-fragments"); + await expect(fragments).toBeVisible(); + // The include expression is the thing a reader came for — it is what they paste + // into a system message, and it appears nowhere else in the app. + await expect(fragments).toContainText('{{include "shared-fragments/tone"}}'); + await expect(fragments).toContainText("tone"); + await expect(fragments).toContainText("safety"); + }); + + await test.step("4. the detail page names the library it is showing", async () => { + await expect(page.getByTestId("prompt-detail-meta")).toContainText("kagent"); + }); + + await test.step("5. an empty result says so instead of showing a bare table", async () => { + await loadPage(page, routes.prompts, { scenario: "empty", title: "Prompts" }); + await expect(page.getByText("No prompt libraries yet.")).toBeVisible(); + await expect(dataRows(page)).toHaveCount(0); + }); +}); diff --git a/ui/playwright/tests/refresh-toast.spec.ts b/ui/playwright/tests/refresh-toast.spec.ts new file mode 100644 index 000000000..645646381 --- /dev/null +++ b/ui/playwright/tests/refresh-toast.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from "../fixtures/test"; +import { loadPage, routes, withScenario } from "../helpers/app"; + +/** + * What Refresh says when it works, and when it does not. + * + * The second half is the reason this spec exists. A refresh usually returns the same + * data, so a successful one is indistinguishable from a button that did nothing — + * hence the confirmation. But the first version of that confirmation reported success + * unconditionally, because SWR captures a failed revalidation into its error state + * and resolves anyway. The toast then sat above a page showing a load error, saying + * the opposite of it, and only a real click revealed that. + */ + + +/** + * Clicks Refresh once it is actually clickable. + * + * The control carries the list's own loading state, and antd ignores a click on a + * button that is loading — so clicking too early refreshes nothing and the missing + * toast looks like a missing feature. Waiting for spinners to clear is not enough: + * straight after a navigation there are no spinners yet, so that check passes before + * the page has even mounted. + */ +async function clickRefresh(page: import("@playwright/test").Page): Promise { + const button = page.getByTestId("refresh-button"); + await expect(button).toBeEnabled(); + await expect(button).not.toHaveClass(/ant-btn-loading/); + await button.click(); +} + +test("refresh: the confirmation says what actually happened", async ({ page }) => { + /* "Agents", not "Agent page": the control lives in this tab's own filter row and + re-reads this tab, so it names what it refreshed. It briefly refreshed all three + from the page header, and moving it beside the filters is what made naming one + correct again. */ + await test.step("1. a refresh that worked says so", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + await clickRefresh(page); + + await expect(page.getByText("Agents refreshed")).toBeVisible(); + }); + + await test.step("2. a refresh that failed says that instead", async () => { + // The scenario has to be asked for explicitly: it persists across navigation, so + // a page loaded without it would inherit whatever the last one used. + // + // Narrowed to one namespace, which is what makes the *agent* read happen at all: + // with no namespace chosen the page fans out over the namespace list, and in this + // scenario that list is the read that fails first — so the page would be reporting + // a namespace failure rather than the refresh failure under test. + // + // `ns` is the filter's own parameter, the one `useListView` reads. An older + // `?scope=&namespace=` pair selects nothing now, which is a silent no-op: the page + // still loads, still shows an error, and the step passes its first assertion while + // testing something other than what it says. + await page.goto(withScenario(`${routes.agents}?ns=kagent`, "error")); + await expect(page.getByTestId("agents-error")).toBeVisible(); + + await clickRefresh(page); + + // Names the resource and carries the reason, and — the point — does not claim a + // refresh that did not happen. + await expect(page.getByText(/Could not refresh Agents/)).toBeVisible(); + await expect(page.getByText("Agents refreshed")).toHaveCount(0); + }); +}); + +test("refresh: every list confirms, not just the first one wired up", async ({ + page, +}) => { + // One assertion per list, because the toast is the kind of thing that gets added + // to the page being worked on and forgotten on the four beside it. + for (const [path, message] of [ + [routes.models, "Models refreshed"], + [routes.mcpServers, "Tool servers refreshed"], + [routes.prompts, "Prompt libraries refreshed"], + [routes.dashboard, "Dashboard refreshed"], + ] as const) { + await page.goto(withScenario(path, "ok")); + await clickRefresh(page); + await expect(page.getByText(message)).toBeVisible(); + } +}); diff --git a/ui/playwright/tests/routing.spec.ts b/ui/playwright/tests/routing.spec.ts new file mode 100644 index 000000000..6714b2adc --- /dev/null +++ b/ui/playwright/tests/routing.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from "../fixtures/test"; +import { agentChat, instances, loadPage, expectPageTitle, routes } from "../helpers/app"; +import { clickNav, expectNoShell, expectShell } from "../helpers/nav"; + +/** + * Routing — new coverage for the thing this rewrite changed most. + * + * The old app routed on the server through Next's file-system router; this one + * is a single-page app with a client-side router, which puts four behaviours at + * risk that used to come for free: in-app navigation, deep linking straight to a + * route, an unknown path resolving to a 404 rather than a blank screen, and a + * standalone route rendering outside the shell. + */ + +test("routing: in-app navigation, deep links, 404, and standalone routes", async ({ + page, +}) => { + await test.step("1. a sidebar click changes both the URL and the content", async () => { + await loadPage(page, routes.dashboard, { title: "Dashboard" }); + + await clickNav(page, "agents", /\/agents(\?|$)/); + await expectPageTitle(page, "Agents"); + + await clickNav(page, "prompts", /\/prompts(\?|$)/); + await expectPageTitle(page, "Prompts"); + }); + + await test.step("2. the sidebar marks the active destination", async () => { + await expect(page.getByTestId("nav-prompts")).toHaveClass(/ant-menu-item-selected/); + await expect(page.getByTestId("nav-agents")).not.toHaveClass( + /ant-menu-item-selected/, + ); + }); + + await test.step("3. browser history moves between routes", async () => { + await page.goBack(); + await page.waitForURL(/\/agents(\?|$)/); + await expectPageTitle(page, "Agents"); + + await page.goForward(); + await page.waitForURL(/\/prompts(\?|$)/); + await expectPageTitle(page, "Prompts"); + }); + + await test.step("4. a deep link renders that route on a cold load", async () => { + // A full page load, not a client-side transition: this is the link someone + // pastes into chat, and the one a server that does not fall back to + // index.html would break. + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectShell(page); + await expect(page).toHaveURL(/\/substrate/); + }); + + await test.step("5. a deep link with route params renders too", async () => { + // Two params: the namespace and the AgentInstance id, which is how every agent + // surface is addressed now. + await loadPage(page, agentChat(instances.ready)); + // The agent surfaces carry no page heading — the rail names the agent instead — + // so what proves the params reached the route is the rail being scoped to them. + // The card shows the template, which is what a reader recognises the agent by, + // over the short id that distinguishes this conversation from its siblings. + await expect(page.getByTestId("agent-rail-identity")).toContainText( + instances.ready.slice(0, 8), + ); + await expect(page.getByTestId("chat-panel")).toBeVisible(); + }); + + await test.step("6. an unknown path renders 404 inside the shell", async () => { + await loadPage(page, "/no-such-page"); + // The address it tried, which is what a reader compares against the link they + // followed — "that page does not exist" told them nothing they could act on. + await expect(page.getByTestId("not-found-path")).toHaveText("/no-such-page"); + // And somewhere to go that is not just "back to the dashboard", which is the right + // destination only if that is where they were headed. + await expect(page.getByTestId("not-found-link-agents")).toBeVisible(); + // Still inside the app: a wrong URL should not strand the user with no + // way back. + await expectShell(page); + + await page.getByTestId("not-found-dashboard").click(); + await page.waitForURL(/\/$/); + await expectPageTitle(page, "Dashboard"); + }); + + await test.step("7. login renders standalone, outside the shell", async () => { + await loadPage(page, routes.login); + await expect(page.getByTestId("login-page")).toBeVisible(); + await expectNoShell(page); + + await page.getByTestId("login-submit").click(); + await page.waitForURL(/\/$/); + await expectShell(page); + await expectPageTitle(page, "Dashboard"); + }); +}); diff --git a/ui/playwright/tests/shell-chrome.spec.ts b/ui/playwright/tests/shell-chrome.spec.ts new file mode 100644 index 000000000..e8ead192f --- /dev/null +++ b/ui/playwright/tests/shell-chrome.spec.ts @@ -0,0 +1,123 @@ +import { test, expect } from "../fixtures/test"; +import { loadPage, routes } from "../helpers/app"; + +/** + * The three controls at the foot of the sidebar, and the state two of them keep. + * + * Worth its own spec because each is a place a reader can get stranded. A theme + * that forgets itself on reload is worse than no toggle at all; a sidebar that + * collapses and cannot be reopened loses the whole navigation; and a docs link is + * the one control here whose destination is not this app, so nothing else would + * notice if it pointed at the wrong place. + */ + +test("app shell: the sidebar's footer controls", async ({ page }) => { + await test.step("1. documentation points at the project's docs", async () => { + await loadPage(page, routes.agents, { title: "Agents" }); + + const docs = page.getByTestId("sidebar-docs"); + await expect(docs).toHaveAttribute("href", "https://kagent.dev/docs/kagent"); + // Opens away from the console, and without handing the target a referrer that + // names the page — this URL can carry a cluster name. + await expect(docs).toHaveAttribute("target", "_blank"); + await expect(docs).toHaveAttribute("rel", /noopener/); + }); + + await test.step("2. the theme toggle switches, and says what it will do", async () => { + const toggle = page.getByTestId("theme-toggle"); + const before = await page.evaluate(() => document.documentElement.dataset.theme); + + // The label names the destination, not the current state: a toggle announced as + // where it already is tells a screen reader the opposite of what it does. + await expect(toggle).toHaveAttribute( + "aria-label", + before === "dark" ? /light/i : /dark/i, + ); + + await toggle.click(); + + const after = before === "dark" ? "light" : "dark"; + await expect + .poll(() => page.evaluate(() => document.documentElement.dataset.theme)) + .toBe(after); + // `color-scheme` as well, which is what the browser draws its own scrollbars + // and form controls from — those are not ours to style. + await expect + .poll(() => page.evaluate(() => document.documentElement.style.colorScheme)) + .toBe(after); + }); + + await test.step("3. the choice survives a reload", async () => { + const chosen = await page.evaluate(() => document.documentElement.dataset.theme); + + await page.reload(); + await expect(page.getByTestId("app-sidebar")).toBeVisible(); + + // Remembered, rather than falling back to the system preference — which is the + // point of writing only an explicit choice down. + await expect + .poll(() => page.evaluate(() => document.documentElement.dataset.theme)) + .toBe(chosen); + }); + + await test.step("4. the sidebar collapses to icons and comes back", async () => { + const sidebar = page.getByTestId("app-sidebar"); + const expandedWidth = (await sidebar.boundingBox())!.width; + + await page.getByTestId("sidebar-collapse").click(); + + await expect.poll(async () => (await sidebar.boundingBox())!.width).toBeLessThan( + expandedWidth, + ); + // Still navigable: the entries are there as icons, so collapsing hides labels + // rather than the navigation. + await expect(page.getByTestId("nav-agents")).toBeVisible(); + await expect(page.getByTestId("sidebar-collapse")).toHaveAttribute( + "aria-expanded", + "false", + ); + + await page.getByTestId("sidebar-collapse").click(); + await expect.poll(async () => (await sidebar.boundingBox())!.width).toBe( + expandedWidth, + ); + }); +}); + +test("app shell: collapsed, every nav icon sits on the rail's centre line", async ({ + page, +}) => { + await page.goto("/agents"); + await expect(page.getByTestId("nav-agents")).toBeVisible(); + await page.getByText("Collapse", { exact: true }).click(); + + // Measured rather than eyeballed: the rows carry a left-measured padding for the label + // they no longer show, which displaced the library's own collapsed centring and left + // every icon a few pixels to the left — visible as sloppiness, invisible to a + // screenshot test that only asks whether the rail rendered. + const offsets = await page.evaluate(() => { + const rail = document.querySelector('[data-testid="app-sidebar"]')!; + const box = rail.getBoundingClientRect(); + // Excluding the 1px right border, which is not part of the space icons sit in. + const centre = box.left + (box.width - 1) / 2; + return [...document.querySelectorAll('[data-testid^="nav-"]')].map((row) => { + const icon = row.querySelector("svg")!.getBoundingClientRect(); + return { key: (row as HTMLElement).dataset.testid, off: icon.left + icon.width / 2 - centre }; + }); + }); + + expect(offsets.length).toBeGreaterThan(3); + for (const { key, off } of offsets) { + /* + * Two pixels, which is a tolerance rather than a target. + * + * An icon of even width in a rail of odd width cannot land dead centre, and the + * leftover is rounded differently by each engine on each platform — Firefox on + * Linux lands 1.4px out where Chromium on macOS lands under 1. Neither is visible. + * The displacement this test exists to catch was the label's padding pushing every + * icon several pixels left, which 2px still fails on; tightening it further only + * makes the suite report the renderer rather than the layout. + */ + expect(Math.abs(off), `${key} is ${off.toFixed(1)}px off the centre line`).toBeLessThanOrEqual(2); + } +}); diff --git a/ui/playwright/tests/substrate/substrate-polling.spec.ts b/ui/playwright/tests/substrate/substrate-polling.spec.ts new file mode 100644 index 000000000..040770c2b --- /dev/null +++ b/ui/playwright/tests/substrate/substrate-polling.spec.ts @@ -0,0 +1,190 @@ +import { test, expect } from "../../fixtures/test"; +import { operationCallCounts, rpc } from "../../helpers/mockCalls"; + +/** + * Watching the substrate move. + * + * "Worker pod" changes on its own — the substrate suspends an actor and restarts it + * elsewhere — so a page read once shows a placement that has already moved. Polling is + * offered beside Refresh and is off until asked for: twice a second is a rate to watch + * something at, not a rate to leave a page at. + * + * ## What is counted, and why it is not requests + * + * Reads are counted as *operations*, not as HTTP requests, because in mock mode there + * are none: the API is served by a substituted transport, so `page.on("request")` sees + * only the navigation and a `window.fetch` wrapper sees nothing whatever. Both + * instruments answer zero for a page that is polling perfectly. Counting operations is + * also what this test means — "refreshes the page" is a claim about reads, not about + * HTTP — and it survives the next change of transport. + * + * The instrument itself must not be able to read zero for the wrong reason, in either + * direction: `operationCallCounts` throws on an RPC the backend does not serve rather + * than reporting nought calls to it. + * + * The count is what makes this a test rather than a screenshot: the failure that matters + * is a control that reads "enabled" and polls nothing, which is exactly what the first + * implementation here did — the caching layer deduplicated its revalidations over a window + * longer than the interval, turning "twice a second" into once every two and a half. + * + * ## Why two RPCs are watched and only one is expected to climb + * + * The page reads the inventory and the list of namespaces, and polling deliberately + * re-reads only the first: the namespace list is the page's scope control, not its data, + * and re-reading it twice a second is a request per tick that can only ever answer the + * same thing. Watching both is what makes that a tested decision rather than an + * accident — a timer wired to "refresh everything" would show up here as the namespace + * count climbing too. + */ + +const SUBSTRATE = "/substrate"; + +/** + * The inventory, which is three reads now rather than one. + * + * `GetSubstrateStatus` returned everything in a single message and stopped working — + * a cluster of 410,110 actors produces a response gRPC refuses to send. The page + * reads a summary for the counts and a page each of actors and workers, and polling + * drives all three: a timer that re-read the tiles while leaving the tables stale + * would show a moving count over rows that never change. + */ +const POLLED = rpc.substrateSummary; +const ALSO_POLLED = [rpc.substrateActors, rpc.substrateWorkers] as const; + +/** The scope control's own read, which must stay still while the inventory moves. */ +const NOT_POLLED = rpc.listNamespaces; + +const READS = [POLLED, ...ALSO_POLLED, NOT_POLLED] as const; + +const readCounts = (page: import("@playwright/test").Page) => + operationCallCounts(page, READS); + +test("substrate: polling is off until asked for, then re-reads the inventory", async ({ + page, +}) => { + await test.step("1. the page reads once and then leaves it alone", async () => { + await page.goto(SUBSTRATE); + await expect(page.getByTestId("substrate-actors-card")).toBeVisible(); + + const afterLoad = await readCounts(page); + expect(afterLoad[POLLED]).toBeGreaterThan(0); + + await page.waitForTimeout(1_500); + expect( + (await readCounts(page))[POLLED], + "a page nobody asked to poll must not re-read on its own", + ).toBe(afterLoad[POLLED]); + }); + + await test.step("2. the control sits beside Refresh, and says which it is", async () => { + const toggle = page.getByTestId("substrate-poll-toggle"); + await expect(toggle).toBeVisible(); + await expect(toggle).toHaveAttribute("aria-pressed", "false"); + await expect(toggle).toContainText("disabled"); + }); + + await test.step("3. enabled, the inventory is re-read — and only the inventory", async () => { + const before = await readCounts(page); + await page.getByTestId("substrate-poll-toggle").click(); + await expect(page.getByTestId("substrate-poll-toggle")).toContainText("enabled"); + + await page.waitForTimeout(2_200); + const during = await readCounts(page); + + // Two reads in 2.2s at the default of one second, allowing for the first tick + // landing late. + expect( + during[POLLED] - before[POLLED], + "the inventory should be re-read while polling", + ).toBeGreaterThanOrEqual(2); + + expect( + during[NOT_POLLED], + "the namespace list is the scope control, not the data — polling must leave it alone", + ).toBe(before[NOT_POLLED]); + }); + + await test.step("4. disabled, it stops", async () => { + await page.getByTestId("substrate-poll-toggle").click(); + await expect(page.getByTestId("substrate-poll-toggle")).toContainText("disabled"); + + // A tick already in flight may still land, so the count is taken after a beat and then + // has to hold still. + await page.waitForTimeout(800); + const settled = await readCounts(page); + await page.waitForTimeout(1_800); + expect(await readCounts(page), "turning it off must actually stop it").toEqual(settled); + }); +}); + +/** + * The rate is the reader's, and so is stopping without losing it. + * + * A fixed rate was either too slow to watch a placement move or too fast to leave + * running, so the interval is a field beside the toggle. Two of its values are not + * rates at all: zero, and anything unparseable — antd hands back `null` for "." or an + * empty box — and both stop the timer while leaving polling switched on, so pausing + * does not cost the reader the number they had chosen. + */ +test("substrate: the polling interval is the reader's, and zero stops it", async ({ + page, +}) => { + const interval = page.getByTestId("substrate-poll-interval").locator("input"); + + await test.step("1. there is no interval to set until polling is on", async () => { + await page.goto(SUBSTRATE); + await expect(page.getByTestId("substrate-actors-card")).toBeVisible(); + await expect(page.getByTestId("substrate-poll-interval")).toHaveCount(0); + }); + + await test.step("2. switching polling on offers one, defaulting to a second", async () => { + await page.getByTestId("substrate-poll-toggle").click(); + await expect(interval).toHaveValue("1"); + // Singular for exactly one: "1 seconds" reads as a page not reading its own value. + await expect(page.getByTestId("substrate-poll-interval")).toContainText("second"); + }); + + await test.step("3. a faster rate is read faster", async () => { + await interval.fill("0.5"); + await interval.blur(); + const before = await readCounts(page); + await page.waitForTimeout(2_200); + const during = await readCounts(page); + expect( + during[POLLED] - before[POLLED], + "half a second should re-read more often than a second", + ).toBeGreaterThanOrEqual(3); + }); + + await test.step("4. below the floor is read as the floor, not refused", async () => { + await interval.fill("0.1"); + await interval.blur(); + // Corrected on the field, so the number on screen is the number being used. + await expect(interval).toHaveValue("0.5"); + }); + + await test.step("5. zero stops the timer without switching polling off", async () => { + await interval.fill("0"); + await interval.blur(); + // The toggle still reads enabled: this is a pause, and the reader keeps their place. + await expect(page.getByTestId("substrate-poll-toggle")).toContainText("enabled"); + + const before = await readCounts(page); + await page.waitForTimeout(2_200); + expect( + (await readCounts(page))[POLLED], + "zero seconds must not re-read at all", + ).toBe(before[POLLED]); + }); + + await test.step("6. and so does something that is not a number", async () => { + await interval.fill("."); + await interval.blur(); + const before = await readCounts(page); + await page.waitForTimeout(1_800); + expect( + (await readCounts(page))[POLLED], + "an unparseable interval must not re-read either", + ).toBe(before[POLLED]); + }); +}); diff --git a/ui/playwright/tests/substrate/substrate.spec.ts b/ui/playwright/tests/substrate/substrate.spec.ts new file mode 100644 index 000000000..3d235b155 --- /dev/null +++ b/ui/playwright/tests/substrate/substrate.spec.ts @@ -0,0 +1,357 @@ +import { test, expect } from "../../fixtures/test"; +import { expectSettled, loadPage, routes } from "../../helpers/app"; + +/** + * Substrate — the inventory, its scope, and the three ways the read can answer. + * + * The page used to carry a banner reading "worker pool and actor inventory is not + * available here… comes from a status endpoint this UI's data layer does not expose yet". + * That was true when it was written and quietly stopped being true: the endpoint, the + * client method, the hook and the types were all in place, and only the page had not been + * told. + * + * So this covers what it now shows — four sections, all of them the substrate's own — and, + * more importantly, that the read's three answers stay distinct. `enabled: false` is a + * deployment without an ate-api endpoint, which is ordinary rather than broken, and is + * said in the two tables it actually applies to. `ateApiError` means the Kubernetes-derived + * halves are complete while the runtime ones may be partial, which is a warning *beside* + * the data rather than an error instead of it. A page that flattened those into one message + * would tell an operator their substrate was broken when it was merely switched off. + * + * The fixture is built for exactly this: `enabled: true` with an `ateApiError` set, two + * worker pools across two namespaces, two templates — one Ready in `kagent`, one Pending in + * `platform` — three actors and two workers, one of the workers holding nothing. The third + * actor sits last in the fixture and first once sorted, which is what makes the ordering + * testable at all. + */ + +test("substrate: the inventory renders, and partial runtime data says so", async ({ + page, +}) => { + await test.step("1. the stale banner is gone", async () => { + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectSettled(page); + + // The exact claim that outlived its own truth. Asserted by its words rather than a + // test id, because the point is that this sentence is not on the page. + await expect(page.getByText("not available here")).toHaveCount(0); + }); + + await test.step("2. the summary counts both halves of each ratio", async () => { + // A bare count answers the wrong question: one template ready is good news or bad + // depending on how many there are. Both numbers, or the tile is not worth its space. + await expect(page.getByTestId("substrate-stat-pools-value")).toHaveText("2"); + await expect(page.getByTestId("substrate-stat-templates-value")).toHaveText("1/2"); + // Two running of four: one of the fixture's actors is `Failed` and another + // `Snapshotting`, which is exactly the case a bare count would hide. + await expect(page.getByTestId("substrate-stat-actors-value")).toHaveText("2/4"); + await expect(page.getByTestId("substrate-stat-workers-value")).toHaveText("1/2"); + await expect(page.getByTestId("substrate-stat-ateapi-value")).toHaveText("connected"); + await expect(page.getByTestId("substrate-stat-scope-value")).toHaveText("all"); + }); + + await test.step("3. the worker pools the sandboxes run on", async () => { + const pools = page.getByTestId("substrate-pools-table"); + await expect(pools).toBeVisible(); + await expect(pools).toContainText("kagent/default-pool"); + await expect(pools).toContainText("platform/gpu-pool"); + // The image tag, which is what an operator checks against a release. + await expect(pools).toContainText("ateom:1.4.0"); + }); + + await test.step("4. the templates actors are cut from", async () => { + const templates = page.getByTestId("substrate-templates-table"); + await expect(templates).toBeVisible(); + await expect(templates).toContainText("kagent/coder-template"); + await expect(templates).toContainText("platform/external-template"); + + // The golden actor, beneath the name: it is the snapshot every new actor of this + // template is cut from, and the one identifier worth carrying beside the name. + await expect(templates).toContainText("golden: actor-golden-001"); + + // The rest of what decides where and how a template runs. + await expect(templates).toContainText("standard"); + await expect(templates).toContainText("pool=default-pool"); + await expect(templates).toContainText("openclaw"); + + // Both phases, and coloured by what they mean rather than all alike: a Ready template + // reads as healthy, a Pending one does not. + await expect(templates).toContainText("Ready"); + await expect(templates).toContainText("Pending"); + await expect( + templates.locator("[data-tone]").filter({ hasText: "Ready" }), + ).toHaveAttribute("data-tone", "healthy"); + }); + + await test.step("5. the actors placed right now, and the pods holding them", async () => { + const actors = page.getByTestId("substrate-actors-table"); + await expect(actors).toBeVisible(); + await expect(actors).toContainText("actor-7f21"); + await expect(actors).toContainText("kagent/coder-template"); + // The pod, with its IP appended — the two facts an operator needs to go and look. + await expect(actors).toContainText("kagent/ateom-default-pool-0"); + await expect(actors).toContainText("10.42.1.19"); + }); + + await test.step("6. the workers, including the one holding nothing", async () => { + const workers = page.getByTestId("substrate-workers-table"); + await expect(workers).toBeVisible(); + await expect(workers).toContainText("kagent/ateom-default-pool-0"); + await expect(workers).toContainText("default-pool"); + await expect(workers).toContainText("actor-7f21"); + // "idle" and not a dash: a worker with no actor on it is available, which is a state + // worth reading, where a dash says only that a cell is empty. + await expect(workers).toContainText("idle"); + }); + + await test.step("7. partial runtime data is a warning beside the data, not instead of it", async () => { + // The fixture sets `ateApiError`. Both must be true at once: the warning is shown, and + // the tables it qualifies are still there — that is the whole distinction. + await expect(page.getByTestId("substrate-partial")).toBeVisible(); + await expect(page.getByTestId("substrate-inventory-error")).toHaveCount(0); + await expect(page.getByTestId("substrate-actors-table")).toContainText("actor-7f21"); + }); +}); + +/** + * The scope control. + * + * `GetSubstrateStatusRequest` takes a namespace and an empty one means every namespace the + * controller watches, so the page offers both. The test is not that a dropdown opens: it is + * that choosing a namespace narrows what is read — the fixture backend filters the way the + * controller filters — and that the choice is in the address, so a link to what somebody is + * looking at is a link to what they are looking at. + */ +test("substrate: the scope narrows what is read, and is carried in the URL", async ({ + page, +}) => { + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectSettled(page); + + await test.step("1. it opens on every watched namespace", async () => { + await expect(page.getByTestId("substrate-namespace")).toContainText( + "All watched namespaces", + ); + await expect(page.getByTestId("substrate-pools-table")).toContainText("kagent/default-pool"); + await expect(page.getByTestId("substrate-pools-table")).toContainText("platform/gpu-pool"); + }); + + await test.step("2. choosing one namespace narrows every section", async () => { + await page.getByTestId("substrate-namespace").click(); + // The one place this suite reaches for an antd class name. The visible dropdown is a + // portal outside the app's own markup, and `getByRole("option")` also matches the + // zero-sized accessibility listbox rc-select keeps inside the combobox — which can + // never be clicked, so a role query here waits for actionability until it times out. + await page + .locator(".ant-select-item-option") + .filter({ hasText: /^kagent$/ }) + .click(); + + await expect(page).toHaveURL(/namespace=kagent/); + await expect(page.getByTestId("substrate-stat-scope-value")).toHaveText("kagent"); + + const pools = page.getByTestId("substrate-pools-table"); + await expect(pools).toContainText("kagent/default-pool"); + await expect(pools).not.toContainText("platform/gpu-pool"); + + const templates = page.getByTestId("substrate-templates-table"); + await expect(templates).toContainText("coder-template"); + await expect(templates).not.toContainText("external-template"); + }); + + await test.step("3. the scope is the address, so a link to it opens on it", async () => { + await loadPage(page, `${routes.substrate}?namespace=platform`, { title: "Substrate" }); + await expectSettled(page); + + await expect(page.getByTestId("substrate-namespace")).toContainText("platform"); + await expect(page.getByTestId("substrate-stat-scope-value")).toHaveText("platform"); + await expect(page.getByTestId("substrate-pools-table")).toContainText("platform/gpu-pool"); + }); + + await test.step("4. an empty section says why it is empty", async () => { + // Every worker in the fixture is in `kagent`, so this scope has none — and the + // sentence has to distinguish "ate-api has nothing here" from "there is no ate-api", + // which are different facts and only one of them is something to go and fix. + const workers = page.getByTestId("substrate-workers-table"); + await expect(workers).toContainText("ate-api reported no worker assignments"); + await expect(workers).not.toContainText("not configured"); + }); +}); + +/** + * A controller with no ate-api endpoint. + * + * `enabled: false` is a deployment choice, not a fault, and the page has to say so in the + * two places it applies without dressing it up as a failure anywhere. The `empty` scenario + * is exactly this: `enabled` false and every list absent. + */ +test("substrate: an unconfigured ate-api is explained, not reported as broken", async ({ + page, +}) => { + await loadPage(page, routes.substrate, { scenario: "empty", title: "Substrate" }); + await expectSettled(page); + + await expect(page.getByTestId("substrate-stat-ateapi-value")).toHaveText("off"); + await expect(page.getByTestId("substrate-inventory-error")).toHaveCount(0); + await expect(page.getByTestId("substrate-partial")).toHaveCount(0); + + // The two runtime sections name the setting to change. The two Kubernetes ones do not — + // they are empty for an unrelated reason, and saying "ate-api" over them would send an + // operator to fix the wrong thing. + await expect(page.getByTestId("substrate-actors-table")).toContainText( + "substrate-ate-api-endpoint", + ); + await expect(page.getByTestId("substrate-workers-table")).toContainText( + "ate-api, which is not configured", + ); + await expect(page.getByTestId("substrate-pools-table")).toContainText( + "Create one in the cluster", + ); + // A template appears when a harness and an agent template are paired, which is + // what creates one — not the SandboxAgent this used to name, which the API does + // not serve. + await expect(page.getByTestId("substrate-templates-table")).toContainText( + "harness and an agent template", + ); +}); + +/** + * The actor list is the one thing on this page whose length the cluster chooses. + * + * A real controller answered with 34,356 actors, and rendered in full that came to a + * 1.4-million-pixel page which took seconds to become interactive and could not be + * screenshotted. So the table is windowed and its body bounded, and this covers both + * halves of that: only a window of rows reaches the DOM, and the page stays a fixed + * size regardless. + * + * The order is checked here too, because an unordered list of thousands reshuffles + * itself on every poll — a row moves under the pointer while it is being read. + */ +test("substrate: the actor list is ordered, windowed, and bounded", async ({ page }) => { + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectSettled(page); + + const actors = page.getByTestId("substrate-actors-table"); + + // Sorted by status, then by id. `Failed` precedes `Running` precedes `Snapshotting`, + // and the fixture lists them in none of that order. + const ids = await actors.locator(".ant-table-row").evaluateAll((rows) => + rows.map((row) => row.querySelector(".ant-table-cell")?.textContent?.trim() ?? ""), + ); + expect(ids).toEqual(["actor-0aa1", "actor-3b55", "actor-7f21", "actor-9c03"]); + + // Windowed: antd renders rows into a virtual holder rather than a plain tbody, which + // is what keeps a list of thousands off the page. + await expect( + actors.locator(".ant-table-tbody-virtual-holder"), + ).toHaveCount(1); + + // Bounded: the body scrolls inside itself instead of growing the document. + const height = await actors + .locator(".ant-table-tbody-virtual-holder") + .evaluate((el) => el.getBoundingClientRect().height); + expect(height).toBeLessThanOrEqual(520); +}); + +/** + * Each section narrows on its own, and every column sorts. + * + * Four searches rather than one for the page, because these lists answer four + * different questions: narrowing the actors to one template must not also empty the + * table that says what that template is. + * + * The count beside each heading reports both numbers while a search is active. A bare + * count under a search box is how a reader concludes their cluster has one actor. + */ +test("substrate: the searches are the server's, and a match is found wherever it is", async ({ + page, +}) => { + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectSettled(page); + + const actorsCard = page.getByTestId("substrate-actors-card"); + const templatesCard = page.getByTestId("substrate-templates-card"); + const actorsTable = page.getByTestId("substrate-actors-table"); + + await test.step("1. the term is sent, not applied to the rows already fetched", async () => { + // This is the whole reason the filter moved server-side. The actors are paged, so + // narrowing a fetched page searches only that page — and a match on page nine + // reads on screen as "no matches", which is worse than no search at all. + await page.getByTestId("substrate-actors-search").locator("input").fill("7f21"); + + await expect(actorsTable).toContainText("actor-7f21"); + await expect(actorsTable).not.toContainText("actor-9c03"); + }); + + await test.step("2. a narrowed list never reads as the size of the cluster", async () => { + // The count beside the heading is now the *matching* total, so the tile is what + // keeps the cluster's own size on screen. A reader who searched and found one + // actor must not conclude their cluster is running one. + await expect(page.getByTestId("substrate-stat-actors")).toContainText("/4"); + }); + + await test.step("3. and only that card: the other lists are left alone", async () => { + await expect(templatesCard).toContainText("coder-template"); + }); + + await test.step("4. a search matching nothing says so, and says where it looked", async () => { + await page + .getByTestId("substrate-actors-search") + .locator("input") + .fill("no-such-actor"); + // "anywhere in this scope" rather than "on this page" — which is a claim the page + // can only make because the server did the narrowing. + await expect(actorsTable).toContainText("No actors match your search"); + await expect(actorsCard).toContainText("anywhere in this scope"); + }); +}); + +/** + * What the two paged tables deliberately do *not* offer. + * + * Every column of the actor and worker tables used to sort. That was honest while + * the page held the whole inventory and is not now: a client-side sorter reorders + * the page it was handed, so "sort by status descending" shows the last status on + * *this page* rather than in the cluster — the first row of a sorted 410,110 actors + * is almost certainly not among the hundred on screen. + * + * A control that looks like sorting and sorts a hundredth of the data is the kind of + * quiet half-truth this codebase keeps having to undo, so the sorters are gone and + * the server's order stands. The two inline tables keep theirs, because they really + * do hold everything. + */ +test("substrate: the paged tables do not pretend to sort, and the inline ones do", async ({ + page, +}) => { + await loadPage(page, routes.substrate, { title: "Substrate" }); + await expectSettled(page); + + await test.step("1. the actor and worker columns offer no sort", async () => { + for (const testId of ["substrate-actors-table", "substrate-workers-table"]) { + const headers = page.getByTestId(testId).locator("th"); + await expect(headers.first()).toBeVisible(); + const sortable = await headers.evaluateAll((cells) => + cells.filter((cell) => cell.className.includes("column-has-sorters")).length, + ); + expect(sortable, `${testId} should not offer a sort it cannot honour`).toBe(0); + } + }); + + await test.step("2. the pools and templates still sort, because they are whole", async () => { + const headers = page.getByTestId("substrate-templates-table").locator("th"); + await expect(headers.first()).toHaveClass(/column-has-sorters/); + }); + + await test.step("3. the actors arrive grouped by status, which is the server's order", async () => { + // Stated rather than asked for: ate-api returns actors in whatever order it holds + // them, so the same actor would appear somewhere different on every poll. The + // server groups them so a row does not move under the pointer while it is read. + const statuses = await page + .getByTestId("substrate-actors-table") + .locator(".ant-table-row") + .evaluateAll((rows) => + rows.map((row) => row.textContent?.match(/Failed|Running|Snapshotting/)?.[0] ?? ""), + ); + expect(statuses).toEqual([...statuses].sort()); + }); +}); diff --git a/ui/playwright/tests/theme-contrast.spec.ts b/ui/playwright/tests/theme-contrast.spec.ts new file mode 100644 index 000000000..bf541e0a3 --- /dev/null +++ b/ui/playwright/tests/theme-contrast.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from "../fixtures/test"; + +/** + * Text the brand colour is painted with has to be readable on the page it sits on. + * + * Three separate places got this wrong the same way: `primary` is a deep purple chosen as + * a *fill* with light text on it, and used as ink on the dark theme's near-black page it + * measured 2.2:1 to 2.5:1 where small text needs 4.5. Each was found by measuring rather + * than looking — which is the point of this spec: a purple-on-near-black link looks + * deliberate in a screenshot, and a reviewer flicking between themes will not catch it. + * + * The ratios are computed the way the eye sees them, compositing every translucent layer + * down to an opaque colour. Reading the first non-transparent background and treating it + * as opaque reports a *tinted* panel as a solid brand fill and fails a colour that is + * fine — which happened while investigating this, and cost a wrong conclusion. + */ + +const AA_SMALL_TEXT = 4.5; + +/** Contrast of an element's text against everything painted behind it. */ +const PROBE = () => { + const parse = (value: string) => { + const parts = value.match(/[\d.]+/g)?.map(Number) ?? [0, 0, 0, 1]; + return { rgb: parts.slice(0, 3), a: parts.length > 3 ? parts[3] : 1 }; + }; + const over = (fg: { rgb: number[]; a: number }, bg: number[]) => + fg.rgb.map((channel, index) => channel * fg.a + bg[index] * (1 - fg.a)); + + const solidBehind = (el: Element) => { + const layers: { rgb: number[]; a: number }[] = []; + let node: Element | null = el; + while (node) { + const colour = parse(getComputedStyle(node).backgroundColor); + if (colour.a > 0) layers.push(colour); + if (colour.a === 1) break; + node = node.parentElement; + } + let base = + layers.length > 0 && layers[layers.length - 1].a === 1 + ? (layers.pop() as { rgb: number[] }).rgb + : [0, 0, 0]; + for (const layer of layers.reverse()) base = over(layer, base); + return base; + }; + + const luminance = (rgb: number[]) => { + const [r, g, b] = rgb.map((channel) => { + const s = channel / 255; + return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + + (window as unknown as { __contrast: (el: Element) => number }).__contrast = (el) => { + const behind = solidBehind(el); + const ink = over(parse(getComputedStyle(el).color), behind); + const [lighter, darker] = [luminance(ink), luminance(behind)].sort((a, b) => b - a); + return (lighter + 0.05) / (darker + 0.05); + }; +}; + +function contrastOf(page: import("@playwright/test").Page, selector: string) { + return page.evaluate((css) => { + const el = document.querySelector(css); + if (!el) throw new Error(`nothing matched ${css}`); + return (window as unknown as { __contrast: (el: Element) => number }).__contrast(el); + }, selector); +} + +test.describe("dark theme: brand-coloured text", () => { + test.use({ colorScheme: "dark" }); + + test.beforeEach(async ({ page }) => { + // The theme is the reader's choice, kept in storage — set before the app boots so it + // renders dark from the first paint rather than being toggled mid-test. + await page.addInitScript(() => window.localStorage.setItem("kagent.themeMode", "dark")); + await page.addInitScript(PROBE); + }); + + test("a prompt library's name is readable", async ({ page }) => { + await page.goto("/prompts"); + await expect(page.locator(".ant-table-tbody a").first()).toBeVisible(); + + // Was 2.54:1 — the only navigable text on the row and the hardest thing to read. + expect(await contrastOf(page, ".ant-table-tbody a")).toBeGreaterThanOrEqual( + AA_SMALL_TEXT, + ); + }); + + test("a tool server's kind is readable", async ({ page }) => { + await page.goto("/mcp"); + await expect(page.locator(".ant-table-tbody .ant-tag").first()).toBeVisible(); + + // antd's presets are derived for a light page: these were 3.4:1 and 4.2:1. + const tags = await page.evaluate(() => + [...document.querySelectorAll(".ant-table-tbody .ant-tag")].map((tag) => + (window as unknown as { __contrast: (el: Element) => number }).__contrast(tag), + ), + ); + expect(tags.length).toBeGreaterThan(0); + for (const ratio of tags) expect(ratio).toBeGreaterThanOrEqual(AA_SMALL_TEXT); + }); + + test("the tab you are on is the one you can read", async ({ page }) => { + /* + * The same mistake as the toggle below, in a third place. + * + * antd colours a selected tab and its ink bar from `colorPrimary`, which is the + * deep purple chosen as a *fill* with light text on it. Used as ink on the dark + * theme's near-black page the selected tab measured 2.37:1 while the unselected + * ones sat at 19:1 — so the tab you were not on was the one you could read, which + * is exactly backwards. + * + * Every tab is measured rather than only the selected one: a fix that made the + * active tab legible by dimming the rest would pass a check aimed at one of them. + */ + await page.goto("/agents"); + await expect(page.locator('[role="tab"]').first()).toBeVisible(); + + const ratios = await page.evaluate(() => + [...document.querySelectorAll('[role="tab"]')].map((tab) => + (window as unknown as { __contrast: (el: Element) => number }).__contrast(tab), + ), + ); + expect(ratios.length).toBeGreaterThan(1); + for (const ratio of ratios) expect(ratio).toBeGreaterThanOrEqual(AA_SMALL_TEXT); + }); + + test("the selected half of a toggle is readable", async ({ page }) => { + // The model form's authentication toggle. It used to be the agent form's type + // toggle, and that form is gone — creating an agent is now choosing a harness and + // a template, which are pickers rather than radio buttons. The property is the + // theme's, not the page's, so any checked radio button measures it. + await page.goto("/models/new"); + await expect(page.getByTestId("model-auth-type")).toBeVisible(); + + // Was 2.2:1 against 13:1 for the unselected half — the option you had *not* chosen was + // the one you could read. + expect( + await contrastOf(page, ".ant-radio-button-wrapper-checked"), + ).toBeGreaterThanOrEqual(AA_SMALL_TEXT); + }); +}); diff --git a/ui/playwright/tsconfig.json b/ui/playwright/tsconfig.json deleted file mode 100644 index b84b7846f..000000000 --- a/ui/playwright/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["node", "@playwright/test"] - }, - "include": ["**/*.ts"] -} diff --git a/ui/postcss.config.mjs b/ui/postcss.config.mjs deleted file mode 100644 index 2ef30fcf4..000000000 --- a/ui/postcss.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; - -export default config; diff --git a/ui/public/env-config.js b/ui/public/env-config.js new file mode 100644 index 000000000..760b75a63 --- /dev/null +++ b/ui/public/env-config.js @@ -0,0 +1,8 @@ +// Placeholder. Never ships values. +// +// The container overwrites this file from its own environment on every start +// (see `scripts/init.sh`), and the dev server replaces the script tag that loads +// it with an inline copy. An empty object here means a build that somehow served +// this file unmodified falls back to the app's defaults rather than to whatever +// the machine that built the image happened to have set. +window.environmentVariables = {}; diff --git a/ui/public/login-bg.webp b/ui/public/login-bg.webp deleted file mode 100644 index cb73ef775e412c542a231e462cea31446eca6dcd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 269334 zcmV(yK0Kkwzn;p`#2Pog z31?`Mr(q9Z|Nmh>xBv5{|NrS9@PGgF)&KuI|6`W_|NWJJm(xY8|Ma=lfB)+<{0iMQ z**Z6Q2%e|-}s+B8UXg@dw${nPwsDFzx}<4e-i06 z>&yG+s=l6h<(H^`V*lm;1JrlY|I7b_{zJ$=&OX)s_xK<4zq7wR-^u5X;{VP6>;B`^ zpXcA1|Hb}q+pqV3`oGbCx$}qgKjOdi|D5&+{G<9m{4ejn?7~r;eTlTo@dAQ{-FQf|I&Zy_MgNv`d*`Hv7x)1$LsPk*aMr(NI7=PFQw@nT{+bH%4vtX9WhU#p9pXVx?o>qXhC%) z8w-8Y0{bIE1Y~*2+(94PlHd2fli%1jTdTsUWx339j~QD!dC{nFRPPAqe168=^~d|l z)&F(4Y24cxtMv*xrI2)XW;kq6T@s9~^npxwZTcW$-}Jzrjwe=>x=j+)qJB>q*oIzA zQ?kd8VA9w~q{yUCEO8Xr#VhR8qF7O=1HtT)Eb6JPHT6rV!V6^eo47`yw)}a#6Ip3@ z+rstax1$CRDo~_68xVLn9q2VVDPDAz#N`MbIij27L;Q3f1sY=dt>>m9iF=JBh8MX+ zst^w$2UoA45xO6n4|*U*nsYm{?;KfQzO7}j+kR0R%h=m=cItBzCAFlu+iif_VED4N zd!%D^7p98nkQbQKEgYjok(!plvTB6;FN6kXgPfN(+2X3$X+jf23+SySWlX)EVBS)V z(WMbhzA`}BXB{tM6knIOEODdwiYL!03+}!q3gP$t^Z<;LB>}Vn-|(5?AA8wL3^)_R;Wu0$9{ur?<`R0eZ1K3bsgP8BvyHP`qk0w{*^%HL;IEc zNmE9*3kTuz77N&G=X687ELjSVq#5XzjPf;R?!0LLBS*kx82ApDQ4`#;dbbZs}3 zZ{enE*lXha-8;1!;i{o2kd5S9j#PED7RV1Uac51VnF-3Da&Yh|YLXL`@JRf=USN-kDcd-KT^m#U}joMfKa*^uGnz8LH&ZJzyC)o&;tIM_-$ zY`)>cMe&RQh9BzTM^R@w#%YH7k2okAJAlW~46`t#gI^o8SZ0S$k7rFo8PE0)B+3){ zAJLRz6V-Jx2h&|1cC6BGVZ$ zp|PHl3I7!2Gxh`n3?2K`X&qf~hOpkaJ5bZ)Y%GuMs<3ub7#aUBe#TPiFl`(M&t3GR z{{a5V6Ym&F-)sR z+7Mb?z@`VKJaj6%LW=xIcdJU~`2`n^lyYops$hI(W^~|jq@kUa>4d|z781boio-Bt zD;WidKe|2QhMZ>c+e0kyZwGC_h%LY0T{3`!bLuB0r8N{F6GMs&Y|uytWi(C;7)j%- zsMRrU1DD`UDf}m+08^639(IE&AV#d|^bs&3x_Cw=-Uplch|+k!F?LG=_TJ_+!|udO z8rb+Eg;epuLNpIMJ9n85dQCGZ{0yd8{7Wv)dt^{^o>6_%Uv1a8yd3$q44*p9IP&14 z7y5pF!fBTv`>8%Gl@LZ&OS-(guauOQ=c$$_WMr5NM62=HpyG+MO6D_7;tx?b3P<1= zafYiLA`biNC<472HADx^Dxh?xvqNe%?h_y2Z2tM-qwj<;z4M{odl5YJkbhh3sLYB) zo$PyWORRXcHz`0R4E(TZe=0sGJxU$Eba(&osvd4;(Wy* zXY;ur;duqLK#F}JQob8ou-!xSo>j9}O+Z0)v|*DV_x2Zed*6oLb8uXb)IN z7yxXL@w$<;hrhH>EdR7g#X!Taq(Xw%e#$4TlgzEirjh9q!K66d9Ru^)OSV|#BB<{; zDQCa(9Alj*ms;C>Yb&#b3OtR_Xx1vF$C)#eT?d#+6y9h^M}(JWh#f*>n9;jD?m-!6sJpGMs{^)_9)^`WRD5U(HkISKF$BSQ}TM>i9BVk($;gK>|1I zdo0ln|8{c&EjAv_VXRjf1V4);x+#Cpp|W;y^enSDo1qGE5R1~vdSKZ9-dH(CH2g2i z$9arNfFo`yVQK-6KID#mgqoPVvUsJ@IvwuTQ@5bdxn#3D*A$!7Fg?%@QeF@OG*_%U#M?l?ues8SAJ8+IRtA*L zk=_p5OhEbi+qW(+>!htW-WL~B?!<|4Nqxwe)wnW%MIcsgtV5wS1rVlkg9IBiJ0~~-kI0x`p1Me;P;Xx}^aUYSK zUMZmEi49DN1^7bo&;h{{bEKkScvK1|G+l&Z8cuWTwiqEmV$UwFsT z7(Kq|%|tJBd{N5l^_9*MYM7ySwSABTkuk+X)_SanN)O5^$wwia(yi&-I3mu-1TY{D|!3DZ=hbMOf zxC5Q#YLe%iFB}VCScWofhqxSWE_rmKb?uszlF<>%NgmXVwYuvx43cVS`oNEl-o4p3 z9WmKLAvNyb@13d(3#9}%FQ0#C)}5$8D|Aqg`gFk_ITS`E$h4s#FTqi8JY=&Ce{SHO z>L1U&qQNK>kp_QxF4|RO7iX|E^Qz&A7gP6BUm8F(`mgAD_wLqQAYkw+nh^N6YRV`( zJJ0~hjRl9K9CF`}vj>AKCpt#=x=Yzx52=63n%WkjMS5XD1s-|?^xcF|koF%B}W z6x#>xe3&`#j1V(HarWRU<6pm~Lr1+8|(&SG6niS}X9>`9ssCl&L6T z;W;?uTTP(gh1b47M_^SAB*RPQ~36$o5ZjF9TW<2vJt=B`t+XAEo>t zQRT*@3f&B(_x!G0TYlNL{}M9T`+c7f*DT=EI0^R*%|186!F~SScI_B7gE}W^Svq_;95Da#$6WM4A*B^vDf|k%qjQOlR8gQ%5@oN zYZX1l3#q}d%?~Brs9wUlQPWpQl8yY2nIEQUE z*^)wY^ZLnZ^ggE&qvaKL%XHYF# z(8(b=+gg|9xU9r_h3=xSJ{xfftU{GUK@iMi!&k_-p2&%K>Q27Rv zNlAVWi0~dI$BppEWn$#IwjkWc)65XMU1S=wWr@VUj1bQ%-r3nVwM^JK`R^#GWm&x} zPg<4P1R}FPBAAWOWpy!vWAA*(rfMJU?M}A44W7{&)(N?Un>{RxD7T;NK4<_<+Fe17 zee8{tEQ&ay>u`uuE8=I!BJ>Y3lB|Voc6f(34rJmpAPC^KG}~W9iKI6a+JW*RxZzFe zi^vud-BEaTMHzTnn&U-K_(7xQ-eT4cHQh2Mk^>KB5oYPTsEf|TWr;>!hGpa=5Eb+W zd-XO3$1fNw1KYHMw%}Br4857`ol+QX0|V~1kC_xILkh2yfu)#@N8O|NPgf^SEn{oekFs7Ku+JK-``qx$5%F1IGxAzqR zw2@;OF{MT^krcYWU0 z-qg!Cso@3103;XblzqGrqhG(}dU` zynYwCs?eC4*y{vjXTJXRcm=>3nrjuO^|odz{}^uJ-|0yfcetYBrK4-%PlnG6Ckgr4 ze5Gn(r4GhSQC_jRG5|m5pxobwGv{f0HabW}zFwqVYOjwYc~Cy{$3$<23gzuE3Luj- zM+`oXct+F(sw98z2Dl#=Dac71RZ)gH@;5X1Bww?Yq!7LoH5apt+Y3sqs_@+bXb=Cg zR+^I><;bWB2j!Yd1M#f;oi|hK>9!ZEIBJ{C)n4Db=5pr;hM4^P(;}+)lX;IuXkyC|ZzQxz4-( zd{xbr*9p0^KXr&pHJ)t~z020$O1en-XXH~B1Z-VlAO7`<4GUd^OvK$y6&2n{UC!I!-&k-wTYQ{MNfjWIgn zP?l40UaugHLfE}hl*`G3L(x6R)jZ)C*wlpgMej{GOcOs4y_^QQglf`nUeT9f3U@{4 zu72sWsQ>~*8pw3L&uLRHpy;KVf6BEyWpH$R1VH|NVPe>md zd~U)f)Ejq;wfxdNCJExGzsTPsitRVZ5iP$k-|f;qLL*zDMy~o4leAAg_0iZh24x$t zY!r`7s5u&;Q}02X?ouPZ*>$94yNen20A8eF*E)23^?#)-{7H}~sDsypK>c@?3&?R} z8Qy0Ztq-7Idh4&%QEws=AzQ9LvgfQ!iTBy|>;^eDFjc4pyKYBSsdyK+@=N z8%fgqpQBU2znh;KI*zM{&BytZcKA2}$SB)s-A^OCCZwm;9niGiU99QM{Oi;=8zeQ1 zlbU_75xuOjRPiU_Bj5&=b z@x?vA9LSD(D1%l;HQYhnE^w(z{z03Sd<-<>$mES9CF!Hza3@Waw3GOkC&ycv#M+OuDO&93{&KF3)kMXOv>ylA|Zawj1c(?f3-o27P~UHTTgM)8%e=X3mO?dHhFk z0=j+(?NR(=W6@#Z;#GPHK<7}U_ai^ER#5+724`O$#%jxFx8ExF?O`BxcL3?k=I_IK zT%S=^nq1kbPVYSq$Uk!|CLr3pLC0cTU;Fq1DH*%|mjPQFRs7jpNr2GZpTw%-ka^oc zpSr?g!P)^#6S}Ylhs+qmn36p%$I2DA^hWc4v>G;ThwhcYz-|2)a-F(A6v4Nu70^jv z{?o6lYGi61yexhyuN4Qiy)^0Kozvf)sDKm?;scwI?K8>v%7x5>Qw(N|EX5r}NA%Xr z-UEU7&$=xDv;XtS?)yn9Bhf*yvCiT${(5J7RLqf=3Wy8(ZNZ#0Cr_oG;NE4Y&>T-c za;8Q$QC5vUOvZM-^;JHmn6{s)UuqJ~M*|`Cygam*Y;}Gy3}y}UP49gk-ZLWr1tznb zmHCtuWFWmm>d~h}%A4~{`XqZLPqBvBs^B=SXKgIA?{Dyg82I;Ykh$k{*%5O_GO8jS zGs0(mZayjfE!^?f-QUK?DJZ-!{)I^G_~s`uFAWU&`w*#vA$4bO`OL_8GW?>WO%&yT&Jdbz_`Q?O$+H( z7%zA`Rf%M(vbRH)KMPAba?g=qBm423+}CFj(qA#)@a6mt`?s>5K+zhD7k=>NBe|Qp zzAj?n?j^>UPXjG*X;woGK*ZaCRLkB{AsOAJhXiCiQm1UP*&;b}RaCkS>^iEK zf_=3T>LP}RIJjNLo4n)boSN z%Bw5Ch(49u8n?TC2A}@Vf+1P39LM+!?q=-8>-!$ep@Ll)5VRg=zm%-&9WbBM{8^0)M*r-(lk(jL zzxzGxbZ36Av?u)u2)Cfbr~w~|t*=RyN=sw@Vkv)IsK-;K^5_Wsnx0xrs7q{>5*;b{ zfTYc2CjN5~%Jyzw%w0ruhQq8Q$2aY!i9XDBfcF>xm_uJaOYC1HGn&a1;w5kV+W6*9vgWY_jxiXG(iedp8*m=gAgC9U*ugic~)qZSdaNfIbjIzTe~vnNRhwy5SbY_(0?zrhIumuyAU(&DyH`Z^o2Kjrw3Z$OvQgPwl5NwgsMAum8Z zwB=aSFotyKVjpt`9BwCf=g@FDgx-$rTr*sFs%)uhc6@5z+3sSk8@IjOap@SBIe32o zUkW=pa0>B&+m%jB^FwXqxjE!i>nL6>4>foWV3J#OO{0%c zHMQ^Yd0$daFal90fYM=mC!VR8`Nv!nk*3$bL^t62l$45(+(s=-Q8@Ge_zov@VF?}B z>SCU)`9dCHwzM|C|MBnAI{Vfro2qH3Bs>c||6}1#m;MLy>rH%&E3g?bq;FlDZax_} zsMv$qYni^;XJsmL9q0Vvhf(G2QI?mJglWHJ2I_G0&di{Wvh8}1+Wy1D86A7KXP0Ri zpUBF5NBI=2Gsl8S4m~=q*KTCW$`quMrr^5u>=7C{YOpI8i z##Utpu$$Nf*I9KK_mR`a#W9U)vi6>6()rT=lBf;y3GN>aZ+;=yJ2TLM<{MQfu5hjl z{~L~Xk5u6u+^7v+%@hf1GAU~!0c%i2r?{BRFB?}sE)fokwiMP45G>t>nCj=;FC%_9 zZR;QZS1|vqN4WpbZcH-&8=>_B$6?mSBPf?hR#A4yPMR@eVlP@vhc(b?;xQ4*`@Bc( zo+nj8A^E~^`H4JBiSCllT2NXRi@9wNWf~z(8c;jSGYM40AL3Wrr~3<>My; zJs73#VYpih$$G3;!7ZpPmM%;6#c)M7+P-uv}}~8bqYiCk}Bsa7?sL$IEBsmCQS2hV>9aO;*bHvi}TUi zY368ECgSz6Cz+vDm=z9vg0W2TG)_Su(EmC%|Ns2#4>0>jfdBuu!ym^noG8k*Bg zzG<(Zxr8fKWgY>69N=AeD)O+BHSqdP-yavpt=k^K8qSCR`T*<+X?ytZJTmY6#g@XE zRj+zW*o*d`6761l;=|#9DjOzcxJ_lPAyv1Qd@SX|U2)<_YxAvM@{QjRR?Y}i>VT2d zU3+{y_g}>GmMea7vjR8ry^-rhK&6^fI()Q!TJNXW!)C}DvJAn_CS}UEbM7h0yeTun zspd7pza$qi1V~NYHN1(+uBXr%o(waZ@TzY&j|FY|2{j7n( z2L+A4DMIH*3fkr!UOpjVrxmeCm>Qa7X#p^9R)t6%XG=#ilb5deafP~4kAIt<(aQ`iTKA-#H&na)SwRGB*h6Zt_{Bb{A^bbnrl}Gd6-L)WN0Fg4>E4(|~I*Kop`#%=DhOi6u7#S;Vw(z|j2FemK+wtYq9f8(`9G4J9s zDye3B%DqPbTej%a7^y8}H6WX83v*mH<>XWg))CfPOI6Z75QA`LKL1^vWwCMEz1M`l zs-3EWh`>bQ7Zvghfyg>2zP4X@9|jcv-%2Q>)AF8l`_Cd+UzClE%?XvAa|o z1MzeM2t0{sUsJ|KRILWM6kYaKk3TyMXlhHe)V%~=-PHbetKmo(RIsFXLJi<>sK5!n z=tJl{l}rQ_$`(WqdP$3JzqS`km%Z&UZ1|bx-elyLl2*aj6>xm7lzrI8=;_+_SF5le z@+^*DeE{0?YXtJ5BB$u3@+U%aNFt!;p*n~Qdr+Es7*T(AQB1N!4rr|oz();2s4~=L zH7guiv%YlYVP(cXBqNbMpy)Ie?ZE}Mv%z!H)PddaY&2`K&HsrB&p9meJ<&P^?MI z>QQGn4g;WZJrf(T9^dtMYGW+MW-XXZ@#A6v>0lIGf#%yODoszE4~6TtUFvtC^3 z*A$2Df{v^d(uWC&~oYtH9?^|mz;#dFq zf0AGSgn4dp>AGs&B7^@0Wik=XAo<=K{b7V9 zB6)MQ7 zbA-?uAR?oe4cku@#)8Zck`c|qPp93E4eLU5@|YGSt!4n9pFm)-46+iIABdD#k_Bu$ zJJco6rWS~0dU@1h5Ch6!bmtj3V&YF&Y+IO!AA@qyR{}w7{XJ(Wq{)CQbB2Tj*OHJn z!G*%aN!F@NKm*d3W)>X&Gp{XlXXj8_O}pGi0js zfdEKhuwC7cP_wqe(76pCf$qK|nOB&>k6r}LI}@Fi8ZW-1C#$s7_>#5fFaT*^WEep9 zvwFrh!53WQS!>fuTIhxe>+lRAcHs*)QOan&{Pnc09oY1!5eUc=8Y;o&c|%SeJi?lM z9}&w-oawE7*)6E%l(s`mJ<$R)w&dolAn)9Yg=Yc;4fhP}z4%1%8SV@Rv7}1KXq6RD z?^_(aS`&j%hUYM@99a^73`j4zuU0F`ABEJgKHa5~>TMcp#}Rqi4tF2HS_>2y%t+qz z1xTl0gEzZ2;OB0p3bAi@B}2$$NHfF>?s0+4H{bNe7j?rv4KZOk|18?Gr*e@Th{j$c zy3YwOITT=-QePqAQ6#m3_Y&+*bpXi?IZV_&qrnv{g~{1sJsUoOR1pP}@Yz43nloa$ zDj#!X*h`@ZmmVcC`s%F}Hs~$jQ({3!nYEHHF}xfe79bU^LfIzd7Dp@mA9Cxjix8H0 zl)ql5?Ed@x!unY$vexSBi%&o?dz1Qfh!Lh9=wqj#zAmP5#HZTEe6wa)d=!BPi z;=wvDViXez?=%`JTHJZ?-PL6J*g znCw)-4TT#|G?XA%kd(6N>aaCB4q&9kacVj4PmM~4_l0h0+Z3=FZk+f>pXRsNXG$}z znP$O1_h9A7Oq_>ZcZ{=01O79h`P)7$weqemGEyGtlJ3u3v!OurpLUob3U-{m%NL0e zipO#P9MJ_(jF=cTv4I0B3HR05`kj+$tV|ikniwmWI%uQ{+ZfO-QKsrINn==zu;uf_ z1X`v6wgm`8G{V)RkeU!>FI|IzZDfSR65_8GnFz*_a%o+UWS1f41<0#KCSm$JzrNzZ zl-)Ej!E1Nt&>c!#BrW7|JwW8Yx0>tH44y&*%lLGi)um)}vQ8I7CF=@Ix`$&T8QOKL5@*bgu6R0j=kVKgr}2^8QK=KWnh z{WrcTHB)hkeSPZ89e`ppMzn8jct*|W;{O_Z)xvpd|)Mp3UZAvG#G#w-Xtd~b1ZcYSw@zmLl>XPpDMcm4An$T>neNXBsEtSA4Y z;5lmaFvry*k7n7?l{L?r|91{w5$S1crgOwn)Jw13dEb)-h8}th@0E=AP!cm z{>hhe_a8a*k^S`cU)*_x9hL5k{apC6SB4f-J6kmlPGWMm5mrcLj-vgLrX(Hg0FLn{ zXM$j1J;C2q#E^ORBD;+o9qra10k)p{ekVjNjD#B}*4@XWSl4*wHmfWdBhB>DgMT25 zHjsC4kcbTp!s}Y{^_JJWRoIs~ACeg(zth)U!63ZJU|Gqov5(L$+J0F(2S1CW`SwU* zo8I+qucx&V5Rn<^`(ux?ZKGWueKX~lnuCjWxtb+r^Wr46lq=1QJ#C|sA+RpNq29TD zS(QBbLodb8<@L}&!|i#xIjU6k)BQdpyEWr2Uvvd*j7ZNy%xX`TV`=~)TZ{vx(8%tr zgEiRiZcpF)hoE=={4e5YjxgCgd>Es{WEku%g3tKz>NG=bW24A(+B?!v|EV3Lh|oR ztB+JRK=+JVg!1tXH?AtIv3yWU4Ir^bn@J-pbMW(d9h3Gh?B=(NR;FR3C(Rr;g&R7# z7eTB|`snCdTl})Q6XZf453vf#WJ6bTD+ zcYoVCft;pNM|u%2EihQWbC8OikY6t9Jie1zy)b&C@^0}R#deY10+E*PQpR?=s9JAMZwi5nhk*) zyPIqL>*JaCJg{67Et&sJ^ahEE@I2J}xcD$RuXOMEfut!oK{eK8h(X^!`ndi5PR~=2 z@fL~n6bdZ#7vyP+EgDmnKhl*C*5L!IK9x zxvuX@RlB9HG3M1gH1|Vsye?Z*YnkO;&sdQvAE?(Tq~1{7D;VYN^)5XGymLgyv^Mv- zq2|>mwA4g2eEt+RsPNrYv8HP);d`O}kghsd3rJ?a^P}K%x|>b0V*0*r%0XTTwECl} zGv+$`T18&C-t(C5!_Q)^^3626!yn?b8`4Z!Ot$x<-u@3fhP<}t>6?_LGCkY2$TMH- zy`uYaHxzXjKb4&YN!|-^91t`{X4gVLq>^VBxrf1cQK^%wKEt}7t&o)KWG^@0yLm|(ruDc?&r zGi1IC4mezcGU0&9P`S-o409Emdw9MD+K4NLn2II0=~pxs^Cq)&cc^xF-A=VzSv{sp zAgxAo1&ok_L23}pF#2iU#G9F3LT6layCHR}N7~>4QUcVO zCzf)SCGYFasO{7rbxAnoKhiSx037r#r%HuO#DzklwUl_@>;q->~wiRN=<+N z`GZ9T_*)X#$<_BBD7)wM?p$8f!D>ENwFuX@W;wnlocX$*g9&mm2;>qDd_F3fL}gsd zyyqR9anK-mBi2W;SlmS#A*QZ{&=AjVeP8}p$M8Ghvw$&573#Z6_iCd>=hVj-!C}xX z2V}9d|CXZvrJYQ+zec1(uq*zwm2Uep|6(D>oRu557OUQZd)8DRL^eq_*<1!#5253Z z14sYaK~jEZER6R{Y|^vk&OH5XX`KCvd@@XPNBtZg%1(qXom|sH2TXseC5?WKC1e*= z06xR{=H)|h+hV5EF1ydiptuU5D-LTPlgFAhgxQOC%#aW$&Kl|nyup%JP zt9d+%$gXm2rwcM$_X>Yb%7Rbfx&)H{@L?A$W{?p*V7Im1j`{tTz2jB@%MaWzvJ}WG z48gJv54>frRF3EZleS?HuDdUAm^>NC1^;1b@rdc5(_j8(Lr0)UH0iN&E>S9+he0gF0fXKQ|@ z-uNhlYu!PV9Y!HFdjzKX`F|<$5d*PLuXKu{{*>-c>;9J%?Tukizts~Gy%xtz!V|!) zTbU=eG&-tU!}8l+gmCza&||q#w2D7$RaJKwhc{gil<{pxSGSDI3PgfYEr|%J^1|S_ z7(0GWybwH8c@jf*ktFLL{4)9Lapj#EH{W@DQTwtxKKyJa4vle0DY@A9{iA10hj4$? z*Nycg_?X+dk(Lp7#!L)LN$_b7LsgF){F|B9W4wa966+WxZQ5RLIp))rTR39E(o~C7 zd$3&uX*;D58(vl=M-M~5Bw4)D%VIc57_p0%0F-nmDV2up$;!X#pFjV^O)ax89!3;< zu|LztFcPaU+yWyetzrS;AQ6p!;4>X~$e7^Y_u{oK!`?U~!EHxz2*{Bt*Ik^cnPtPC z=#B?nEB(PE+^uUQr;K$5tp=5JswRCLRpnL(ZR&iKm z)+NbUhHXDNpIMy9+V%pET0PRNi9^+)aJ|K+MgCB@nMi9Mu)SpfqT4E7o`*ZEF7eOrOhlgWqXgT+OQ zcU$&JN&2TX>z=A=YI1MlSnG42pQf#!d=rXH{6>HK83>8fI5*i>I#t$HWVgkaC%;sPF5_q5}aaiU}&> z2BP0u38F<$uVm{-tDCW}IrIy!RYz5zCSjv8AajDa7~+2b9~h8ze7N4HwET%k3WAfb zxO(*!+W>HHhe{?>ugAU|lvYkHDQC)nWa%0ad65XsE$>ucu8u2~pq$>D2qm#q?9>eq zl$SKfF;rgu!Q8E}b)aec$OkgqvAo-THp>_I3yE%7-TW)Uuq+?xcZgkZGFHw_E3H8B z`q+$qUVX9p+Llh`MH&@#iVPtk4L9=u| zBrD_sCmb?9Nr?moCYVFUbMn#_oZzbakPyMAA$cACq4XWKf!T%BWpKo8mMtt%}GjDw=Ur z!GJGI8*$MI&>$zQorU46ukHH^5NkfD_+eH_+oFXO5R(egpHd-CU23&>Pel%W{{D!@ zgds!j|&)<4rNOZY7#D9!my&vhr5bi)+M@;TB7K!tq!#c zD|IjNv-STJ+8x;&Z>Trh5X6CUCbnjYh^Mf3Cj|G2vSi25#u(h8yYWCTxipPqG%KMp z0zcsjBm|G<@wBZUYv&k{e@;vZ>LE+$ueAcLPmMDd#O-iWfxiWoYZ33C4|MdQ@pRKO zFuYkOc2<0IXZq2hX(@k@UIKRc6 zx4EU6ihBsRV0x7kAe9&ZL&N=77TBE>P~?)l{)K3ioX*v_ zbj3!-8CmOkiiGi{=eX=0ww-O6TinFqjU&(BVzN(3NFq)hlp))JP?q;`r-#LtgdlE8 z-o#!q>Gih+@dqwVdf}eBo@Br^HZ}#Rq*VPb1&>yb z{*0vFSHAK%2$hdBXKZ)!0;9&A3wr^64L07qnj@s*xr6U<4`j%G2F&R!cENh7L4!fu zyy5Ztj)|Yk&tZ6#zmNqJ+1$fkr+IU#8g(HD_ahIO>}}a6LQH?%|I0`V694{rCy((9To|OT-+tMF&ZC&_?hTT8>@u@`jv`C>@7dzuaPzfZu^g!y}^FT|(%eP38)T6vonc zy-QS$jG5tSLUBvomvUY`rI?n#Si2fvmC9&b5xPvu3O?J0imG)mmNG)Af1f;`Ez-7_VN;pnVy12RHt?C7teEoFiwF%=R2-SQX;F zz%V9bflO&gUOGE~Es_{R6e~wx8TeH+&a#|7b*}pbZ$LLZG?V1x0R`!RCG4#f9s%M9 z+XL$;Ggc&gL$J-hRNlIyX4%-aSuDr;1N)}Nx}pD%Yh5uJQel_>`!YZN^tq9uRl?SL ziToNOx7xaVgk%u-!VQARqYHTJK+8e@`^_Fxk58v7$G~juLRV~wxQ*Ii zcMTPENQ$S5tcnjHOs)m&_e|jL(TTTq_{=6UNGstk0b_A*l5M=OX%Wjyw0VEwvUrMx zVI#=zT(79;>;ojm1rkWHD&?ZIne#l$Xw659YxGjzyX80Q!M;I6LEnHpaI0z0GNK}j z9vh~TUjP2>8>@+lBk$|NKXx0=@N(_Zr5}>Jl!b&op~b~q#BOxgPCws&PhbLa(vS*v zdn^y{7gwBk*drtgPx!B$tiWiW_el}`q|$i^nl@`2iEMWsv;zF!uFD;)>s;)rvehQR zUd`^_sDix4+Y>3HG>|iJ=<%~DO{l3Zs=%?g`-La}TYQgFLGsSD6kZhkk)M|y0C$y0VJdX z167pB(IWokQc019(-sJ;^nt`Z56ur3un>aeXP*%w;~I*kAYna6(-z+L~J-0d6&8m3Zqq zmFP)!L_B}wooS-9j1R8F;en8^u|3Wz6pf)(twb;No9hm{#ZOH92CDsi$3E-Q*v`bL zY4dK@V8w zxsz6XbVM(+6~%D zgi_H4ju(z;8-Z%AbkEW9mHE~G)Syg>T5`TGxAj-z$mdr7(DS-N^le|UUJ1)*G*D@? z{)D2-`#u!cr=2tha;p^bx6*vR;6Vx}#EmjjXitK#=KzGd75JZKsapy(q85xQT1*GZ zS^L4$DR(eaB_K#hEU%M>ANbGI3-TAQ{O{?rF`MvBhG)D@q4^&sYM)yE+cP`{!?TYq z>)(WgK;-!OrCv**;T@SygJ_C}rnDj_^M1X0ENFr%bVCX{QWrMqLfLtrOIeQ=+OJ8n zwBB5)D6(!G!#1A^J}aDC&PBkVyO<)NQ)T`HVdmafkgn#a;nNAX#CmTd{Du=P@3o`| zR2$|0G@mEq?iwYGXUSaJn&KC5f+xun)lr+1k)p9GZ=F}G7Ku#fY>9J!0Q%T>BQ!I9lzm~R8|2mUAB}p>`bcSU z*o;Yf*>II+P0;@FY5ca77k;B>yURg>dzX%KxtM@Ez8o*gu8cpGTL8D*@r~O!xwU)D zOOR-Zl4vL?$t*rhU6{oC66;On$JnO~LXvXaVOnQomkA+Wb2xfd&Vs$7xwBGO zIcyf8LB{u$3JerE%ao4Z-h5H-qZDc%c$FDeIDEOUQrWcQ_3+)7e!HNl%;VN-9O~Ad zbgotXIeZz!wZVrDo#S^*MClD*w0R&SaOqtkAW2gFyBUWjE@UvGLFVQH-?X&~9^L4c zJ7(gyQRkj7{K7(43a>B;I=HyV9pefDA>#>AFzD*ZZpj3g>#CoTL#DiA)_Ad=EgLb; z4?&awuBVbLN!$GHv}8Bu)E8`hDtou@|Ao>j@~p+Dqyi|jtwy%E*rPzE2A}E(laVts z$%s|8*RH_4>KkkW=kL5AP&D z44;X!;S8dvO#LOl!vO#%bxHqxf5bCzZ8SsL_)-1X1|jW`F+>J_WbL!uzNiqyz?laI zr0Lrn*3)0P2Ia4B1L-L>oOfWo)UiZP03?V&{ia_HL#oBMW?~Bs=-JP{o6D zACn{KXjM?ofEAk}yO7H1bW^?n`i6@%3 zucJQVA|Ym<)GBZ+{;QtIn1Qxw zuj*ELcPZpei1YEWbjGg7#t#OVAz34m1_??Oo`M4y0Z3kt)D_S3?B{DTUN6?1H=3X? zsyV1pf9iVgGa$4e;{wqEtbDr)8r(UZ?|s4d%|lQpULOt3lqhfkx8f;9NnvO`I=2A(@yrSzBBTRr z)NZ5-Lnka4kR;cD9i_%C7q#Fi{RK`dnvpch2(r4*FW*V~!344gyr4#yH}6aG*C~+n znHL>*M12njoenaJ@N*_&Q6ci#bE(|+kC3lOFL&tFRd&W;Lj-Bz%L&+^P$pD*j! ze_0Dc4tc@&Uz`ihbSY5z6>GeN&sx*>4P9}gs(499*%3ei$834-igU(N7BzvcbKsad$Yvi!DP3{ z^m!DWuep;dKD2nB${k4=n5?+KD|JRJi>65ZE;vZNg(gOEtEGz_x%2-fGiX4gDdB-? zkTU+Lp-=oN+Pw`I^Oc>DYaY4HG{x-qw}Q1LEksNnRO2hnNuD&fd{b9PLN;I`_@sHA zS~ogAll$Az6UWVX$WDpy7TPeE@*@=vn{uyMv*L>5{ zHP!*);w<>6$w#+iB&I0{l#d1Q2g>PYb?T*^IEmAMm;58*`X$7;jE8^!|7-*P*Dinl zr$6Db-y#^=0cmJl;)OBL^8>+Y#NIt^JKZL_+-< zOA}ge49=f5!Hv+py(tm0`- zwkpL-9-FFf{2$4leFnKH^ercim6rfG7UO-1UY|lm2BiDU-C<*%$P52X$eai*JGoyV zko5Yc4Qqh(T|H8|ISYM#XwpEy&v`53fB*h=CrIj0XdIy09=oR?e$XH9efSx)b=P#T z`~VfhjefrvQ~jx+(Fy1s_GoQ7BNtuTH9w{*HswypHmuDTnRB#;T9Sa4G~rdIrWCVA z7#FhXjfhNG#GfqP5j6nkEqR7vuJcTCX=ep3K0gnfi~c0dkIQU7;-0;z{C@7IvjN^u zKN&b;e}?0?{&}A}uEgPJ^#&L57D37U|J#Z5m=9HlOFTRhbWUC2=nJMH8CkDD4REuw z2Ae!RYI0WjhA%a>46mjR_Bwom?V;G{6Ij#2$jM`#eO70VN~;PMfB$02e>@QvAK;iI zh1`w)&N@htMPTe?-&@=~py!`TT{g96P1Gf>>cfmqsahz1Y18CF#AD{f%5~9p#`C)1 z@t#4&qBDBFv-9p+w+jRQ&9BQv8ldmS7ir#$5G!X*mi-KP#W7WtT#&mGI#-1e3^S94 zs2b(B0*ZL07qq2wC6qx`UHmFFHu-dQsHeaSnm|1 zKJU$lH!zR>b+lXY{!AN(88j^f#RU5Z#_Ln^a6XY&v-sG2**}-lRVDw($=l}gZhNU! zmd+LK89~dl0xZG$?>N%igy#3(B<<1*P0)GR&XvUmi=M=;eY7|Ps2|SJ0XLa|J>wlj z=`i54#mkB8{70ekw-1%ydoNlHq*;nteSNP#$Y(RKo)VQj#`(WBNr~6fr@rq@2ipfR z0rN=rmXP_OWWVySS`r~cx4czG_7f*EPm>o@mPEzNKe(nyz(?Ps$O zos_M+tmXEYN-%=CsF&uj`{-X55d(0-8a7#T$@(~CfY4rTkY+~C`xi|pbPF;6tZyP^ z8q#HTinXe{C=;Ed#tEVqoc)c-4t!glEXrHhZ&idNjy@b(0K&Yr93B!;jt@}_|6@zax`(cqL?bLfdnv2x{xA|41*6UP+xFSL+uI=4rIGC)DRC$CI->r?je%=R-lI@4?=*MGkf z=DWE)xidPPz;12O`bJvQ_c2my#^7oJy>IhS+8*W!O~cvfR3(Dck*zx6e>j1l?>x%W zDX?OjZY(@DF7%V=mi*d}N$GV&RyV35E zNN>ybmOPKDO4jQ5Rr`e4cl{W5sV#RdPev^rDhUE9A9*8xr@5&@qcDbrJC~8|i*xlV zABJ^QPw7?M5m!}c0d^Nf9+-r1N}ZTgsBT$Qb(-%6uyy%Y5>og^Cc9S=vRo18e;}mUoxv1V( z)(W?mYYnqIMAVy;O-Orh_`5tmGR6vEy5Tz4TnLU97*Vg zwA~ASB;L2NiNlFx0u6Os;9frb=Lm_9Kq;~RBZd$MmvIf5w$%v6O230#&Q!1)bIhnw zOAOv_&c?f`GBfmawZg3V|7$EYzk**Snv6)tXdUB~8C_5q7XAe@6D{Z^HJ1Kkr=n25 z;3KWLyS6JK_6)&djOPL!uZ!j{Qw`R&h(Jz5?FWq=kks*6EH`XhtYov5)yak{{5fG0 z2%Vioi&zxsWX!6%16R#$3^K|3tTMhRb<;ki%U^Z3`@93FU;n2C>rt&OZHXdC~pKSmomfZx|S zInJKYg-^|;l=sd`d{~@8D!v&zJ2zs9bj>uHLBo+X;1yf&ru*BggZlg5S4@r;9ceD- zl-REvS*gh+y>DNgCjLHySHHed*%l18&WDpf&~aHCr^xLWK@=2Bq`ML%tl(S+$4e8 z=Fd`gqhTP^{s; zBI;?zLRLVf{V)vvx(>_fW#guPRWMf8-4d4w~xrLYCz1b>B}Tef?w+ z%`xHo{isV)nI*VaX@*X;p&`;e5(0LR|NadI_j9lAuI)W1e`vQ!DA|nB0YMcrh2IZK zn#c17xqq;i{z%;qTYsXECknokJSL=8iF36Wr$QB867t(^+0aHTRPv`rSA9?c4p77g zA;s9Cx@+VW^cUcKM=Mil;Zpro`XAs|CV7-Y(@x78{ND3r^&_vvDaQ~1dI z1!32XD-ab_4G+A4XF{=-wmHc(ysNki&3?p5REMexe{^0_MicRCTg}WXszDD`!FV4F zw^B$LhxjP7<2)49633zx;F<)fQT2>&Ii_4vqVd&tu7DOu`&_zrj`^%}K?j&O=7GNh zBh+BC(CP7InWETE=b}bN+F^+l$nm5U81&)?a}4z<@?VeQSWUR>`ILVh#YKUy!g%@E zU0?sgJrH;&U+u)*A!1>F)M-hMXV39YaDAqAUm82qrfuq2wp>N7K`6CR zbe6Cis>joZq8*XWn>rs3)pgceM%aw&23{j62GK^y396(>v>moDX6Kfy9>g8^id9&JU*L4RBoB#ZIBW2Qt=q&Jzk6GhuV~>~bN-!te+6`j zja3BQA58+9@#WiEs650i1}e%JhE*xmBk?;pOdlHEZy+IV>mK5Fq|84fbU>(@vY40~ z1Q2x2I}DHM#}NkO>v11Y?M-h-&N`)!ET0^fP&LGefQFjQ5w>jL({PVM$r#cGtTlb2 zSim8cJDidh^ zVP+>#T#G$3qCu`}s?Lq#50m+PAb&p=C{;rpIOB*(c1uh*Y=o9)3A~P1enY?G^@51D7 zx&05)({~@&5drE*wKL%>IKbDH?fFu!4@Gl{IzjgEPUp+&Sk1;31s{MVxNd+G4DTo! zA^o$t@!k98toGngPC1e60_!1NcReRi;yj(AAwCzOAX z6^%NB^s=_kf=LHoJw_zCQoW1&wi?9SL4*8>7BEPTsN z@pO+GzOJ@nQbnqrw0UmeZ?^a;QY*en}A3C~dkom#h1gjMa@FVi~C z#^z9zqW!5Zi?`dQci%oO$CcQ(b8!`YT>`xSq1gsKRo3)z=&$QDQX58y{amX1J+ro< zA-s^Z$_z#(Vum#X){S|!P z%0Qccu?*)}L~Cu6Qj!stlt3t0pO<_TxfU4=M4<<7J|}*;w)h%K!^xxx*$;S9W${HP5N|OVumf4W@DV-=J&^EsmokEYs0Cu?OCRF&fy9&>?Jgt`ZM0= zr3=cZWNV2g+-%MNC@z#0L-pM~-(u4;UOks?5bWs}&vh1~H0_;zBQpB(L;O%dPVw{X z4p9d0!g-iIc+g~jJqb~$Ni95K zyUwh5n0ny-QVWxjoj`j9p#YXh;D)zoE&tj$_lvt1Z)?POaoyS{W! zBtdy!L!dJfN|lA8odBx ztvgO5DRG{)b8gM=U!4|jzR%@v5BOi1c?bsozS+nvlKrxC;&|io4hhGK1(nZJ40|^E zx_YK`3NT^7C)Jmnw+gI&=EFc9(_VDQVBjj%*^d+AELPK37pe zGZ=K&yWmOE@k}9;W7=4SRh343ozhr6V%6^DUQJxV(@hLzV;AS_k-X5e`kTP$F^}8Y zb0A0k5j`=pzUQ*Y6p*jx(U)Q6EiJq@m#MM(LGsjx1syB8#nZt0^Msl8D)Oo!oIT_w zG^9MJ*@8o91yHzIhag_6yj%#U;9S(7o!2g4rsgxq#K$d2TNyrxs)X;^7rG48kO!jS zhpLqLnyf$n@nfTEjLCuv-s`qOX*MEc3sRMth7rEzIY(HuY{C(JVi@?_CWTgswT*4P zd!Cj}eOwo_`b*NF5vsg9pJ6*%61n{}9AMBsnchcXQr{fmlPqh|S9rPd`Y$TazlgK8uUUdj zNHX_9Mk_nSecV_jZ5H4mi=Y|xbdO6Pzv!)e&_VQn$Q~rXXS&VgB7wK&Aw@A5Z>q5OU$M zb{JomiPP~r(3+g^8(pmd#r^5xpEBVH)ej%K1*Ic08CY|mo4k_f^32VIb>5YT`@C20 ze->w9zU)bo&Nea=dHh~~2kdLS2bOz}0dl7A z#E($fKf4`1J%8)d8CyR!+h5kEWGm0v z?WwIs{@46YN$@WSDd`mvp;f2&p`;2^p0x=uX~yVGisxCdM-4x9DOq4?z))gt_>cJQ zz?kl8Va<)XuLrQdIz9rgO(rw(G9xZEa&}L6|3FH%sf`gu2L0UA635;Vp>7rv2g$)) zZ&1RSj8bKvb|~~PKSv}K?uq^K`42-Lm3sRs)VF=fojbkrB|wRpdX=Mr%yjD#dtA~k zR5#}Z3C(`5P&vtTCy7Xk@?y_R`OWkWY-m3m?+#PnJJ8g`nZ=&?QO?;twh@&&%Q zfOKZ85|>!;p|=kjPs1$iA3^B(-}VcO_^BU!v-U%4*TKxAboZ&LXg^L=^)WC4bz$d= zH5CyD;)%?UpyEl^QS4j#X_B)#*hhd3K;$QfhoTE5nr6}id<9EXlyDDz1d~TEL8|_A zY~H`1WMiAOTaBrG!%-@+?fFt~(WWF~YM6DW&e%;+F&std_P~1Qbz(m6p z1N#8tT=DDid{FAmlk(Db^Ls9G@2B-eoU8weE6IJiHEhhq zM0oSL&gDO^_&qYaych%g;@Wvka%lmT9O=&DIB!5@& zPrYvOkN^ID#=ULf&hLaokI;S3|L9MA!bR8VGSp1*>($m%&}r$;l&?MzVE)u7uyCAF z{;oIi?{qja2Zll=P*tDyvJ`SC`npjMy^s1?^Q#DCAuU+8`Zuuc(Vzv@L>zX_T!G+B zpQlxg%l*);N&JaKDNxU{8~_5{o19uCsg(FDpr%V%$!tzEKUE?a0*e4px~Q62z7+IO zrqtWJ9KJKo&E2UhKlTWt7UyO(C{a;5ao#%LS_OEhF34!745Y5X#7X|qrzW+;E`C>gF z9gB4CC1XnUyArd#;)dvn0LpSQ`?<=`V%XO%jm}Xi(wL9X{6v@y;l5{c(8BUEF2^a46^|kn3#7N;O!- zU}XYAL)s;KFwrx_=y~yQe`OR(Vbgrynvr~;|I}HgD0>+yMRQQp!~gn) zVL$p-C**qt-QMU|_(c9O#P@0*@|Z=}3L!OOe-jE}$6V{; z41ZUwyN}C0bqm(akcjuTi>&MEI~1S*ZYIxO`S^>bb6Okp4(>*V9X(m~9+2mlo8;U)B`c#gt7#FE)5GPnk_op|Qsqh%= z!|)pILi4%tBr#}-CIUSv1q5Y%ninz&`1lA81~y^$5oWmUr?i>ax=I)Ur_JjK-nCyY`3 zke9RWpE@RI(6aI}#x1hoA#`qr)dB@9_{Q`4^jGen(j`98v9H|&<%(oz7b)P3Wq!3Q zNbesZ1^3iPAH{$#iJozq!|}C#5}b1xOGjiCtO{T}8f*V(xqqha<8_5}kM2}6WZkCd zpLm=-uR?wV?5rCWk$V#F6j?^TeKjljW=hd1|L}k&oAW2qBXr~8|JmsZ*MAUil{c2f z?{TX8O#)hevDywS6pplz7C?ETe{bAsV}a?}>eoeOD>hCa*Y{xF>enk$ACXkEPCGA` zPdBzki{b&nrL)jLkk7^asrK}6A#jR&;JFH}=Fch0q;YUSR&Q|tuw&W9HGAnl`1}=o zIU))@QWHQX$hdGGXIhqr2a{$cwbE=ASVk{~ue(_`klAB;iheX_nZ(7H5QE?ytB_{IrMngJK$>$adM(qJ_H?_1;( z8Y9vtnu;O2huAyGHZEL^s?7PC(%iu#JFe8Yn^aj(VILHo$YG>{W|Xc&=%8cPC-KIPM@=zwxZ!Yw@dk*1HL^$ zDD?7$G+kRV+Ck38+aY|@T{JJL<#s^or-flvg6_KJlod9-8FUn8dF=P(h4gSWG$=`6 zp&Sx#=iARQpI%NaPiGi>Es6KHn%Y0$j6n5`51WZY4SeI`*Q@Fl13ZnnokQ} zBx3A$5_DSZ;Wi1wqyBT%_gVk`(%P5=7K6(tp+Z2>WVr)&ali!m7}{`&F$o_Z-MW`} zPrOYEpyQaIwBm8>f6=NTohjT;**k8%vhe2}l+#T}S=x&>j)iI?W!isSi1xCAaRywQ zReddY7hF)O+xRlGtt`K2L@T!?P4>VFeu#=*H&Lx8IxWjn_@;)I%nhZp{rnw8Xdcf@ zC09bDHc*ZE2Fyv^4$B!QSKw#T;IEHXU4=V0>KF>ye_V)Q%P+4^&^{6%L z=UpIDn3WRI`;0b78dnLksU5pLiY%)6Q+yp7Yp1A(R0<#oXpjH9XH)CVRoqG`EQieo zg}tPxbJ-h2&pL4%fMSLV+(B@y=fhL&nPiz6i2>@V9;V}Eox?6Q4ww|!%H)L?Q^jrYW){>YTtAf@z;MA*z<^%ZFT1U zX;HhU>Xa1LBramXk0^ZuL7@62lI4hjz2@eu-OKGiN|=6Chnj0P&13-aYLK3y;Jk4* zGvJ3MA+7`V*$Y)jzI#64{J<^P5H{Q*jpBcu)EJGL5C7tbG?;gL_^-v*x@=)JwIM>Q1;3JiUCnNjA z&S~mOx;20O_fdAYTC6AU`pwvH^NXrepsU@dBYr`V4TxlQ^CI3anpJ0=^O1y*ethbX z!n@aGtL7{B91T*#tT5m8%SId%jG#*U)=>!q;%MoX5en|l%anISM-JapCbSK+O8jCC z1an}f>n_e3XZU}Q^woV8shtfZe5gOc{{nY@W$ETZ4$6)a+KAV}{Rl-g*qS#-;*t^)cO}DNz5V z5mG>Wrd1(*I0%2yc|scR&IvvAwd9V!$7t5|EXK-^5l8%bf5WGM-)8BQo}(E6 zKB1avCu4fjsz;R42=(;KQMnx?E-~$6Sle@$JaQeGR!cife_>iQv>P3>L^8q6fqk8o zz2N@IGNxuj1MH~Ghyf&=S!!U1)sD#&m?F|gAXa@;5PtI!pS-K$z$rT-btaKNFaM>Ye#3ptp{?%O8na_;3tO(*xvFjFKeq~Cxf6Ir{oV%OqBE5x8E9eVs(-*C zjnxa^Ag<#Wd!gA*KVmdvOR!Bqu{~<(ri#m5?U$>M#g_d3jRQr8+U#nf1Z4#b-dx#; zya!508jkMGvxg#Z)(gY)9GRG$C^?%rK+Ty(@ z+nFBq$>IxZdI?Lb$(1_bfZ0&-3G{@onCpiBTc5xGQd0!l?5Z_M8h^A}5*BV1i@?h( z2_NjQgAU$JU@-k^5|TMah8|=$b=*OE{t%j41f>Y2YF=Ymtaplfvvn7l_r9c%- zLF(b}2T69)>k^cR?{W++A1UdMf4?0+51J%}M$w~JE=zm%Lw7$N)@wVs+d>fDiUNd8wenw|+2g z@@2Nvw}^JtRF1?LY~=#{WuT2gxE%2o0w9zORGO1egZXK*REA+)@{=Mxa zkQt>S&qin~&2l+E|6-9kIsaQZ12t*N*47dz!tGJ&t!=Ff=}UciwrqB=K^)k8x>`Pq z&%!7^F%NrmITG*F$ek4<0X-nLKmo(c*d3|ua<&_G@G!@-JP{t=a3^O6(uABbGmxo& zn}#9v!f_MtF@*;1O9haT_DNEsxq;~0_K9h=z!v8CMaWnC++%87DwlN%P43qz{K7ZS zkZENb0cqY492Ua4l-TW=Dtn=)Gqu%m{Ase#jq6uHO+zN%b+Yt;6KPK|PQ5NJpoVcd zv$Ibhn}Y|t^c-`Zv*y^=&w{`GRT6rSrfR^9OdtRM+r#|+`hmMG%u z&(tpi_}J!HHdl6H{lHn*ZM2AIu=d?!N!pNa1c`zePPPz@+KOT^8rMx3Phk&ug!!@orNxAO6+Dv(R}4 zm(~5KpUyzt={2>(jQt+Ahe7+PVZ9 zL-9TOwJeSenP?7n)X{{G`OlojHDo8-8*x}|atbu1BdS|w!1xJZ3ZBML3UdF}S0O=m z4=GLeMDYDD-Zsla`FVOxy1O!*$y9G@JI$*94@veG$=cgSEsNR$2fh!%(P9tF(?O}G z9`XS9L$YY(qEX(X{5KaoP+>8RQ--6_70p6NLqmz~>o@^5k-{tiOJRyEoe zyy1U!FQ*C5c%2receq=qe(IY|31U_}AL<1>Nl2)~q3_jw2c?Ie=rO4Q`w@uPSzOJ< z>K?{J=Uiqcc#tG}U*N&$}wpDn~AS;b^bmE%+HU^(Oam6aHKq1dgZhSZc}BWWJT z(jnA?aZov59`Ojedbf6nVZv9E8S>Iy#If7+5YJ@@Xb#QhExC72!IPAKvJxkR0vw)M zv3|r{Aw9@-0e8vKfAccf|G*dj`bqQTUy~t|@MUbO&;2BFh@&WaDkUq_5wvHD{)kCQ z8biiZ_i>Y6mZ8USIw(Cz1&DXEP7$fxR9epG4ilx60_0Qd4aZg9_j}`QsW*03H8qV; zoS=L$29{AXiP!r01s@6*L)zJu?6J4FKtW(fO-#u?@&3{1-OQJO&C$;t3j<)={B})o zT-4I1QDk~Vs6q+jMP}1M$r<}K>ny0uh;n6~XCm4*Vu+kG5C+5$o0 zvV1-$p(UD~2%9NcJq=decE$et?4{^dVB_GY4&e5?Gv zf#LJ2jdBf&r^+>fLZ1$#5s6(SYmq1K{fF2f(+1wN>NU3lS@9WC3^PcBi)ugtgop)q zV(C(>pb9SN)aR3@Z#_jLLwBCVMSGkiLzgvI8Smo|e`02lVNXB7)LUmIiixut_T^EU zdYQDbBBLqDAG{%l6t7H3aLVuMnO=du2AWtz{Z_DIEkj4 zY`XTmLZ@as3u6{*b#4X1*MCtK|rAJ3*K=xI8Bv9?#-|oM!$?rS3<}oEuWCJ;91Kn%+j-?kxTsU4gdQLUqw^? z$THv!vHPq>YU)iQv>1&}^NE7L*GTDCt8OAIKR#0E5oKVNp;91gh3!{3IEIH010as_ zK--VI^ly|U#V%vPR>D=*424rxAcG<4Im1Jt+l#!P!KqmYl~GXqon_9@{r=}CRKD^5 zO!`?)X=+s<{tesWPn_ZXD=X@U8W8kq&UoRPjH{BAUC=TMxxE0}X9M$cz0=+sdw-VM zYeD|lrk|2~-Dkt3nsnGc-vI(!G`IVBNld4*H^TuUxNh3ny5fV*L3{qfBJ$A0CjuDM zD=e^G`MwoVJ&OleA5a@C!h0}nuj( z4IV$C3w6it^#R2VVf%;F2GP1gT#XmmmGrbHhFI9{GzULywS*(Q%i1^xx*e0vRWFuV zf;*eK*-zbQ0-Q!Or`-0#{!rRq{?P5)tW8ZqjSy?-S7{Yi%}xF|OC&!(9h*)! zyl-@Wvtk^)irzchFE0G1hLEc%xZkncfL|^C zg~oOJs$1kzE7{LlVQwiD&f?l7%lI4om6(k$6byJb$4G^ThNzTJJ9mS2tf)t2Ham6r zz4}|zZoHpJjo>(H9R5Dg*$(1<{uCqcA;*+TF-&!fWRA}b!7=%r8i0ln9o!!Z1}(N{{{pDGfVk~@ zh1)FbrkYOLE&#!a6{8I39~IO;;M}rXRDo*|XFt+B7|D#k|DqxP{97a$l0)p5x)#}` zbQV104M2jx+Vvc;w)rnl>VbQ&ariPj>kSTaaqSU09V63vh111F=-Kz*doN8X)8{!h zo@yly8DxxGtcCdHHjBycDXWm&n@5F2Bn_zht)Q3IQ(l}C?`AMp&3%CK`uYFzGTPhf z|4>80tz0++y9Urg`MQ1b#Zy0k91*6@&DK2*ZjM|mh9;tby^j}`!PIYER0sNGK#piIH@74oiq@27rH9hmfdEJjNe=nCzyP2CuK`TdS}@(+^2!B~PpyRdZYaXHoTpc5hdPMq}Qy#CF&jj1HKV3Ee549O9tWM#mm<@1oG{dFC0WvC+{(&rq0yfV+~7} zw7*xWNao;Utc-_xiI%<45Ey^}{`HS}m8IRqB}l`{=_QB*F^eP67|4P#^0H9znsg%Q z$YGb73mO1C?u&{#GX^@8yG@xaa#}v)ifbUbWeyQ8izPOosz9xfW@HPD4^cJ8vHZIS zLhOP*2b@4@!f3&p7JJ2sW}G%A3(e911AxUSEiboP05Z+jI0q7~w4<@P~59U!Uy)So>9WANddNfiJMe$fdZ zdMtdK4|y&RX=CjeZfm(z{7n1-oY7`c`9uJbO26h?M)8Bds}aNs$kexkdYH1PLwcGKjO|J$Dszv{A_PI?0=c z&oU#~8@jSQk46G40>qwLfcr~E!6#OY1YNEWI zTINPzC5XpgAs{J;;a&~SSSh(nPf>z?fH|*3A~WzT{B6Po>=XU1(7;$)ifsczK$#eJ zv1YE~ud23dx5d?{sD+J|r`0ekNs)wBTWlLnumkhtn?Upr@<2Jq9AU6BoYYZKkmq9S zaYEivbpl$COM_Je+QucO;Ap@wzC1nu@ze&00!vYRKB=) z0*$gMv-TVR+o~z?m*DVwN<$F5WgehQTteCd3L%G38|&v#ewQ z*jFMw#dpR_=x_X)+Xv{^8-<4D@p9u_a{a(5LoywmR?hTVKzclbOuanC{=OI9+vXaI z*cnA20?(lD21+>qm9g8!!6k-B73Qt8sD&)BHvVFavITy(<^bd6HJ1kbj=qJF^@%JG z=O!~Dtamm+7q;UQN3(paEUhm`yk1_I<}I-F0LL~SVKdSp#wa3&^&ks0PB0-i6OwW* zF&6>hQM@|Q0L&O0!1h&@{AV)h=zm10F$0k=y`X1;>RS%|hfkPm>_eCXKxRk0ol2nS8&p`rl=R9L%01>X@xyIWtlLT zrCW=Kzy^03)BsmNsK1p&fdmHtn|cAnh4lpLI%W%nYJiNJ5)7c^))=Dac0w<32h26C zLcl=X07*u&eu3^>KuROSyV&Vi4vY;w001-qgujsC*U$5wz(n*4?>X1-ct9;diUAh} zV(3z0f$tuX3fKGqbA&~|QvDB3nNS7f#4fL%qkTx~_$r5pIAeFn3wXaAM=j-*()R$Uk!5#3)rZw25ccUH z6Zn&xEV8btSFA}mZ7Fs(ga;xTEedWjgfm1dAsxEdjr)Bc8I+Y$qyT?q+oXywrSDui zW-a+i%K(pW1mNY!u-GCdVlb-V0_bEMMyE!BxuFdR2nZClz45%EgzvSsY&cP4ijm!Y35XC_)E8Mf&WD+b$bhAcXnhe)Bk3mD#C;>g zOOYT%Ec#W<(P8f}ZJ2-vGoMrrgA40G#3hwD#QZHF=Pra=UcDI*5Wo+&<6skmW~rgL#fm$u>mO)ZFsV&{D0LkZD1D0!+!@rPIhB=V5g89 zS-c4**uEX}P|*Tl!G^$4g@7#$ZDfjI&@*H-Sg6=k6|s>FYV=!=2)xO<88DqNAPUwer*ssuXS+{e+^)oK4}Frpw{3}-}muZ5w;}B z)<3z;8>Or3hOr}5UZfGiqluLe$=+?Z!ANq!IG+=@9ldK~3clnaa#w)NZ+h+-29(pm^pnd8H zq$nDsXfnI+8c#{RRYu(v$K1%wPX05|J&&C5rJIVMD4u`V=mFmt8sh>^ZecRt+8Zq( z9>QJHyRa6=g2O8tjl$bvX^22{2OC&~e&y~$;;=@cq$D6C0Kun0HQ{|0PUm!Kz=pJH zAvz;4Q%y9%N5&vWfBI<5eq;bhqGw>}%!p6Hb&>BN_aiE{**#utnWk=_(sC3~%FMX?N0>4>tjLiKT8w?W#I zTT?Hl+XG@{8$k|_C`6;wdF$p0-vOQN$_%972)!-LSv*0HP8>rbXyg9RT5A|v zFyXgbc9@EX<+S%LUabbWLg;P(C45rp^I*n(XO)a0h~SDzj``lD2+P66Q%*!3;Q@fD z(Y|5So-{zaanF+r>Oj&%OpXEWY?ip`Syra*zFOpbk4!JH@RUVusszJg4!V?(`9t30 z-R0YkN~kA={OLV(0MBR909?HNO%M( z#n7zWGGO6T8UXlEv`jP`_RrV(KPS?4C>ARB(=rG^tA)V3VVEG`qnEJjm>~72~Eqge-I%; zNV*vjUNx!&u4t&xD4kZcJ!cB)!KrtSVh&?Y%Ns;|WW+;ocoj0~hy`8XKSsKy)UZJ&!q~lZ z3;PkD;4(&cN|Vv;L;8IwVTnjX#|6Gp6k1?j0+I8W^5h?(8CGTBmJw1EVXRu(7Kr~q zofnoon>gz8iOaah)k3J?wt1>v78hGZyC_)A9fm&-^DF5BL8sDq33H*-zjw7 z6ke&`@_|{>Kg?)hUp0gim@>eN$Fn)l+B1EnpG%Y{{)TfE)F7-*&eAU5BWskAmabmuqrhHxMLm!-VbSuIzM9zxY+pc49VJQlVd!QJ5E_ybAWPw z?*3Wi2H&Xk^a79oJr%RSR*f}|DWY2z04Im0L4P+Fg?N-D5oh#{N?^qh4KiARRVSr-%J!litQ!4QvC47BSWn+S;f=!2k0-Jy zr>%1_+085IrpaSg1^(G#&=c#3pQS}4r})J{3qoJ|l4}E^`5zA1CZ(zY0Az1HJCe|- zXUT{(DeN5&?gd}{(=xy5=$}dKyZ-@XTx^0s#vn}Nosj^aqeH`~>*kX6Q1Xn;QTpG9 zj<3{$CX+T;qNSkr>8@-Y%efShT6(N|rS1OAt)z4+mC{(B5bCnc(zKglmOi~L>np0j z0W5O(%J*kqHMEH4cCcj>V6QSV$L+dtPMKZYj7@@YN9Nye1=JCPSzLv%fYQ}fg$7HR zv)T5;Q}ZoyU_V4Zg2Q?8j`gVM>EEl6+YWU)9nuD7XQ*z9@V4w zBm9AG@OIXa*=2C3V<^Cz*JpJ_Q&NhlQ5X$dz!Jx|h!L_f6~(K+xyJ@r4>_JWfTNzd z5B%AXsp=N%GGegH&CsYzT`7$RSivaeqPYgKt`AY3%?Gs#F_6EOLSTYN9d;!?M+!SeM`S=Y1}+jFJ{eFq4@c++D3(ur15hFeoD}?= zu9W*~wtRY#M4f;*Knd7(3C0&g@4=LoDzK?5*OJ_sG)Tl`!#1cs9$(N(+QlOb$ zin=l{c-j~LfO<2a1rcR}?6zA_-9Iv7AcK+bLh}QZ(zG7T^~398PP?o$iwVqi>>Qwl-B;BpO3dp_X>pFNy5|`|*1lL>BO_J=KnhuUc(_%v?!cpE&5p;FjTw0@(1c%hSzrK23O&EWLLM^1s=-BvpEfILXZ?N& z+qtB(N+vMch+^W5mfg{N0XE$4aOO*A{UlR$SiX-0e;CHU4?5yZ>e1?LPyKap6ivru zVXGJ?#i@L%2M8M%lsifFjT2yhW zGpy2Oqgp`HiLziao>-I2q$fiTn(A~b6EKygBw6)n^FAz0!v8M@LbhbevUNuU3 zH=th?@%J~@xxqf4ARn?{vl~;|`O0KaatpQugT2bXX3sB|fSEq7qb{`+>OGAE8h7;` z-bX$8I4=oZDjbQ3cusvs%w7F!x>68BdM2~q^KXH6yo)=O#!j%o&N&OW8XtfA` z1Fn_v3TQQAPM!cFz@5^;Ax%vcrEQ+id?#k`%t1mm=m!jugdude)rrBHtUh_}?cY#) zsF%XQfG3SWpf(|^EeJH}REDolRB1d$TEWBp1-fgeUUW6G2Y9u1hfJUZ27e>#rr9qn ziKDu>28mbjiJyQ6&H((ftgxbWklb(yC*q8lkM|B}j|91Qv5yhk`>?y@8 zAn!0H;oTNqdE2U&1K`fAMn3la409Jlc!2d2k`_jDCsvzAi5KU9 zS4lX`3)a8-2F5*VMA?UA_jqFVJQ=iPQKZ2R=*toN-OgWip43wwY5mM_8XUs9TKW1i zcA7vKSOYgI2W`VjKHpKx#O&Ut4Leq zesKVHJr>_ zAK}5A6=xMRh(S22QZgYV;YgAI8};Wu^^0xwWeKGF{}G!5lf-!{ubPolqRLVDCfnrd zgOWN&EALOm{F67ez_4j(i|Xs)V9M$Rp>p6ItZUe^sPoIcUA~FVNV|`_GA)B?fq~=C zGkK5e=&^=yCA8*%y&*69%1U+-s`I{{|I1WSp4w}UTyTjs;vnFN)fOd zB|kUycr@uXOpX8;Rit!*0%4z9)xGbrda(PAL=4b`0-iy_xudWk*rt!|i#_#QOum1^ zxgQC+hA$MeF=x4*4Mr(}&y>j1C%vnJ`2GUYwsOB)0WnY3C?J;8d5j*RW56r`0SYU9a@3^yttROi-;xi%4BJ(BER z?LZv`TihBTMr>iGCAqF-kH6p)B zo0X^*4@lL-?H(uG4x^Y(e6Ne^e|_YA zJCX1R!Ch0JA|@b`sEFhKs8okqJ~htRXje(zou6r0ajgN-c%DG5v>}@Za#fVGk3{+2 zWU143h`RiUU@{*{MhYT{;MHZN(42KCE*r#JmC{oj5=C2TwAx7HAcG z={S1{uBNQ(Wlpg)paY}r=P5mWu{c<3LbI(T3sess{P5KL*NC)dZvpl}=3dQPT99EV zT4DIpGNL*XpvP{}g6)#sKTsA~cA8K@_L+p9%NU)S!c@`oKh$5w|3{$&$NNG8^-@(c z?bWU+F?`9wx{_c>g@-^0WI$*7LF$>%Ie05{aToHQzHP;Y*_O8=<0PgWAYL@#+vlcB zCguE#-|Zpw9n9I_;u}mk7RWKW!cQ_U<8T=eod-uHu>b%|_LUdoKniZpo<}BPDhFgx zb{=p!3cbDty?vgQud(Dpl8d0|U4;UkF}c^s++cCl6`Fu55y{f1 z_8IC%b*c19$Z#1uB!ZKV*vA{I`vk@n4qoRX5m35k22$>#)!L;i?l6%<39KQh0NqGb z$`KnimD%sh@Yz?&fILeB4cbo~hr`&@JyMFWJX#{N|B7=V$cR|Ew`q)~1u{70;|J!J z^U*>q$CkpI0=8M(^(opWMj}h~e>lr&Z$btb0pF@viL?prlBM!YEhf!m&bMG-M$9A@o=|rY6 zHyd5GEtrYKcwSb^!^H64U-5>l1pzy_gw=fv0IfOJ5` zqus6bz}ZYrR#a)!Fv=-@%c|Z6#4f^Bbj>s z6^Z=hH_kB5aT*Ds_UK*(|FH<8g=W(l04m_N+@M?FV@S!RH^Sv*ZnR*}Hi;F4vjy9# zw|r5%qf+ELD2Zt!Aqn;Sv8U6n;pNCAXnTi$_?)l?$i5$p04y5sf%lyO#t=}(YKG$N zC(Gm}<@|R|d8@hr?jo7?Zu|N}K&xLP1~*Ab;U0`hprMl{rrm%7+?$k~{P9=_mw;)u znNU@|1d3Dq^4b2QjgN-d17Nb!WyokO6NeCcW-OUYN4qC>-&^msQ&Ib>#}G%$d&ZaDMek^YAJora4`O_%@?|HZ z%CR`*&}sq}x@Pj7OCsMBJNv!%au)P-{V3Cw=S@H=1O#sCx-RT z&icDCrwLXYFYUk`sCC!GZE8ZKayt`VkLt4}h@xYki+B+iJz z@HAC(i^5Jm6qDQ>H$qI)X|B98RDBB#VS66NU|sSxSfJ%vxC!7&P2PKmjC-ek#7QAJc4&p8#_y zSM#3mrQ&Cc`M>z35bI#RdWnk?xP@2sz7Ajj!X8H2QPZ-RqW~_XTiypK zf+TQTBmTkh9N6A&WAw1y zx`-{`5B`?GRiO5!fJC_W~sg-F{SCoP@l5B8^n5(+%_>0ff%y|K~d0xa@6Gr{t!AoCIz6boNWS$Ai z2DClQ7qBBV{IXwivZ(PBg?@K>RLW+K`CW;->8fQmtw9Eb!TZ5}Z%u?1@}e<)X4+)e zXc?Eo^0fG&ZOfGMokelW%)Wm3KF zSL(+ROt|pe%v@3go2PQ^h7!<#=@XgI5>i6;X=UMW`K3{gV9#3_I+A7^qTp z_d}@xgEp<~_Uar2sijBiRdDi+c*UDG0!ntd?Mt9}mk%bM{P_3>x^gHM4e@3iwAT}&;(_?_E$OtJbL>;ex@ zxGRU4o44#d_?~~BB+BsBeQ^D;JnW%v<&O2p8q*9`M>27Y?5fZ=40?|EW^JqsNOFzQ z3`)nFE;!>PK+7Stv4+gC{JlNAF7USCGdZyJ@5tIC0*NThh~K9U0{4#~(q0bK@{ICw zl-?l=0gMbJbD8wSIezW;5r?2D2NK&oo7 zwkcfS70ZC{6cb7{h0n*qp8nHxSkjuQ-s8odtQnU%uz#;o%NvGpfP4TbHNn0HtrhS# zs#J}gpr#W#7Iy&Q{W5QuxHH_V#ra#Jj^W+d@~M2E;I13iPZGKO)PiE4Dpd^(6qXs( z!k!COEfX-JrmoSuDNa($da!a%2RAg$O7J#0RMC4wCQ#)UBn>$FzLRMRvdWWli3(lz zUtRAmT}-3PE$&>F98-Fys(l3aU)PNmh!2RTbYY->kS^~z70U}=GQ=s;WpNQnffp31 z7&Nx}*WguT2F}rNI~x}#mc1Vw7mpDpogKX1$9vh1@sOz|dhX2BGEPy`(bM@?vjT)7 zZ6>7q~v4EFtb%5;>_8=`V8PJdZgqqScF~Hn~nTWrfeBl_P8Bvs- zUfaEC%Y{@tj0smDq7XbKG~w*69M;V>RR?!XQRr`!?u>e>uX2?kIC~up7PG_t;4onh zXA_t20YvN9P(?YcR2^7@e2&L$;d+#*p5hiQ_c0H2Ty->)-W@WFWJD*#4g>oRB+DwsH*Hj8Ovt^W#7MqKL!4Dbt28fV78r+XdleoS7`AKu}cZulmrN z4QC4shuDX!cBA}LZvV;@8>MOt;=`uB#l4sv)=mFk!O*J>#QF=|lY6&i+CZ@>E;>+a z$oh4Ccb7V~w59i*hU2nwm&|!Mw8yf{=q6rjS!;04iSJO z7zclZlBxbWta$JK%5W?eFq0IH+jfa$b!Z0@VP4m8lS@IR0I=z!%9o^VtAfVjOWBid z^;R;;wIM0uLIE*CWZ*~hUagDUCH5eNCK^ySNajphN~Gi&^mYnbxLC7%jGij?KK z0N46EPg3^P^Sa~TdCB9?$t5Bg$gY*!ERN%wbZB5G3yD}*=6M`eCxx6vQ`rZrWN1l? zu?^L4oPSEqZ2lQ39L-`eu3r5~!w#twi{)j5?T(ND1ei7cU`5nXLuXV0{9aEY!%E;Q z)&MhrFC3#U80G~_a3lk>#1s5C!IPg)oDPuou*uzVEpq<_wyjtFzj{yvZi!r@kGqKg zV|MslYErO&Ct~q}FHhfE{Q!t8hiXttTU@)zMgar1KM$xt9E@m4mC@o|_i@iJVFkVU zM$gE{eqiH%$ZYGdgK6*%^5SAN^*~Z`+l~kL(vNsD3s^);_UVhS8LY9!?l7juF2ksj ze1KqPSM0zfFa2=%KVTkDj9}gVt*f4P7t#SI_H_f3kvcHoMVRRT?e7}R2b{$d$mo>i zkdkI&57({JTS2iS6OTenc`*{)31)go0r}*?0ZYCH+6#kD@6T8(=xR4xlk;q?>UE3) zw(IFw<6zb7#M}$lgPjpqWxCGZ3Lp+`!hQ+G-L3=xSII?8quk~R%UvgG0(J*C>7o*D zjM8$}Z~vt4u5Tb1jnkZnJ3NTCuIRyl!BqX3tOU~UM&3S1W5F!6y+FY>YTxglQlIG* zi@#2NfU-7I%FfW-#e@@W^!kxaM(75{&+zum;7;%?pF{4s6Ke-~d#W~Du*>e>$t`-r}vIobpK@r8wiR{L!q`|Jp=DY0(G z+?QKz-+h)zOgg*>KX6%SjsL(o+99yeLHq=2)v5L3i)TQ=wiAPBw=<;~+Xi6!jm${` zC7%OhXnJV%$}9x@{#DCO<}ev3B5p<^bku{wg=&+ny;*d=v{bxJYgcZ&TsB@or*Qy%7SHsu@JAM5{1_21?#b#`wxEZZr z97nFhlI5gOa@l!cK9wwG9UD2wgf>u3-A4F;x9UC@6<%rvSdr(61>$-bkbxRUzK0pA z@`|dTc+<YxD*_SftKy~Nyi9UAoF@%GwAY#Nt=(Sfa5$Q7d2p2ku$6CZAc8 zl1h^jX{9o+jWIh;Ai=^M3q`E@UgT~-bNEr!wn0uRLP0T2?BM5Cz-1|>O;RIud)7}x zA71~Q$UWA0_9Cpq0`jOx>uF~}m-_~BD7Ho>g4*87x1w=&g#!RJR?EoWU)Q$d``1#? z0KDkn+&JKliS4V$0hIPPCWL-|r|hz=ZVgy6ENQam6j{ z3K$tf_)BH(kqw?CMX;*>AuaEviddK!?}3;GHZdL*hLQI{^8t}DI7hEW&B|WwqCrDGz2j!-1~%mHUYlFLiyZ?Y^6`$XAH)kB!7l$V-Egds}2n$wKUmw z{LwCq8bp{lKC{}?!~?>j_w-0rO;fQ}W;Bh__+_(_NO&3EEvNvJ;|d314DsS0lGByc z7222Vo7XqslPXtAqnp^_AoWm0mI&GHlo6fB_c3`FH`kTrh3*#rI~j%&*1=0@*fof> zmvj_?PSw6OsDS4yLFSvO#Yu^*>1kHn5qf*^obX!a*T*2M_Zr^5H}>+19JCh`Nn$eE zDg(CD6TfJNVSFeDAZ&ZqHbM`F@&*kAdQfu||F_<~T9{glcV@ur5FKJE-& zwRxIJk<9VKG<<%)(~{_Lu2}U2-2JSV^<*TRhxqD6%jw3xH}L}p?$bLSFLAjMnP_ju z%AjU8At>X=0_XAFB0O;KIVTxwN4i>2ghrxc`}U9vyAd;ir89#uaw~2BU_<;gt8g|| z#>S$f#rQ?0jZRdnLlv$=JF)n_=hNf^C<|<=9G)ET9sGU{Ly$WC43PM;OBDvpMDxWR z9^ZEHtpQK#kcae6vw|Ti1`=qIyliQ}EF~uNCILSI1XsvJE(()V$I-mW_ zDN1J!K86^C5T+8Ww2uxzr-;LvFV(BN_WOY_X_PYfYFNOM4$BEE-k)VHihAA9YHL~Z z5HGpp$**<1ZiwueqJ>9e>(YLmyf3w2`$c26uaHU3UmC4Lg0+ljU5?vCKaHIl06DDuCS*+Y3=RR!1B=w?Eh!?;WeuSB$pCdlh<>3nHV5uu}k9}HuR5j8VJ~MFQGe6;b;6kS8mnQbLH;2G3j$UA8$S(v z1;dy6`6;v(w&1?E!@{dUND4lZlIPYct=fo~?Xz3Q^|HMZrWZ%|^CkqG*fwpe>>%d< zSHEAF<^XL_`XdursX@A}*}F|nZ+0Qx(-t*z3~g_}{YX&6-FP7qP;Uo7UOh_#kJdVw z$$Ej?wbhwSeIq>PgXzswgPpw+j&u)WbPCEUB4U@)Y9pW&_q9M3gwgb9C%E4oz(vlm#+kA?n?<;yV5-YK!1gOntxMZQ>D5F?%}wW!_lqRVY#@1 zqsqA4WEwSfMG&NwD2z6}GvClRnRp6vjN9Trt+Vb03q>7*(J@m6=gXP=mg_Emi{c5f zAfHBvpG9c{`x$AWVt8?5mOgk}yMA!AyJM*$O&5?Tahw2TMoUNVMz_OHeF5(~pb64< z*$?fo3Fq~2b}{V*8DY+87)N!Yd@efRJhjKDQKe!VsU;q+B2&aNCA5XoK{t1`Jh;NL zZnKY-4vgeXI!s5Mva~VrW4(sDS zz=haQ39I0W3?=u)7>kIXFj!9O2}b?Xi0$c11JJJ8Q;CkYm(kg@#fKdB#ApM2#serW z>bh4Gf_6E{JV6-ic4VTfRtm<~(1pQ4Jvp)z8(kjR>5^m_To7Ibicc<54Rsu@Kw@Zw zkkf2zCzuy1TYtqM`LhV9Ovg}D;Se>xPSqu@*`W1&m@ofcpc6fMhL5R`sGxah_pKkE z)PLVaa!at9dn0&QgC5d2_8#05vENg(zm1`nB#sv7^JQpXXFLHZ^wp0a)z&`%9r;G@;zJ?_K$cQcn4uaHhm{S*y*JN+dNC&8r zMh!(nM|kF=TC}1uQ^wn}>T1lwIn(&yMWB1ioN+*@gag&xsw9QrSRMo?3yYay*ndr5 z;)U|?Lw3ysb?c3VhYo-*9}L5OJSEQL1o)@$9y}^-;9VBT)flXD`UIZ)X!Rf%ZK6s( zV{sHg*yKS9vPBso0|E4Pq}VIa002(C2o5X!S{p8_aK%cReC)*>0pxsw5Lku+1odJ< zQK7a3)EWI>zs6^yl)nfKXEIPGLMld4XWloQRN z9;*~)A`p_}Z6isjfntae9}sMRUVmUb-myoD6T~9@<8q7(RywS34MlQ#PuTgKV?Q)f_Ybko^c&^=Y0 zvn^Dq3`ejm`e$e!iQfN4`fp7&!B1LsXvQ`KOzRcjX%v78vU^1(xP5UXLV7k`W%C-5 z7W8BQ|I;&Pf$eW~@a9|Z95+Z>u^uN2aysTb`K6P1?IojCcvAYDKe$db9nndVa(WpV|J;dx$0W-i?pj(dlb)C%ZQjGXZ9~8UC^Dq-^*?)}4Qz)K`qB1Z4 z6{1-*XPI~i(pZDj7?TJZyMbrrUv}P5>uUB&LS!JMVjI*ma0AS~eS!`bQVG~F z-USUB!2#UM3g?MQ3`5ITO5|42rNdX-ql;)J|EaX0nhB#00;ck{(*YW(! zjI0Q`*x@A}ZQea-f-a zacfR%XW>750e#9?xmQg9DO@r(*Dqs6XQ(}>bBXQJmky;#l#CxG7RhVyQrt;EA960l zh(TDqqKA|a?a);q^@FqzFzRmHSGl3EkVz}m5LR3tjByue@CcV5J}BS zvmQ`qm8(f2BowT&7vu)6Xx()&mR1?u(v4ziXjnSb%A?PQ4OK+H8|dWGne-UW)zf~6~7*cJ`KO4O%L=}eZ&kg^kUiG~?qRC}u05yv*QMjK8+#pV28yOXeY z|A~WRI4MypRBg$rbq)-KWjhT`y2fhCbounMuPyhi2ooR(Ulu_AVV4(V?hMyt^x5}a-yj&p zyN0#1)@LPH6es$5--V%jGk%6)BsS zig_0V`N^X3b+CPv^FwR!1MD?cpspDUNH0=4+^_DpbkH3*7-B?bur8rHB+@*rv~M1w z_rTg3e%{Z$T240)n8JMw!4?v<0;wrVB;m+5P6><|#X^}9e_j~xrIlY!C7axJm-Qaw zSgEyWe6BNVHWXy2Vg(R|VvQd%KHX;>vyOYe$t$4ck1JZy{_*LY{XjFKIxSk`6G z!Pxq0e>8hEBvb?5+4siMLxp}dYD1DA@`r8(Fb&ZNW;&K7B^Fn=-u7XQ;WGiI8E?@6 zz4Ifcr2!Pdu*K>qEZ~F5e;>dhlL~SvbDQOGNKUa+Z9Z=A`l^0p8BTN$+x2K}ys~eu zpwhVL4T8~ST=5=ZgW7o%r!B+6=@UQ(pT^&)iIkx10&?_0dRlr*fF$G=%%!D;KylHE zqg%U_Yzcd?vI|;z*>CS0q3vAvs}=yAFH#vBBjdZSMC`J*>&C&E-}D53E*-^EF{{Tu zBW8q}OUfJg72@~h>ljPbnoK5mX$od^vVI}ee}F>8MQF1>OR#wyM!6K&sY{LvFJ^iu zWHn`ZD5|>nh;uF}(Qm;AE|@7OQ}2i_Mhbe1^@ zcEztadCglVm{7KmNEfs3j^2 zVo%Q-WiAd079=~C3jXGsLQz8M3o^fueyc*kDjXP)T53t5EO!(= zh57e0qHgYizJVo`SF7UAAh_M#J^?sIuvTNLP8O+3|$?Aj2&>}5JO02Y;00Xb?o4_z8w^GTFu~mMYc`dMv>3}b z_gD^mCCyR&>9}t-m#Awh0{xw!{~-~<000i4H(t{CJeUV2FP3jD0m5;^+$_D4c_6a$ z$0(gTUcZ2?%V1?2ngA3-6NjbS4Bx+03W}ZKn)2O&tlll4Ua0se8f zTrX>@PQ0Sum)OZHEDN9ah4Eb? zujZg`vg^oqZ>Lzcb#SSuD)p2h5baMtg6E&gJFUkUzu-GA8U>H9a@%J&vjNA!b)5is?`wzAX_X;cPxh(e9=k7MD`dhBF*prs1_)ggZmz4O?9x&c zKH7yU+@pRIU$EUlgU?nqbu~x?2^?Jp2+I6;(>g5lyPz+VSGWr{TKLCuZfwTg%Vu;5 z4ry`3!3Wl>r=kqDMz_N#(}SF6skYVH@(AfDIJE?!BpUG#pCzvwcIjGE0=UZbDoG=&(9vv_M4sPBOV*zdDh7=15 ztXFO$N6Qa%O@Q7@5bBK}FyqpK_1DU@@VwLBo_Y;l%|***Bh2{y5$Rm0tEMtkxC9cI z@?&&Z=(O(|Gnu+97!KP?nOEUyh*tkiRJ?;>62v0hq&)@x8S4#A#+$ws5`c-m#NjNyVLE8aC#h<+-h30@0<3= z6yaI)XT6FhGA+Hk%w1o#@p|k=u%Eaj{lp;12{XV%4ye9z&b{ChbTLleQ%= zTo_zf+1{d-x5iN3SwM+5W`FUbvZZ;Pvt5iQ&84EaYH|D#I7NS?XFb)iDw%X)l=T$% z0pCo=buZeM)zYguro8VwQkAX;OuJT=3?c#rAm!E7f?i&ih-0J`r}&q?;kjP0SOGcP z+~1?@eH*$|r06<9Olz!%Yf~V#(SZrQ;W>e3u7zb`*3-3UEFz+BCh70IMDSVyD;m9E z;a|vlKr#vLa+ugU^!+mjybf1cZGoR$A2pG}gHdr<8&HX7X9>bZhi$&FwnTd3b{bj=qm!?v9%f#{wT6Po8YBE1IJhNaY=B|za%r|4noDWpdJXu_7Y}K9IB1GH98~Xe;O^t3+W=0JGJC7 z8Hh}L!SGRhOf_U2BExATkk@ByAC@EE)N3Qv-Mm{L;KNY1c(bLo?x{CIH`o@ax4J#dAG;AJI2636aF9Cpkcjuh`%#T|HMw|#` z@IGCLZ)w3Zy9k-lehrik&?F2nhD#Jo>Bo4;qDac3I|k_MUWK8Sd)roJ4Vz$plc1^&QkCu`KvC#E0c#!ynQOV8gUDv^9C{?f&j!$6Q zct!GmZ;JsG(}e%{&ovGuV(P_dSmd&@rswo>5=O2Cop^zAI>DO4>${5=ctMg#3U&xV zIG_%QrX$abJ6s?3WFBkJET9x#G(F=wu`Tp1z7dBii7-Je$^JH5Ytlj|LRhaoDtNJ*#e=Ok-uTNW7Valx8XIJ~TIe0AS9ik>VXoUe7Xr)lqQ*awJ ziU31Eyuau*r(dHL=36j~@8zOn79+O&&RH z>ijGJWwWOKQ+=2H5IfhPLJrAF5i?jzDmfC)R54dPleNV$JFj#Iz@iySO`i@qrR(Ma z5nw~1B0d%vq2!s=Z_=J+Rw#lYuX~~!&vL??RskQnk1 z^cmxov^-!LRt!DKo zVVLtTDm{V1>kF|oNjdD(SSR7&^^U2GBl?Bm`8xY+4NxpiYHNUk&L?=cl7!;yo%;#z zd8<63%!aw9`^XOm0#g%>>OkrAkz4T!%0+w946~5=PW%&w4->uggYFRv|IHH(aWjNB zD^3Rca>hOI^^*f{b|S!Z>9agBALxl=myf1Ly@l>k7I`5c zoG?GLZr;^UFWHPftyW@*Y1y6XT%*N7hFs_gpX4U`GKeGH^x`?9Mgr>1s#89fm%||g z!)_Fv=mLZuCtf!mJX{6KgO3>S*+R&3UxfKe z6@sB2jFg}t6rg}L<-Z5puwv6g_~q`?8ANmC_Ok<7cZ^Yg^`>vJSTxcW1_#*Dx0AUl zOaqgp8oC&TB6*#pp`4r0x-HhkfN=R)6#~%AEnh<}0QH(?$&kjBiaf%# z6biby_3k%n!)t-vLQKq#j4-z=3i9OuOt$0MyRFTJG6vE>cXX3hCozYZK0shMS#8Gp z*QL}?;twI8n$y@4wJZVPODR?@8v~3&Oac>zX;iV62cf;uc4Yv8*BM9Frqj#9M;k}s zHJBJKmG-aIjPOW`Q_=7CFNHr@k6F(YZsALq8T*U=!`Qa@9jW zMtK{lSyuZ`uMYAI@4N5iw~ACkqcCEEEB)^3%}sCtHD6jF1|lzwA7D9t9y^-)3$aJW zxC{m&QR=8nj2J%q_`^5{#vn2I12S60btGRo1R45iP9iX9;+3>%yVooImNbKAcr6=D?4`iI zQU9apn_4HJ_(p6>RpvWGv1YBW7A%-(_tqY3v)jYmjca>tPU1RIMy7aurXtPJ`#Lt0KcBQ>wMSie$MnlF!G?Gz=I$VTqmC{nI7_1-%8YBQHqg; zNWj8foHf{Ka^s$>xS5UXJ1w*#hs>pL7cWT=6{7 zvH~O~k2tsVeWbXrESHw3S_71^eC&oRk86Rzs+Z7372UQAfBC8D9O1?H`W#Al_9W*u zQVSjZ^F}9K2Lk0Zb|A$^7n_>4LKlHd%!H?HlB0zU0aVTSNAwUhQQVIw$tB=q#8feQ4jfjtPr&)V-&n=0ftj?g3gs&H?pSf**u5J5j1Cl=Rpet5kV z0fBnO9#Iv|a9qTI|#XfhE@EX9_dqwS%Asc`YxYkrZU zOV9_CJq;~Mf#Si`Ya+m_P0=w|l1mO&5uCRmwhaw5zA>M^tSXLs%Q{e8(Sa&ITqU3h zoi*kyAAPm5Q*AmTbQyj--Rw*!s7QimJe#BFP#`S=g-H>*KM=eQ2)xxvv;UVYD5bFJ zFk90*t8Jam18A4!Jd7sLg1dKTkdwqUQ%|5kuWQ9N-WAor&EWCe*GA;3qltSqm8;jF zvh*OP@O1Ztx4!yogt$4ju_^^h4ilD(66my4&nCyz{YUSGyg%v=ZWh?7z135X;(YCG z!|>b-bWWeqX<~DR?~7eG7ADk1Ms09f+{zgs>VO6(us1awHU$J8D$oFY4ITUI@T1!7 zms?ItK3AsXt_nEkL)zn|iwv@u^6I%#DWSdEUhG%2N+g$i%pP_`}iJk9G1;b|7 zWX1OIZ#F~(@sWq#btt?K2x_?)hlnm*Bq_)Ib1iQYq<(55ci@Pm2O#pdC*18HCF-&V zCP_P+q+a1O3X6Qym_#7)WfsIW6{-&znV%X~6SctE4fZ6Qk@8_dfa4COME zuMePV=o$`B!^@rKLVgtvZ>A>xI7{kVLEch39uW64XHtc~p-AV!Nnsrw*xJ|KF_hi6>pg*<+my=QikUQo<< z1m{#7zs zVYcZ>TfgbAf5uAySt$^-iK&s(?0Y{gbkZ*BwFSQuU=w&7mM_;5FvLKvX3^Y4s>|Cu zkTAY!n@=!{SOX|rttbm>IhJ(I{|T((f>3(5t*X1~Ulsymz-^LJvu%H@3=;UuyG3m9 zUOzD^r!~IoYm!)0{vi7`<@atTV!NFw9OwmxUM%gpBbHS0zZ<3UB+^+Z*sGbM#XUK; z56>wPQu5u&u4~o!xp(^Q?Vk$m$`HX=9r0{E3S`%)&mwiW;K{jZet+|}A%VgAtSMnT zsz-qGLDo!^<1@tu92e%XNa~r)#R+CaU2!MT!U`uHBqa#(f`Bc7z@Y-R099?htvn>4 z#L3GK+-+w=)}H;LD;Q~yP5lR%VD3a8!wKd;(LW|BnMw7!aG%Hmx;5q2;A|}<2ZUk) zV(VkO$uK`4!2>roskBZ1QmO{Av(UyvsL5S=HID*IDt&rToSz+hM6ze*#}*!6Y43CH zg-%?;z_g=MFDBq=f)krsTEAc)(%I3!qk@$uRy3Tz^vBth%z4tAIY-Jom*!rIL?`Xi zl~p`s$bggLG-)Pbc}UL8cy_Pl7Kp{pz&!9Xpfiku0yi>*zs< zSdWo*1OAsca*gOmoEqThQlB-bv_XoQyhiI3X3l%)^4v49mqyN~7)Th6K|P(m%JjG3 z;`68%2SzAFGMpQ|?*yD5rzrvY5{vUiACq@?p_M>i!^;ttUL%#`o2^st5jzbhtZy%6 z8Uv3TCuAfZi45u0+Y&VVYJ?u(-QfQ=T^;y#f8$=V-ANQo`74b!! z(1$}TxWQ(f`ZTc@SakJp#IC@hO_6K6f$R9w`Q%L&8g$IFQ|av$KRn>gZJp1>ysOunl@7 zfO|G{t$E=IPZsVZg0aN|U&(tC8q|9Nq?_EvO0~zKL`PP}w7je~3440D>nAM25b?SUQs?xj1 z5jv+smp3Zg^x$U=mHvb07G)cP421Sex_iWYW(E zHcxw2rW-MoRQ10v-GcL?5_1_>^qHDX3oWAj?3V5J6;}U8K1HKk|HUSkV^EP zl;w@CYxTK04M+r92MN4@kWJSkg@m`GtS10yuPy}fdqAp^ed0Rf+il7t?{BlNQ%frT z8nCQPCoE=&8WV0TUVFia1rP`Ja3)d5E?6*6xLdiz@ehEIl9%7xJ*45)NZjq8vYtem z%bT+QDf{!V=-SE4RKClij>r@NTr=kCWKLU%yQ>ys~R1`=IK3|@3T2KHBtPVeMDR69g7x_>D|MU>PWsr48 zGh_3W@MOSps`ni(mTa?vlsXV@BRwOV+?I%5^roN*D5=pN_O}>RtR=DFy?zVLp8e|* zy*JtR$yMUxclAk-f6ezE@u~RIF1RZ&nXBtk1Xl8RF!Kb#9qD70%yA^!u0^5GyOFZZ zly`i}5I?zX(ofAMX*Z7Vbgj?Pe!)=RC3#&~wkIACDH5O-2ZajIT3vSVG9K;Z@`Tso zXD{LNn{k9B5}t4fML`XTMVG^Qf7<~lPHgUPneQN8;!2lHLq8X8s4NLnw2(+N?CW9& zCPUo#Yd$HWnErG^1%EKcNI&U>~ZEp zd!e96gk+r=&uvR*(Q6>#ydOptX!df11v#^OM%Fo`jc*c-NzuQ6fKDO#5L7CmH`q;M z`eb_Q$umgHmDyHHvQyj@3YWi2v@juVZapRm1RLliz^f;-@!`#0hST2JkNDK-e9Bw( zQzIKh@@`5j=AWuW{pSo6-{YmgIM{hk_nFrMNEnza|34yw^IppZ8vbSx3;=I3#u+1k zjP>H>q2HCfWIMJFO(W3O-NpvwM6CPYcU{RR-JH$Ns%v+CKqRE3+aNM6{m zpTKbh%u!z7D^zkgWl)0G772l=?+&n?{kb{l(+#HrS1t=RPJQOA-_i!2ORtzx za1}kX@YPp?N3SBeAmrDM1epK_8sE54p&)aFOyQJXsYJ^=Vr!%FWhvbx5j^Roff}T> zH7bjXq`WB~b-wxOAM=v6k&%=Q(@MQxf8}yL#2SECa%UKU%`I9w?l>lj<}({b<%+A5 zE>odckHOk8I>G7(6rhrJoMG6RwUSqA-^86=gE4ZEX-_qyF-xW1FNqKOHkZ^n$3rwT=PL_Yj%!1YZu_tY)6Lt{{MXkrs|h z`127F_Kd`_h8+0OvwaSSw?Vk;g&!qHbvz_p7Q7KU+NZ;?Hp;6II6{M7_J-#G9`xkO zX58ye@&Q`QF}&kmG(C{fp2b^x1)Oi4p#lheRP0lq;HWXn%AVOf8BPhPx6V1!Rx~qF zC}3C6EZbn-5-M?#eBPhKe7erV2j_{)^|1_SdWumeCCjl1pKC;SCx}q{y<6{T5!qv? zVbw>tntqwN#O%^CHXr#4&tn;Js0Z6%(e#w?i_o_FwT;lMeEhCnYlkm=LVn0hd^JK- zFsUg0tVl0}ftnOZ)3!!$-tx|)nto&kQkBIiEWqpv?^|e>R&4w*3;fOKtA_U`ilEb6 zOdk)ODbIRuj)Bzo{&PCmun73Sn(&6>TE;`hUhwZE`!qkd1IAx4Ln9)ByjS*g;2liz zM$b&VW5tSFcv)9&J5VIV(Tn~6TTL}flT%$FIWdj)it&{o$$H_|UADV^7=xU2*O7+~ zRj96*fSneCx!HG1SsGFj=f!M`24F`!{&hWhcwmR$3+yepUaS!b-qM%%S$8Zk!OeXD zNIzxTfnJ4UzD&w~A{v0aA)vkCx-7=@I8atckNnGr8n)X@-FkeWaTQ}4)okczO>{VQ zY}Y2hzLc1T_n;x09O@$!bl%mJR!?M+B{)U!28~uWM_aZByUXV?BN>KaUFvQ&oW<(H zsw8c>Cq>8_jS-Ehe)r0NEa<GTfkX%nS23XZGW;K@QU58wAEd+;pL()+=Yte{D$L1N~@2tLGR?8AE7) zA`#&U*LrA`Hxck;K5a3UO5G${J?xEpl}k@#it+<{6VzAWi@yq(BjvrB6NY%1pL>!) z2TVWD7)DqfD{jxJJKk$4{LVc7y~AOSUKAEhI^i;DqNO731_%be+bT1+EksDl(a9SaG25M-IZMoE!$^)A9&k*&T8jy`$e!*n~t3%s47ZIor zO?8pZ%ApR0si9+XIr}2Nm7a5#@Q9m87nhlm1!{4yS`IA!;&>7FpMV7?>}KMa?f zqxAzNTP8%$#JIy+exeem6OMc|uY%FB+a@#ds>@5@si87|Tlz7CTe8vMl$YkK*(`t9 zH%ecdoAT>zZP~OG6Z!AvvZUw~77}MJZ;01EaI*`Z-HQC!+qJe$SV8{VKyu zYwgE|x*CV-y|O5S6ime6H5p_AoU;B#IIOkjf)U$qptwD!8)tJPQ#DVW_BmCVRyaD4 zu3pH>`E-V9rhOOA%GnB>iMxNeE;ybJ$FmoHoi989C$NYHwdiyjI6od3yPez)<@g&r z*rxUXum0l*6@2S{o=CZFUF!7xD{9Qszq}g&h$`JcYG)$v81UkD-CVwQqPvdErJAAF zQDgr*M9RpKKs41@Aap#F2ye{uI%N`qsw?^XUO{c9`AKy7ho25$Q*$yP6skI8roKexXEYj^4F*bFf6u1s|h#p{#F8O7&G5l96`_0q_jWHACp#2 zsAQ`_<|b%_U&)yhLI+$_M7Zkbt5>WV7cFe_M_tE38QH7ck3M0YG-9U;&^09j!iD^_ zDd66x<33vh+G_UMt(ugOfH(rm1?l$?W1?77BxrLEuxoCf4zK<{ivYd76R?o5_*Xz?Gne8yBi%-rwa2w^!u+v@hY@qhk zLra2bM0ZW4?Awas;!2q-qZY=D39QQo_cvf!U1%aBa^h4{(C zAanBIc4sd?G|1W|RKnvi&aGWU-b+z@N~CJ#~SFVPX? zaxp7F-a7sXlv?gA{oTd!f{Vf(C7zM3uPg$Ta?vG(qkNGz)E($*L-5qj#wCS&l@U%lYLZAbU+Pl0p!q8&CF-OF z@B=9|tB%?Oxo>KMtY{_p7NdG98hABcAF+*?8xmy89k}z$r(?5Uwtq;F(DA*OyqflK zdT1va{dZ(;Yf%mL?O%-Q>>=FeF;^@qY3soK4OQ5S!N=n+`ZTPLSK3_3uE%g{7+tw(Bqjn=Mt z0?cp#0W2nq>(&;77Ww*(A3@}6G(sz^mlEmop9V}9y~|BEa%UjT65|ZUgcc5rOR~xj zQpG~}D|Die?J5^=3cDaAGgF|07?kVnN)CuQK2V-(rI->t=++m zT^ZwQ$Kb}=eg^duor@Pl<7Tpy+vP^O%zMXZV4)#h3;M9g*+?0A6&sAYU>We~BbxOC zfdd-JgE8-YZaqkjXVI#k>H&(pGW3i8wW$Yl_6=obnGG5uufq~6bituZHm1}(1Pu>F z=L`X>S*ujZkAQF1{ zc5pa2+IaXL%dkL7wq#ei?v-&Gv?dh1P9&7hFJo;={*n@;7h%Ei&vsvHxL=B)%ufw8 zB$V?ub*hBdPasfb3aTaz!G`R$AXa5eoV2Q3SH=*qvJf^NZ@^uG9(8G}+br{Zwx_xl zpUy{x^!$Z-$3F*<%Heh5*RcD(I6woS%BjmxJ%Auut$QbSNY~aYRhdzLNxKOA;b9aN zpxHtIH;e^%(NyNGxo@@HL(F1I+i2_)v+S^PyvbC&SlmjxMr(q|pk7p{xuyHI2JpSK zoXPJT&vwV>$zQsCaet6R`2Xhf$&qWGgrcmvUi7|H@k~(R2)=neLC_wcJ zj`TZ|Sas3q8_0+^F$a$^-C8%)MJU$WRPgYO{Uvg5*H%GB_f2jaE`d#6ev!!?Led<0~A0JMGJR19;2w=tVM;LLM-0o;H^nfsZFM+y@!J<7>P zhOGy=s{N)Pc1XRDCfj<|hK2_GsIWxx5)X(&PUAyYQdj5GaKqBe__Vubc#zdR+L&uw zJOmFuY)jbeB1@+8fPwB#Cwi636FJH`_zK}{lws_y4`Y~Gt}AIlNiwk%e|M}Vxl`g@ z=@;(L;~7pvLF{WH<4We{8o5jZx2mV@5q#{PnR}-IT-?^!B$MpeYoh)#!V^N4fskAM z4xgRM^|Mj)S`itNRk_Ms*O2oSRzm20cFWn76hM(=Cd@i?HF1Z1TqMga)ld{QoLx6g_BDSa(jYW>6H;{H0~Dv%k}D4@L&>4!?JK<=-sByC-qtI_VzP{!h*l*EJHAuD zW7}myX1l+5OWr}|!=5QI9tMH_0<560%;5TjBR)NY+b~-rIB`SR0Ch8$yFmCyZPA~R z2O)XdHB0ISUVeuS%9g6Q`h?|0KXAjEskk#rF;-Ps;&d8Qiv3n0R3oF;fLoj3S?#V! zx)24?dnTOg-31TcVo51~pmNxv3$H_nf?cTVRB-~K)jH&Kj}O6m$r<-gqUrfio$5@F zjJ4Vv`I}!8@3FS*)cXE?87nwX65?ue+LcY+qC%-mOm;!2`U-Z`h}wp}>)@ie@P0l| zd-bB``zgnxO2m#ih3nl1q9QnflV38HqWbYmEVf@4N`f^*$beQlQLl#&dRdunoxU^o z%UUY4>$t;#f-=^D*%KZwyq-KwFJ`Ug(o#7q0{=C4Db(K6-=l)A(bueBY|+%F>OpFR?Kzc4(8n3cB4xO1bXHB-kPG zMY!AGN`}D{QKQ;}_#?D@JnD<)LyQhO>0?#ZwS9@?wEZ}myZ*b<;wU4lq70cSg|twp zr1x2?>dlL)tQu5l~tSP+sQqR>?vKmXzoB1YH zUyftxSBXO1t5&cue_Z&qi6@lc%n%_f&AZ>h>W_LoTn|noSE{XTO>{cuK|zQwV!Iw< zpyc2Vz0_CXk%+W(`)fPTJRP5OK_h|jT2CX56>NNx+Fj=fh942=f-vmj2e!n?sP>2( zOzd7KBQo*Acb}K<#YVF@1S4<%nJU=X(^zeZQ@hN&!y7Q)-brqKf`$%C)l073EZ4<2 z6CTs~Pel~P`p}F$qn|rl!gQx=PG099#DW9vb4cNOXeuyLMwI)%ZgTgeAZJ2(pxd{R z0gFgaTq7TRnxjAVt`Ky;`9^OP#go6ijBe&V+sA^@{hONoc@!C5RG{Q0Xw~%U!OW1& z(@o0Rm|sTn$HJAY1xmsPsm@+G(1p2Ks7>vpqkOaMy4$YB!MDL*J?=_gewW!*v@oKT zCGV)=4$b~6?^eK9{_IuY}w;PH`fMNO6O@3fkY+r2iWvh!9%>pH^OrO zKPg}hRz}AIK%^)aULzXW^mVW+02dy*VQkmOkjmXOc*0^k?bkF3 z9`WSIwy>?qV(zq(TijPDbh2;+eR%*Aqc!1;zgBM$+q zhL7MrK00RZ%v}-|ps*4B)0I5(q$dY$b>MasxHK!@7L43jEZdq{j}Nrgf_#@t9?mjG z0lw{ib^N>ZKKTSB^4sar6#ZP4n~1JA|AdP=JYqbD9jI@Z4u5s8+V;RXw zN}z}F((Y+-L~O+n*q!Z8{fKvtS@cjW5Vz;<@cAwqE(i4d>4`_-5K@S9D^lT00p90`&_pZ~a;l$k>IeOltuT8b-`w{yP^HLiegDfVObdH4tAr!IqRo3- z?y45GNFr=J`z?i-k_K^lA!%RK5e;_s7J71UVyW&OJV3fp7{v zFtOy;95UTE^26^h=l&-mzaHR1F`8jEwNl2;dafsQ{Ik51Gbf z#|lgflQn@hML3!{A1^p(+okUChV`hbN&@DR>+-F}hWA{G{#6eJ9ZtryBXqk=k2_VT&Q6R( z0E@`PKk;l;51z?lioGvh`(tXi#Bp^st+;D>_$=rYmhTm_uix)HJ7O%2sDYfpCZYHO zt#b|HDCoRG3+O}($g!WP7~~7Q(bP?f^#9HO;jG7;=iaSL4S3x$a^27d8TvT9dWy2l zk(U(le|aAS8;JwpiH0TwXDf1d;F?{PP7z%^dQIjZ>KZPPOGk|^ph`f)01<;i+~lA~Ug!Hb-9{tbSAmq65O`5mR8>zLyKDGmd^j#@N8 z1KR$fruBiZ44{Cgz12%#0NxPX22Y*D{q6m?P*;j#{Bd{Tf048pxTt-s3z>z(p9co? z9uEqtTR(RB`cS?)ON_ef#^W|Ww;%D##dkUgeLO9Q7XK~84>-&CAIUi$+iA+7vT$f%U}rGyXj-pI@Q4XDQzYoLz9dRg4OS}Y^7#m;^`3u9gz)?bgUCrY80q35D%&q0_z>^G}`` zF=$foQ}N(AnU6PN^W;k}31EkYb4oJ=VmCCS4PB>M<{jpzrp4lt4|GfY(04-Nf4i1s za??7hg)t2oRAV!N0cPJLGxew0+{6O|5H36}K0!hbZgg8t;qjd`4#&L;L-SIN%G=u- zJ|X0kHd z`Lumre`eiIzjoEX;Xz1$E-}?;z=_>?%u-+&6$4r1-Y`V**7W=NYjoCNV0-sujOuNl z0HY~?J!S;pOMd?kdW)fJ$?BLPSTkn;{D!Fp9>75fc z<+ZN?b=5796(^#FTo>AS?D981uC$lkXb=TT25&*kUpm^*g@4}oALE;%YBN1E<*iBXfqeu_*RfYI*`TKVtO)qBTkWCDz?t-k-~gH z85`9Ns6jiMWPw*emakUw0CS1~AZTkx<-iR_dd2{L&|EWfM?SyHDCUEp5^GGQC_gjI zt7SmmE18sfE4`#VU9Z2(wfzM;yp0N^48y{aUP8i=*!#<2%tsYaA%X$kT#8$JYx*(( z|Kyqm7zzqIP9x#u)1x_JFumw@;{f^oadmlLO$Rw{X_qz5GVhCU8cBBn0BD2mA&Cd$ zEKqP+14-eukopE&MVre)?}Hg0dFazh10#HCwk_2;w1lE`0MWe#zm^Avf*pH5?zGZgMdAE-d*%(qgeyhBB)vV0Wqt5`8pcGk+##e&o zdtXfEwDwc&zqDFrf<}YK9*rhgZ6Xp{-6LuKiLL-&$k*%u_l`d6J6^#-gvFVa-hO;= zxG|adlNYF%B+7VsR#_{Z-%5xnb5zgWjNnnv6Q8o*7)1Zg6BXqJ=?IdV}*+RVpxrc(>ES`r$Iz}7nv?ya@k{~f;z8qnE7+`2Yjk6b4&21w| zHyvZm>$57pJSf5_-(Z`=0QndLmQtt1)Q-z#(569U{?mDATpJi5rxIBYLEuyA}7RbB$&BZA+&W z(8lUZvQ@c`!e8Dvo63m`KNUF9E*ZQ-BYDi4pa+zf8RQBTWizJ#xB2~daU*srRlQ%Z zms?zQH<}LIu8lJnWU}xytyFgi?NtjPDnM5sVD(_+{>Uh|z;0^qI#=TN%XA&kxOF{~ zI4elE@;T+HBVmi@KZgOxK?_e&Es4;=iiC)KL;lvcHV9c3I=Oyj>#QYsP@&ctDW}QUVyrSr z`uz>1nKr4W`zu+kzb8Qj{XS0bHH>%IHjgO!y2;3`xH|C6t+>T{6myl>*b+)91L1Q` zU$%U4f7$8kDui5+ihaEj`9n^NSWx40We#2ge~Pv}8nc!bP^cF)lwSKUg5{OkcjlZJ zxfkEo>^|e;m?V)&Q)c5Qotu+(`c5u3C_@w?1|J21}6HOa{MuT+H&O(dL1th;cVL zK#(*VRl7cn)K1VKaWhY%O?`hn?7EAg8A>20YB-@Y{Y%bC9xTh;;Q97hdT+Ab>*Dco1NxrI+G$rA|akZa{N}0vXRYoT^1gV>Md4 z!SNrA<}~hpz%>|d3lNYM*T=fhUjJg)^N>R4)V8`y1hvrDz&xGD*=<(i1lgJ7=+Zk# z@NSdWHTsNgc87E5ya+J3VY7KF*8R&{GLq6WZ#~}OkjDk$^0^l2ZnvP&5z6ir>%PM$OTMPjA7WHogD$-d7=NeDQ z-h^0dsKD`aQkboxnZJ}Ia0REAf=8-NOx(=byO)=+i~<$%yn<$4_7Z(T#p`S$ZD(*Y zLE-zHDlKJSMr8g)9W*8tG2c~3qxKq%^W|l_rq7p@&y#q}^#E;R5dNw{r)3e2pnaJJ zXHns)5fQzTLT58c#m~4Ce#TQACAm%(jR@2}o7k4tsrnc-Z8gkc?FwwyR`7h~_HG7Z z$iJ?ulZ>#=SH;ybqzz8=&0O=}+OcAr2w=ttIaDnie}^ZYNzr2%@4 z1TRP~7SHr}V8^i1O}+eIY2}3wqr`^z`}r37pmkv%)$zNpqI;L?1_pM{8zYXz=&#zB z5rCCy6b-V*zkJ#R(!sw{23Ofk+u@>vC@}gzps(i} z8IH&lD>ln>nq55xj|ScAE1VR1ghgHwbHvv`bxP*%i9ZmeJyirrUGmH4r;hDhErC4W znAFo^j`FCY6R^*(U9V(LtVrwhHt3LrG4KxYvqiynU*FCPMNeAY=LQ-PVYK3Gm_O-w zQ1pyr_hJqagwUFB$-P7aG>f19I)YYgo}CXxz0*&QfH*0R1v~l6pN~)K&{d?&Wz(hL zq1&EX)l0ef2`*tTh1)G^^jE=bS+B)o?uup_Q>ox$tqO;{$r~5tr+;7H{k7|YBHH4Y zeak_8_EmfoW|R*#Ay(ah$z`i>_>jO5k*(1v3d0DvtAt7yW{TLsKb^xmmT0a7To1#y zc6a|T*S&|`8*0P)jWIW6FiX0uOD;74BVRL(!hUHthgY8 z3Sga7H@V5T9e~6vk91sGGKUCPA@p6thyCT;(8j-XUm!q1`6}dY%N#c-68q%-5IfjC4KJNdR679wnMAXmfK}oD)i7@21vpzqD=6t2`vn=- zncbjHv-~u!9C@V*%k0Uu)$T0G7wnsiP%5g=g?O-gACzT0LgTb}P`NsyJE?7Dd_Kn45I*!la3Q$rSxfAapJtHB~(-h6Ck;hNjrw?f+8 zb@KMQa{Kfte%>BFLhoBmu|uy2y&;c^I=s~0@g~N}-v0a(0!(1ZR?Q28Yex{^f3W;5 z?EDNmmEWLf!1Rhf>+rk3SXx@vSW-@DXuK?Ble)h`i=p3rxbTV}!Gg8U*hCj`GAi+p zJZx?mU@XRL?6}E z31Lh9P$xNfA6*oL-x@#_wzP;rVuu68A18nXc{<9vqdTiZTVsgC$6|b5jWXo9-kYv3 zqJJ%ftNKpMlqBTW-Sl0sdY^z@a7tz(F@g1n%o1FQfVlOtptwt8(Zm7v%Qa(NO5h>< zU^a9wdOso%b-XA`!)v0$l~IV5J_}a2PGMKmMIWCFCl|&XmdeMEx6NT~oHeUs)FF=T zjLtU3r-+x6fH_BIks)%hb^ZjCA|H_ebkH*C1P=x%*LE}P;1Ci;`U49=!|wGkdN^hT@sINMMQ;9Je`h zThqY(fUh7UIH}tIMeM30E5V**s&gF{?1>SbT7K9?@nHGAox1q4YJtYk%7y&ndAncP&f=ez39)y(@9a zgxluS@j*(OdAuG;^eZB|?DnV4DBey#uXvaLN^VlH1kqp}Gtg{j580+hRzXGVL(6W1<>a|G zCvsW@%>;h&m^C6z8Gvl3E!;9_UBK45FIhO=c&A#0tnQeKbLW{mfHi}6wd+3vh)MQr z??ULo@_kQY3>#)!oXEmLTRnwlg`vIV*`^6!>?*hnkV?(FZ`zs)3VGPH$buZtabR8d#MQ*p}@89&3sP%jYt>-r@X=q6{<{7b$jgspH@owBR}oIzpm|TXpZ^qslAffEmE|$} z9$kg0{?P0X=u^NAeo_s%o*N!G%nQoO7$UOi<8w};EF7F$$veRTs^tMUQVWEtzA{_t z-Gx#v5tl@}pirQVp8q#KMsbs>$Jz_1PX`$$mTX!jgxaH-oR71hFc39c`aW7;sZdU3 zxbLkFfm*uktAP(%cWfwz4=lX15dUGq4n6oS=0mJ^m}M2FlFJzv6_6P)4mZ$UFk6Z@ zD5>}$Y>5*CSl8QjVW~IbjT3v(ZCDhPR!9BaX z56=jwb_%?ns7yQHH9Thua7K*u=}|tiYJ)1NPc9=u^P#)+3>?M4O~~U5rD&K<-yn3+ z`U71oV=&d|eRwO1(w>=}Y5I6hZ{xGPfdD`03ryF7i}NA*VLzpF^^0rce0${-&qr_M z17w0|1@Dpo1Z?*I;*$2mk~8-cDUoWilQ=HSU&ZSk5(f6UcgvwHX_SQ4uEXZz;!5G>|bVWsIk?l;Qg_Rr1jdyYak? zgmeh2wG8`PouJY7L7jmP*Q9n3LTJL=)-R@(bn*RRLCI zLN$+D%tQTxSvUfoMo=JIS$oHUgv`+=+$c6bVfGIWU3DFo`$^7kuM?ve-b++p+CvX; zUzc#!o?{R>UFLuil3cp@!XBdT2X}^QSJ>7(qsxO$Jk6TUIJ4bOU}14bs5PxA)HzZZ8-iVKg>fd_B`-R{6mH(z72?2;Fs=}y z_8EE0UNO4-rRb$XcWtEjJRy8q){bKrF39(lt$bRIvq89 z;V}W7WEgW8xxD9atVDb)XY4f*ljq{_d0I-`rO($5^-08;R=Jc>g;+HvO-JeK^mHVR z{(!f$;~^w!u#=`4`HMrAMEVf7QNbhb+`iQQ7W(J(+8W`IQE!KKtk>N0HPgO^-=lFI z1uixjVeB@rA=A0OOBBk{Km07mNh>3=qWbYLRhnZ+rA5Ge~xR|W*)+Kgjr zP7oap;a3qkOf4w>JnUhv*EEEs9Vp&FRRB3a#=njTi1HJJl3#^cRK{M(ra$5y?RYfd zC?TB<4ey9NAAVg=((TbxVtOF$R$|Sjy#R7^fK69QJXMM7M-j}7;sP_>_rqJ5e>z$6 z85oaiAOR9l$C^;_M^=2;2<62CvCrg^Uev-{pjZ_~&-`|A+Vj!U76e-?@2tf1f%EC5 zbC96GoCQe1&px2wQN1RZ!Olu+KJ~8AVqt805?;XXd}e}t*%Px-SAm(0A8sG|enk%# z4I;9U8pFmZHm$654~w67TH!nF#r91Ojd<*WkbxhD5qq}RI!9vzMj$SfURD|IUt!La zb7ghd1WB+!IEGruW zs+3hQIdVkN(RVo%$kl`5-LIN2#jBu~Fa1oAbie5g*B!)JiB0y<>(-9^c}kxPLA4NcE_2eF9}dXm zYGL5U-y2P&xVWQSt|#LfsT~mOwDT(ub`U`SKC|Bk66on^nT#r8p(LSSsK(`GT!KY;rq_!(e;l_7uI}N-x@B8AQ4xD5I*Ec#oX6YG+q9B3BZ@dkia5| zilm~vUdekgAvp(Ql{`Uki9&!=x4g1wI>_Qlb*i6e^)3Jw)K%+N-&(Tg}wQjf2u%Iii^ z#XUSX%i>-x8xY!j8`925sM#0zRjlfiZm)8DeMunahz~w?o{wc4+`p^kMX45j1ivGW z4OSCcIe`nOTv#PZhK1I_F%E>{xEug*1D|6CH7%i=nt$#X>Y^k_aVg6OcK;`g`@cKY1kB?JdwCwj{Y_V@pC<>^rW103*%K^px@Ow<|@?EqTg+}l3a=;^Y_IG zwu8RA3GW$Q=pYjUKS66^u(dRIi*|6}P=hH>i(r=!!HLb@uNU>Qjo{>zr1!qX6)km3 zKvk1Gw&yZN!BI(j+W$Gx6%ON@hOhGt>wqKvL@ee3QQgP5ZB5>K=P6OAn+MzeOB~mYhfS5R(5I3`{pc`x4nfz8G0+1 zY_bxR&svZGK_qID z_(Q>^6RV!+5ec7J?XtIUD(_$-{gpGP!J3Ti z1SwLc=IjsS+uC{>P|2O#vQ>i>A1OAy{t` zDO4Cd54!j76@^6rCe*`6hl-cXkaFP`B9&F=@rCjKPo}4epIV9W_>-BZI1lJ=lyD9p zIdFV@QQeK(W1TR@YaY|0WdG(iA~6&^?iV89j5HpC;_ny*>wPz#;I&x2+gFe~A~U z0&d2qU2XFL1O%tmwI0S=$DiJDW5Tx$9&jFOL0aP}g^QxX~f z(&HX%DG0QNd9s@w%(t0w+hP}6NOlVIeM)HBeP2iinwwo1l~cNkAG4KUG^Ni4h1ObK zni+2Ycs=?#XrLfMG=%Z1qSe_P)4O$a8qw5<3kj?OzKOf?GFpNjpU!U7KWJ$XLw-H*!}7ZhwRBru8%SdM=vKu+yO*4`NdqZ=@vI!PKUxG z4d^sGmq!TX0Q02D=}zd_fxJC+g*OjF^t~^#ZcWA>vnfj}Cd9emG?=$8-AvaS*VL9( zKlrG~m6H}~{e_41qcuVd%`f^km(hCdzv?6!-&e^hl<={1zQvR_8(}0 z;#s(=2N}1WL*HVU2P&VCD_FY!=hNVRSN)4f^4!f`KajS{>?dz_TU42{^anw?NY z;o;pe!zd%ZfN%p6k7MJ#Co6B`t++Y&+tQgko_V#@^WpN+oNn!Q*sg-aQUyN#7v(&a z@H;`<7oUDU>{2A#^pxWqpA<4B3-BRo!&`s97uqa0fzBb&7(xYYkmNBA$7H3F2!>nB zZM2V%;_L}^V5H-klL?{j$MF?m8{j^zy>BmzDqJK!HvRvsC$j_BRs&%x5xD;*8>jx_ zsOXxw1k}dc0w0sCo@$TW8hHDN?5{T|lY4TmtuT8t#n}`#j#5U76ti8JAVnE$bQ|8M z!3TpCf-w>L-|1=17PZE@KOSe-Bp4q5T$dmWvxL5nz2*5rF?|bH8A8gh(fMH`{ zr6z@%k{6O;2Y<`Y0`azirjkr5CTpFWq+JKACOU2$;_{!T2Yu^fpF3^TeAMwL<3;-d zzHVfGhhEP9kZUGzJ8nSke5Oye^xiX%ycu3Bh<4+8&nZP`T9&#e&vv}4aPLEK>UioC zl!gWCo$wyJ_s=4%N;jWIqG_D9XwxY|9P$O1ZOL5R!Z?<~+8A8!5UDYG8KWKZ#(NR$ zB`0LcBq?Z-1a(pZbJA~2k%%iy+${5X1_40F(a1yOe&CYDI~-#IDMY)qW*%=*YFtHN zmN6tQ?AGK$&_ zdVAOfc~__nMH~?z%XaS?+80AA`aMDU2A{75W{hzhK$G8dm;#~;F{)7n`-muGVPi97e1YF zFG@p3y>=^#8!{4?ys%$f$v?_&ebuE)@y{03YjGRFN&S4jjm`v>4x1|cu6^(#hWnV( zhjoMQ_7dHv*FU;M(p7#6HU~DkNB}I?B`L@+iS;g%Y^rM9tp9EdaJ8E+m~;z~x{_^8 z)XbHcM+b^0!#U4Lr7?Mp9|XQLi2vm?BY4f%imTi;&4;SiZsT+-)x*kbv_`*YcrV1cW6G zzl6ZF9W?59g?+K|rrjmD_*FNRO|B-Vda1?Pg9Vl1WD=R}&rYTc5&vo_2QXu)_Plm- z^}mi4#s0(*dfExV9H*OG8BY1?mry?FpH1LtyE`?~N1~ABO(^Fp&S7c;1Ee?C#{ptr zh1lfkt=q*&v2ZNiXwISxcj&_PWsxF5d)zKwV4XS-cG9bYkAjA94MB!M-ONwY<24EtR)tR*sBR|xiXZ!%6G4=7P$Y%XEFl2lGsjNnQf^q z&5F>#Z_f`pYRtvw*H0v!sm%AB(eeCutvn3ZYoozw@3QcBB=)AN+!{C zet*IZ#Elm>Ort)e?}N3}Rd<wtQzbpIrKe@FL!Ne{W73A{CehwCr&Eh|*ekg32R|)Uni6}V;C3@qxZ6mABBUQ@y63~t2M+y%b5zA(5( z=p=iRTf>D7)S*|O9lXZQUPAyyj#Wji1Ir^Vgd?jE%FS_rSqDGSd@sZ1gA&o?ZIud##z`~Y@ge1m4m&@sNYWAbXkro&L*gksERh>tk!h2QEBe(E#^x&#S~E%SLspeLd} z*GZxd3)xEcL}q8&^@lasOBJ9}TV}FEw7MtMi0+XfQNZgiub#RGD@p;(&Nh{%@nHKX zS|2Y|Np!~B&qLBdh94K87def%w_D>J8WLw3ilHaPABqmaI>CFzsGP+(ep&%pr_OgA!{U-PE>7LNg$dgIg%zq&)M;&K1= zTge$Wl9&>ZSc&yW?UoRx&yZKg==a{tCh93f1#X+a&8s;FXy6|wW<`YAIiV9aB8>1O z5wQ%{dF<(DG@BESN(2rE&4O9nMT~`}tE!Rh$X->$aO>7k2(=waIGk6*Cw(QQLP*g1 zw8M>U*f62}XtnG~C}2QiM1+?)2olv;-8nLnVTzue^3zwIp_9fMYd4G|?ALl-;wUfO zZ|YsUqP}H;Ru7f|fO3VTV!`d%<2OT-yTtlcdMFXCOrLTI1GGf4tlMcK9Xa?fDj7V6 z#H=IHm$%(T_zF}EG_gC!QaWOYyiCjpv{{=;j<2**12Zx}=Eo!HSotT7hqdzE z&n*kw*%p^k?zQy7wdffqO=lhU>@vhzZrN7Wnz?~}g&%QbAwh?oW5qU_RC(BZBE8mI z;jV5=h+7}N)1|{Dk=j`rJ1|aLw8Dc2GRa>yrx9X<)%eM*!mSMhO9iyg#^iHM?#qowuRjOtv)qmd!r|F zkJ?_@<4^tJgB5GY_6Y?X*5SMd#OIkf7Zs^;j zj242i{X$y_V(^!;Kdh@mIs)V^lgC4k{*S%=Pu9k4k=;(!<61lRR}<~(w@KJeDevh>Kz8|Zj5UKRiw(FJE!ye~;uCz@cip?ha<0H;CW45ZPq4I&;yW_LGC`C&s(#4i zm!q%_{aJC&y33&v>2olq*#-)+cfr7v=@a<76@2xD%!Uk$>y}Vp9nt-2$|~$Iahqqw zI+YT>!4i84Qx{UtD zp5@QR(7Hr8A_df&GwU?Z&V4=4Cxl;#q{);Ge~c)e$vOV2LWi1D;?VxFgny$n@MVV1 z49&y}D0OT>_UMXRR9&;^&kGl-Zf+v|EiZ-2f;;7i^>IUV22iD+m6xJxy8ktD86or>5`MGWR<4t^oXG z@te)h37slaWll!>(o zYfhT)_Lf-Qs7U)D4>1p`k=Ze3vj=79)XfzON%76P|BMY5E4psvD+?nA?2$AzCO`3g zqgV7lhL_Mto^r*UzS?_=ElkG;3Q$1vHAE@xfuta; zp+ouPFAbI3-*9j^^KfX_2;XQxbaITZoC2nOi5GK5uA72`hcG-dmXcZlp2(mAxeZt( z?GMhl#CRRV*8|zVkNq<|_KFN6di5x2V>cM@c(mv-CNMK~C)7c}8^_H_Cy>XKlGW2p zVeM{Vo@&~!yX{~(cz^8sn1M-;kZWe{WRr}_S7VQ+FFwnf043h;V7XUugddtc0}X%l z=?UeAyv1>=&D&~T$=DIDFd_=Fn8@c*Inl{N>QtTxa9d~+5;r&CL~OVdmqXJs=}{>} z-ocjMGNE#Ew9wfZD(E+1=%kWcT#u7eo(EHP+PnSO{ZY%@!-K_(uu zHl+I?P7ge5Nsn5JL~K`?&fE3%D1MV%?{?)s(nUu$m`r~xCmj{VkMun=X^zs>)~Loo z!(u6~ED1IS#LEF)AB~#x1x@xkc=B55{j?R} zTiOR|*H-5>tA9=>K&=T85PA8`$vE&U)XL_}^ZjwVl*q}1@sqmUm8?MX!ea^~F*`tu zI+$g_B8K7P1>6TX+oA`7u;IQq7~7UU`!AsGtGGm_8ZQOBG`Goorw}7b3v2)X~ophUT4hY5cUb78b5EyU*75QndeDSZ1q4)GP9C`w4JkUzsfu zaF`F@uUx|ic8@svMAT`7v0r~QT;C3XVt42giqPdW^*416`ifw#y3Os=Aq$W{W!}m6 za!Qp^;yJ)qTQ|0glx`gn?6!UpNpZ13qP^eXlqCPfz_MU(BXFyp78JO@!1q&JeMjd2 zUp{4Dar&^k^Wt5CXgW!73F>Gi?E5v7IPZNgvDIe1;91d8uL}>0_!W4x+>)|dyw!87 zfS@*P>|`znPW|-n(_y#vXW(VT5-~`^=KA{tl28Up%$4!vT8iMwz+;TEfr*v;p9u@t z6UpfKBtq7anFZ2!-J7u}v)>@uv~gVq@Xd%XK(@K$UyW`Gg?o1A63%%Xh$W;jVhcP6 zTZ@51&VX|Yg76cl)OHOs+c}LppO%Q08w%0_^AJ-ZKy|(56CE*!y(FX*I7FU#X4ZQe zN3-0A#2`d&Q$H?nBn_a%AL04+a~0L5&#ixeRUzRTbwo9z3hN(t!BTVLHLdtHlh0;F ztrry06%j6CNn=uG@lyEJ79P(M^k#v~dy&~jrg({DcMg(*>CAGGF?jqkTicp?A_0mh zh>!q$$vo^j`E@8q-9E?CX8=D_)+JVG2n6P8+ad-1MWJ=fHr|H1FBR$4;dW_Zg1iCZ z1HaHV6omN(A21k8@&BPpC1m4hQTVO9kSKgYiE?|R$dmhqN%WO?{uu9kkvT5$OmJ^( z18biq}Y+PFoF7!FY;T6&mwB z1s(Ouf~1FFTX@cS2{#C^9OqL=T-r6}%%Gm3ra1L3rECTUh0H0e!rs`dUIu9W#;$gE zBAYohxCc5WJjK|ufMI{p0$B{qskOr22GyCU&O%t9!n)o`@IsApj@xsOS|hdcwR1iz zKnJ$+(=?0hN88Z&yfu5A8Rk$))CWy7At*6tScp&G3=?@I$p^TxUBSOJ*sDxIyI{Jo zic6nb4&07Sye`&`S-qpw^hTG```(s~wetBgAQHj#x;D`?zROgkH-wHXW{WZVDA<+@ z8n^TlNK8SnZ0O$#XN_S^B2urjesfgl$jm_*?f#evZc?^LkilU{#n4l$Kd-G9tnYHd zkLZH5`H4mjzk-1ul%~IwmZc#kX4JKHkLGA-KS{?1!u&gKVo`H%D-GtX-jsm91K|Ow zrC~gWD{J10I~8W7=dUVCLE2{P7o+TT`3{F9DZ^PcGmRai*IqbvOh=nF_EBZUTdJL! zbS!8ag|)fI8UdZa6nELh0-4Ku8!FNNK7n27Xw}bKyD{#ajfx2A>H+v5@lul6WDVATYf!Q)hbDz2 z?HHlVA~WmuT|Ed ztbk+v_YJWF4LXK;AeF5qHg`pKE)-Hhh&ebHy$8TZ**|eGN!*lW|FM;K`7Wuh%Y zZ?gtZhG8zSg}aj(#6qwPxcJ;f-02D#lY~cr9tmp6m_PHh5~+~raX*FP`K7*_-j@;! zsh`jmgUW7#$s#@&ts~beXWdUQ6Foq7MgOh}AJE4f-x-ScFQ_g-L}9F_IpjDzch~k@ z@62II?Bmg9N|gOh5(d&HeZ$rDl@b5}vlfD0y8Oz7B8bhim4l2Q@LhYnPfZ!jaU;LE zTKD&u$}eZ5$lqM66autDJyCJ*FPljF)H_voH&>xbBV9cU0=w5E=jnBeo%(!%LQ@3C zXq@g)YUEh6(=NePT7D|L;}OGYP1|1%3=;AS)sRQg&kz{KQ)&^(GMSi0ZB%wyYYvKH zW?Cw3+wv>T5^H@AT#)?<>xV7;<%Y`lmPb9G;*!cFg~-WD(vPx>Ir{s15wiAi$ic|v z2@h>`El$LvzqRU&LF%4DFa3rNMQ~co#A?cPlrO?K2X}T~ZI~wyKE7hyaW>pOkK@eO z5k`MsYPJ3QAeRz~r1-k7Km4gB#{@~kY81RdfbAqxhQg9H2lB!dld5wz=f zGz@~G$PM(O3ss-N)LLh>AkJo|-*k@KbS=y7jwGG)=3ihUX zJ`=Gkb&R}>ocZJC7Yo`Q17T#rU6(alcH)FjXAaSz;GKvUEE+U&*H`ylFNRKqy3ov; zjN}ZlS5(Pi5-KD45hB_vAXtU5aLuIWLqL+l6yaYCq03teJY{UB833i33#-Q^PlHF* zCr8T}Ld)IKbs?zo!MT~6R;>mRpQD0L9k(ZSH#mYh798O)NXDL4t&Xi1G^C+#^A(F7 z;W56SOud9Flk;&$l|FZ2`_JAD{r4Kj6tiqM4F~NCT2J&jnpnHbzELgb%>-xWA6;b| z)9K*zzMZntb88n*kfRmpd$-1o4Vhez%J@DY!urt7p$~M$FI(S??M5Xa0Zl9JzF5|t z09sQLHQm~`^p*YN(;YKsuBbM3fxJ0Pw=-fO%K?3A*dsEbpoL8p2wn3-L#($HUJJ8PodcUEY_v;G^rSqZR7py_zlxi zf9kLV?>n)$<$7140#2&kwFZw8<~!l2a9xIXZnY}C+|NuKT`wWD2b~3XG#wI{tmi6) z+8hslq@r^F`|PEEKfb2G6n!_S~n2l@aW4#KJpS?|Hy$ZiD4r{fVGIY^b1V>^jBDlC=ksDncW02_U^o zTEIMaBIZI(Pddy}65uReI_EYmXqzTV;CQwx3*fMF>Zr3p`Y*EhQ+Az;4H^{FX4%$m zWj`HIFdXZ6<)d^S&M4#hbJBAVThhcO5J{$eULuxEv7*g>bWbQ7R7Qt4#a@xYY~hz5 z&Y4bt*V(50nsxwYC?TOHbE$BZP|jJCQN<;*_=}t!<0d7RI0dWBgj5iW#;KJQi0R2F zjnrLW+8!FIBE2c)9iYlE)0^dl`dc62I-^38t|0zv27j@?IFc<-s^1*#Wn3}_P3KRA z-Mhy05@4eZ9}0nPHfO>a)a>z<0-ty8el;|=RJcIgXt&&>Q^4U&!~THxmIN2uCA|!~D&@yhQ#+n6x;x4kU-6flCTuk4 z6^+)bC~Q0cfxb=c#{s&WqFNbm9dzC+da69@5cmR_l&Y%scqs9_q-!L^3HO8EExddM z&MYFs^N-&fOFYtRUE*Y{nHs-flVWqjR?jY^ye>j=(op)*duK2hp}!r-!hKS*@UpR7 zOz`tg;%<%nMemvzt(Q3GU!U=`w>`~%POu^A`Ra7H34tFME;fpTh#2eCCla=>Nj6a$ z1$y~#k-;Nn#ASY#3^lMY#5&v+qOX3s6thj+)pNl zc>D{)*g8!|i0c@Uz(ef#@*um~SlgHPb@JSnV75NR6fvHoO3QY%6n{$Jq!LdP8+ahA zc!?SOYihHIjF{f0<*!n%)TJB%p1@g(a25K@CJSqHji|j>RVHrP@V|<{$yg$<&YV0^ z7s=Kv!%HK!yYN%mU_z!F{9nE3IV%#S^O}C0!cJh5jHJ8J@;90#blneQv=!Y>R)ax5 zFl)MmWyFruAVKb}7NA2(a6iICW;g+bqF%#pdNfWmLZ`<^sGl5?blhn5g9VAnP%DpokVCqrP8 zgi<7;4d~}x6XJ`{l%!=xAVSb6YL&1#gxb=2dt2ivikq_h?3x^3xR1!K^uB{#kO9+c z>s#{%`bD~ZwY@x^$v<$Le$$^5Bp?#UZQA$?{p1HBr_BxX!9FF{0EoyfX?@7BV*Blx z_fm+>R=lTKp+iRp$e%QY)uc7$n^!!fv;q@74E!?cjxSZ`ymmtFW}m`f@i{s8NCp$( zzu7i~%e(k`A}uMr1sM@pT?{yWq#tP;G_lv?5HQP+cUY9UhO}?$PJ*Q8I?E4o(9!$i zjH<$2{Vy)$B{$M8j0m(yor~d>G?F^u)=x3anOz?v~vk*rV zYD;gW-N8fQ`K!9EN9GWS9G!Ra*;IY(XRU6Wp46`ek8Qo_`AV+P$1xOyQ|?kVN7l(1p&MCF3m2CDsB%#cHa zX_?z3vgM$>KWJ1e|1-apGDJsw@b)FZGM6tqPkSGD4CnxXE z9ch|Ew$eF}KSYBWcqaMW%EOY2OHz<_ivTfQ2vKqCUD2J1NpQ*PyfCHx496U|tPq#R zwrVRS8mzwOKG@?- zrS()|(q^0!c!f93(waw7;`c%|UY3py?a5Isq8CsF;|wLDMs@Q?`TM4H_(c@HXLGbp2b2B)E!l;ouZ z%o*kAVlTzh@PrRUNAfo4-)jn=#W!@>z1xy`4Pt%X=V*3PWwOOC#H_Hs6Og~M!2G`n zN_zpEj)2Cbh3b~)kx;TlBM$B%peFPKW4)&9SMeP}efr+KodK}t{Ds?WNn635Jhg}w zh&yb8TYxD2BC=89JaSDTk_4*V?zJBU5Pz}jpS%C^|KeB3T#q1Vtt*11 z`mh};>5U4Sh&>YG3h}9Xq)7XmZ+u*hAEvbVbU@aNI!RjhOyc7{9NDcVE8q&?t z9sJg0Dw8sGI*U>~y2=8AEn31re~5U3wFU%>ijoqfh4(UziT`NEXZ%W<(|f2UHe zB2Clim&t+5!>*Nvz)W-#C9&P!5OYLkAHWOT#nqSDYtNYwYgy}&qw*g$oU%*r$}!7m zniPkGNe_wbl@q?!{%u8hmY#)i;?rl15<}ZjS&(7GJRBYGhk+OP8)h4m1#bAj6V^VK zk(|vLDluNn{7@=13>%XqS+QvVgOH#XZZ`D`#9QtH10<38&t@q10}^L<-;d$fkc+vl zYTE-_zL94o!Tz(=>3^=xv>6$|yg+nECAwKXCl9BBRB51*n z2Au6{7W7F4>lvO^$hBhiLkW|cLrDUhk)=INw+i7cPMTnKS1qBM|3(>$goIhh!^%>%-!xMHd_3$ z_=+Jvto=~+qML(N-k45vQs~BF>d~vAw6T?I~xk?kd9*sEOt61GeM#@cA;^zbXv zBF90FIQCbc821Af($iNZlYCgkyPCR-cev*f7K-0ttErX~?BRZ6jVE4pyYpHpTSF)V zV~jwNVvg(7c}m)}U85o^OeKDVr2uNZ-3FU}{st)7YaNykHGbJG6`wP^ign@l!SM%2 z#%Y$jw+e9+Gw%YgnFwo(dIFXK2T-V0t}ZEhKZ-8K%6b|I!HZnVk5&IO*OsYC$HRQg zvWf%+)l4#QXG~Hjw<_&@43e{qh=;+X`j*esV994v_ra1jp9AC{GCtzSa*Oo=Vl_+w z+O05s5gbh`XrELakf5kxr91BA_GVbkIP zf6?{~Z|t`6eU@aL|9kt>Fg7hP)k)rQMsyp~pFKvITQ#3#gV<}Db#&sN?l&;aVg26$ z&iUPn*Ou+81q0YcBvJ`Iwm#+9^M)si#&~3r4rWZY%c$75*;MJv8tTnRKA@ws+mNKj z^TKB-58^SDG%yb*JKxO%RRIUG04(Q{d;K`LoEGrzr;q^4=*g23p#B7NY(z&^JnU1# zY{BLjcgye=^e9~$?%p)I`=sF$1Ol8|!aB(*%( z)8Xc4_vn#Q&cgz#Xw$_O01t1nNLRm>skLJtHL!^cdU82UqBoC^i02bv=LIi!1rCNX zV*Yj3Tl3?08H@<1sor$NKr0YtJ^3$-EOp%bU^VxuAh2B3!n2GDJos^fYpTZr5%!|QRIdK0IS9$*tiZ*)qg&}$2hxw8bVNXwJNUW7pV&Ky zaW3nu6`B}fqfBv zvrPo=3^NJWPZ|2`b8!BM`7Wi!8nJ4)21@OjwFPtUn+rMcNq!;T@5=c3DP^D_Y2F(o zpfQBHQ8hkqNt=;1cyzsEo}{eQK|_Wte`%yiN5R)Os4gh(z-*m-e~=@a%r*Ll6$oEg zVT9h#z6Org$gq_E#!C(?;@kqJWm@;RH9YR7t{c^S+0u12l4Eum9l`rjsW0kr_;>=O zmC`%Gz|<8xwW+r0j}Bh)kw{SGj9%|@OWRo?UPWeIuPY~spcA1EFe!<9Ny45sO>U~! zMtoKCrXaGU%`B`F%BP_}crL;m$4A%ssS?oW+%GIIm`e@#mV|hAA&U>36$81ev{T@f zUP<^pt7{6xL0}7v`4igbb7(9qm+s%{rFDWYU!CW=I=N`nRWtP?54U0Na7H4zPd0)a zH8vHP9nUY~T!_leA|JA95bR6#VYks=8#5}C*^_8XPBfW;E1T2 z(_ar9j#5zSvL@)<_&oE{VWX|4*cYbwDb`<4>!1Qd!wJ=T9uoZyP80xRV*J;N+r{vo zKu)edrFKDz%>PPhEed9=@vX2OwF;2c3;FYZ^WfDSvQ5MXlG<`YDOJi?xDjf|fn+*xOCKc*_?c zT;n8eXjpL~Lp+Gfh&R6T%4d8e?EldwUAMR%yt3#|`UZF} zb*vpORa0(D##2r){R0uIhZ=6QdH9!V8Oo5YvdGd%$D*B6W<$3E2gh*j=x03R5DObJ zC(klhjo}}%8EZ5nFjU`q!H!;^*Uh3Dug>+$+UP$G5u^+>xv{B)09uzVWd6pMc=nHR zc~M&>!2gIi{1p}G+(Z^9TANYRJtmk3qs4zxAw)J%$)+Cocd^x5CAX>sh`J+SbQNOj zazNrX8@%Zgit7{Tu@n#ITJUwBprB=8Gv#7o7Jh7FNF{Ja)WrM?1 zvLbU?9hWz^?5>k_{i>B3QKZnB6G*B|MiiwWTr{`y|9(bsiP55kiDz+q_t7q26RLKu zVePHZ3dI+_PZ0&~;}Xh#K>b1%9rSkG2t^?#+Qp#sYwF^47V%qIFv5bJJEb!7-=dqV z=mym7``YMoGE~l3`MS#`-%V(kz#!Xu&hB_$j4q+0>?TrHs=?=`d6w*xs&@m%U$idI z^?*seXdsBbL|S47hsc`Dw{GrC6r#ZFQlakYeunx*&O=DvNyGNgAjfa_;wU~H+n*md zo)+Vxh~W%dZJ*<7)U*(tJNp@iIjOW!kb_!luLni>9;H%GO=Oitn zuW2Q}5#q-F$aJ>$4Kq#ut&?&DYF$aXY+={qv8Vs3MHGJRxE@r2z*NO|Y+lw*@P{G# z7hnT{Rwi3a{C#Z;rr0i#bkmlw8$Jk!vD>cO{r<;pGcr9&-7~i=cMk3p89xr{I^MUm zj$o3f`m;W1=0bt4h4^k*!;HXw3X#Zb`?$Ad1)~N;^hJ;E1P-ixoeA3*o9_#y2Tnw$}K=g>mI1Qu6(3(-;?u#Ky5N67K z3TG%XT4)JN)`&u{N^1pa1fx*)!vYRBQ&PyAc;;zI*<~swyOmM{Wfr4mJ;HA6A{?dp ztSC3lt1w8H7*TJ)2T>ti_^T{C%4XuxF!A&I@!J#DAk$CW9&>e{&LzPDwG9O_R9{g> z=F8I_a*YRumVBraD?G6&;mL%J4F)V!DAH+zsm}bcOUmmh8>H*|yAIM&955u|P7&O^ zniO5(BIXcoxJY2_JMs|9V;3o%;h5KHf;3|eKpJ8i z9ck>eB^5(lysrsO&dHUtfkk+a!D$<*cE%IsvDw;YGv?_9IrqpzXboj*p_-K$(5=l~ zLBQ-8V};7Yq7U0dxD)J5ISZm}!NtsTLZF+{7z6}dc$B+2Yo0|oKq*s4=UZB)khIly z>WpwyqNJTm_Yu>DIVqeT5(Wf~n2`}u)|Cu^{4nhzQG)s9_M}qxc8SU^#Z0H|e;0;c z4yS{9kL!K`JAnG!ZqsG}DqHLg#Jlz?;25K|tZVHW4{ORx>VdRX`|2$<*XOn+T>GY^F~z)T|Ws-nQaoJzU@5!?dWpdQVq(s6}Q7)0lN#vzr%RmQ*m zPMMosy{ieQFnh;pQo?h12j%l4l%4IonEZ299u}dCX&``N=@q&blizi?)I$D#F@}Kt z=K{R%`vv$23YcV2saGqhulFIR^XJ{{rq;{X%uBN_9z zjB7?|s6aY{6#|e3lGA;`$~YMvjWY4PTIpaTB+xPcxubHOGJ?axx1QA1hY{;+AWsE5 z`SnAUmpuK9f#x$pQRKjdvuhWp<>CpA!+k*Z|8PgRVL$488LOU-JXD$@V75x`s-3~%9 zA5Nz}bM`DJCRlb*d3dakgr-30mIl_Reemg;?2E0S(9_8E0v5ZbhQ~yCL%6h!J%DTAJH5tQZZ~|My#Xw6F$)iuU@AOS=SVvYkpK>_PV{_xM8lU)os9cxPr(XAe>Vor1yn&~=sq5NM(Y>eov{XZtqvp(ibNIdpe zdF+#~>T83iRZyNn%!)loiP4+2`hR4=*did}NA%}yc_s~J@G4d9g5&x@{sB$Y9!4ko za+&z))HEgG*+}!BZD)(XsaL~fxY?YEbJHj)O?twa*3-`^w1;=# zClSZ(j@IkZ-a@BUtC$^P3+!dSZVhqm*(YA1)Zt}X)2j~~;nF7@{_m2EO=X}1`;%k< z?vF4`K`BU?mU8n3fGn|t8#`;@ZWkds=>YBHo*;q#Cxsk-nR)F22XXv;RnAOz7hi43 z*`nj@7~|o+-lIsw=tlKppsa`>yOAX$-a8uHYeuO)Mij zykB987}A2--d)h}c+-;R|A2gUq#FR}XzQH33EktC4^m6NSLNr~@XMbykwL*`A>(E- zT*j#1VwA!2GlAKONB~lq>%abSFr_Frs%_eYQ>+Yv1lr}`Ad@C1RT;;b=2-JCbllrx z8yz^aXtAU5-ibPiO-^?7SJ>M;=&F^W@2YY@2y7GTAd_rKWngzEk`q`2wsYB6Jg&(# zhvgj}Nr0wmcp&Hl3Mqh~^DF!I8qkGHXwc3&)cauBU4I2M9I0@aiW;*!>i3(yw1fSZ zOo)c&)AD(54>=Xq&C%&~Grif`yNxGm;J5(qotP_ldIQK_R4;~9vUBqo|Giq6N^PBn z;6;*nuZ;)sKtscxY~?|Oj@Ju>`)dFRzMIdFV=;ilE_T^(653Rtes+#X76-KlwwQ;VM(T@-H7fiKpoKyHL;T zJ`QcJdzs$mtc{8(2&^VfkL%P|$wX zu361JM!s@xj^6nd#xd+h5}xPo#Z~wkt_XG(I zP?mxIZ5QMS4;>)-w@ZLINP^1dq=1ZPbzuidE;5dU+Sd$OEeg>=>}997Hhi*w35M_3 z6tYuU%c=$Q3m27eP~n81oX6ixKtVk!Mwo*)WPPG&?J1vH%GFcV@^|Cdi8s+j7y1mJ zF#&VV;VN`_Aums4zeLG;L{o!J%J08u`;ldg2*B9pH!0ukX&PPrunReOx(&~=LnD|s z;wKvy{^4jmgYR=M5C^DbBHOzT>h#mNxq<9*tIrLcZIfR@^mVdR=8)6xAg4n2$BsD| zA)>{Rj)P=Z^++!kpQDzbDfv^g)w(odvwT4ciqOLQMQh_pZ-+)l!BzdXBIV$Z(?xtz z)y%@~a1(@nLUzy`Betjc0NTm{o!Q3kNSf8Tf)y%HhU2{E_gFOJGq%;7hr2L@MV(tns*bH0Gn2VSOE;@l4jE;z1sU2^St`x2eP5)@pjkOa|E%V?A3<5o?TLjvYmIzMa9>{ zZrf?bkIr|U8Ljsdr{9oQB!!oQPw~C0LA0Yft?Q;OljMqqs^ZEj>5Fy;^g}Bi85CuS zS2;ZJ{C9Zkw@wa4FF32nsf3Ab?Q6v4O|H<04uXCN<&cc1ThWlhGqwC9f=a3ddyZ3} zyxnj3lTB_}3W4aMj)UHd#Ew(Ff0Coavg>3{^BwRqQ8-4H5Z02yN?PWq(`rNuXF2oI zjdz;_p^?zDcZPyTMW!o&ps2`R&KRj+zU_5^M>`lV!Y3G+H7r0`bF+UTGY#Kxb$K@t zodr+H1liI!!%lBR3nRWoNjp#j(?jK(No@c8XCoc__E@&DgsObI9*K!I%tFG}L=Xui z(&06WocY3;_0L!Z9J6h&fv%Q9Iv0hY``T+CdRdS8(24+O_*9@a z3vw&jp(S{#+R6wn3?b_IEmYM+O_~2_vtj!xQOUwM4cnZSF(_cH;>87u5}_uR63zfGO@4# zq8QwoXgY@kn5Rji;h!yVyur)L0l+#QK{20q6b-_|`_VYdV#kGEa_0ofN2he4TRmL0 zmg(1KzEOAcK!m3|5_KCgVxkmWcvq_J_}4>97On?-&p1l54{{Ef5n>^LA(&JfoO3|j z2Qxy@hm4t7@>uh01*l@$vlwBX+DhgtFx%!X3v0WQzztjtup>j<=-X(*5iFLzVXrs9 z6A)8=QUarhs&HbIK)S-QaMbgqlNFs*bY(PHN$ zd;${4R&h>1;Zz{(W21dE)Maiy<>*~tS*t(d?~+I>mh$?f6}Tv81$5Ugk3nM>_cR;bVo{ucznlRGHgjLIWly7H{2E!Pce4WTRt>1GEQ(VUzQZ;2w zGn{x?CJKMD!B4kDW?PW9HYObF-+L<6PBfH3u z6;-AB*hOy18qz}p7*YL_>~X{f42SFWh*-|0qMW-#*De7tXF3BsTNF7V$#R=kCHCV- z9X*jJ__UA#pWn>Frb54t=8ZTVC)Sg`Q*p*5b zUtSYWFn6lLls&0oF?BNr5!Dj6cxo*?OGJ!D@u)%e$xmKYjQdl28BQkL&h7j4iZY>t zb5W7~!6%@0S(ldA9tt(&)j#9~9h!h{8hT^pV>FE_F@l(e+Eq&BYR`I$`UkM4AjCUv zWiAPdtUNL&VfAKimL1i+mUD61rK7PZTS^h4P>$)1MTn+nyctzgzERxb-$dR?2ZQlx ziOwv8O|Lsa_p&Q>ReK^nrJm$If2RtK{rXKlrt6xtLeC7$;n*$q+1AZr2ac0!??8=W z^_(=Jo1Gd2rL~3#5neF)tJ=IUg>^MQ^~MbQv3KKHv@+OFM6I?Sq_qyjmPqj^c;^Iu zI7T592DEI}4k1S147v`CsTxrX)8{$_hkZ_N-p_Z#Kz`NYpN*heWFPgy+NerVN{mK;jOev2^gpdL!X-Phj8>yIZb-1P}|wb zaPDd0O}`=*isR415Mu&QL1~V42TlEw%SvZX!_8Sa*1i@dsZt3uEZD9Q18%{V0uXSJRz?PptWNEp-2`15nR{iN+1x~7~$Bw%qR;hLQ-hZ6{q89aKK*9 zm$&}$lF@sYVxeJbg?m~dZ=f9XJZ$@cB*lC%Fav+^M3#c3^6l3oH8N|h5D8_3bltH? zigxAX4n!*aG~pCiBsqOsW#v1^zt+IZ3 z3JAJDSQe}CW2^@#)7!Aam~AR}zJ<2?8Hme~Z`w&r&kd-{rwRaM)Uud zB7TugCO?Uq2_1izbsJkH8MxPM|q`d^(ow#59Sz9S~=g#%@!-> zHp#d)J^Pz;(#zVdFziBnmcE2Rsb`A8$I~LvaP75V!|}v!h&= zjC+2lC$f^UH8a3v6HlB~r{L9NP2Rg5n&nMmB6f;t*r}uUoiI9A13zWCi=J6|iqlvj z+Q9M<@QIPP__%`QV|ChaUnmW@es$Gjr?mka@V{4u-_|)`y1vGG?BE3X!}X*RRBA$y zUEJ-7@ERKKWt)C%H_l@p)ZskFLr}Q*GsL6o3$!bFWZHXLxPX@bh|SG?UekYkKlY)i zntJ!t!0^;5o`1TDFNAvggieE@5(}21+U?|ZV5Kkr{VG@*LMH|6(f!j z$R#Rd66!hQ2T`{Q)z z1D+F4^$&#_ko?q_2pa7}F1A#tNuYD!&e6R9S2`Se(GlCz-+MJ!?Mv(6v(D9-dYp&^ zkk6FsD-~)Koz-OFblh|Vs}^M&A(Y_PoL+klt=gt1zxfd!FOY8;MyjLip2@D&g#rk` z$;0Dev?Rtg&nelfy1FOW@O}R;4D(g8vxpTk;msPA<@aX<>eXWf6O`A%sZFTVR|BLf z#Q+A!3FICgb88nnGQzBaElCO3WflFvjlpk`wmyX{1527-z*xZl@oQg+Wi7FAY!k-V z8wH=%1RyZj#f}CW3!v z^H~K|Jl*XnEr$;1TOh6=&+BQ$?xEq47W}CbMpS@v=txoPL7^aLssdW{*3K~4cd~No zp{xi%wW)NUo2Q#cd8h&gb+@47XbcbfvV#6;40VI;PQVaES6-B!Ui|ZFjt*xA=`?_l zoR=jMmOL5*ndo7n8c`e{eQ1t-izk ziNkjc7pol(M8Vg`xN8L(8K{+T;3kC_nDL{kcUII!VPOA^h&oSJt06m45@(fvLKK18 zQq=$gq59%rZ+(O%l~%{Ric94%9^;_B%@`LEmh90v`h{Lf^7LX~9cv)Pk&MD|`zSlq z!5ueT-$We*CG2o_qfkat?jkERF=rI62Ey=S;~za2SQQq>=D(0}pceIvSErp0gik7m z^OlLQX1NW`@m(smL!hb;N=a^MD8^}-XveXoxtL?2vmQmXemXQEq#l*R7Q|2tU7kVcYmirb;s7Mwl`*5p;@l%MtF9#>6)S?~eAu`QdI``F**8=Vv6{Of-C zs-s656t-3B?1d?S`$jc2~3GR~ZN2%e^zu1dX$UTC}7 zXcpsuD@6#K`Uz+%Nnz#o7oyrh)dMu)6bcRtz60A0?_15NBJbsqYUPQVC!df<-r6@u zggVP0bVKGDg;qh65kbZ0Z$QrUs_OfYn5+3nV;ega=V9B%_w@ie;QKH2L(2p@G;n2= zfJUMWz4yzIB|FB?=dtMI6<;qH|A8MPE|GEb>X=xg&S_q`Ny8)iPa!D##l4WAr8u*` z+Nb!ZYcTr7b_E<4wIG5#@YN z>EDG^Hx9B>ZF-IsnuG2|YzCC4llDI_t2gxe{bbXf=206#b?No;8Tt|`bvmJbzFxTXp?0(gaTLLMVfAQfhWREjJw2)#)h}rY( zB=<8-75`dp0-6nE%cAKeiy~mP5KOZLNp9gW9J~Y$F_8p(-O1URw)yRbPnX~UF3j6` zG{S~E1XpNDwtmp(DMUpe@?8#!WZy+@X26V%3dfa|t+Xg3?}o&gPWx-^Ze8`6-$??S z9(Wi&i*Oj&tsn?{+kasytjo$6>p@G|DkDonV7UCx%qsg+4s~T^p=;x8)A>e>}hUv zdocsGeTDI|k!ROvp-2jaX}yZ4$-Pi0rU zVF_g|r%**_KtUp#k+Ipah<` z)t5Q;a^>x|FmZp6ab7b3VBc&Kf|D||Fbc3qWnXaM!s5aw766onnb!6PIE6riX{ji) z0omy0Z;aKDCRA<+IO_D7{$T1B>$S1JhS-Hx3?;;NaRW4kq}}6ogaVV?k`3OV>GkJ9 z&Pd2m$})GKy5-!1tm)~d({ICwTBipT|Xk)69ax^TKGK+Nf!TtqCUGX{7TMfs|@M%GYu);ux%QT zpVLtYYPoTAWu{-9!IyG6Ei3(}S>k=GdRr-{DWh2N6}0(u4v=6-Zw62oO)=VSSb zk$u0peD`SVe+GGDpg{<|y-xhv*0op!-A}F%ygJY94v(Z0ExUHSi3niHRM*@xAxCyg z62F-M_!pEJNaF9c*&LFNU8%v=10EcsAsvq^w7MX_7P>HB?G2-A zlYv%{4W&VGzKSNF13w`S1>bK1#dgmg-M^=TVDe4OiAQ}EEmb0zA9_ul)5w}{*%3K^ zKj3Y(T=mrAf&ETV+bhG4;y>z)Ww}pGryJg}uX9bq&&JFsMcj{{>NB4N=ifmuuUck2 z&JzOaG3U-ZE{|5F+{9v3qYlouU2B^2vZ6#A*7}CQokmH_^U=>;w(s7k41vxgwJq>~ z!&X+mDC^4)Ts?D%eUp;_#{hMbEVmyp+iBP_5Pl^a!3b9-ThcSE3_7lGU?zr@GfNH0 zR9srI)u+G7BUhd=KZKPtJ3paKKb-Jhu<5>-40+hd90;F)d~-232{1^uBdjF?xDOoV~uWZSygm@tH4R-TJLy0eqH~BG8$_Z;6_C&ig&6 ze925eW+E2DSy){3lJ@BUV{Z+$xCC3h#pSVPI1yC~uTXNDii*OoBNr0h%88AW=u|i~ zLpZzGrbvV zw<0%<;~RDp!2hDzGaqG^_Ywr+J zrCfr1-?GB88!vZ6Uzpd$SPl|-@OZot6Mn&bnY?Y2p|TIqCDYjNymd3zoK+fX`hsav zu^OzFjPB!!ww39}6neiJ(}Y2x?Cwt57uw$`HDQ{`v8T=4NyB-HA^)uSm|#A>wrW5` zs0bL_8tYwm8=$iKarI_I+;{#P7&gZ!CZ^zoA&&m*KKIWQ+X6KjxhUrCeFDOUc!jA% z2+a5UfQjOoJ;y5TVRIO#8SCI_J5hS0|6={w3dKt;Crq1|_6ul7`8|FieK0MVJm^ir;6fb~zu0(}@85)0`PZcmVE z_X@qGc%=&eoG!}?rmuknkPk#QaGGt17nH5Jze4yHxTP6V2_5lmWsP0mE{vC{q7-xA zd9!H2WXjnf%mZP25q;(mS|JY*4>0E^->N~ZV~#>EN8T0AEU3Ago1XfvKw$50e5gKj zjn<#xBYG0cHfV&DS6gW7<QpA|*-XR5od{*2=1@1b%oRIj?I&+w%ie7mvv8 zDhz9j))*@eAI}oto6XO%GS@>uwh)2q;GRuBn&?iNEqf|g6)9C^zQ=H%|8T|5Wb&BK z%B3-0n%W5jiHp<$T4Q>%;}a4tRlpLKk$cf;tf+ZiXGl47X*3!vYRoG^?Ndgofv2vu z%IVFmu4m6#9@eo?s3kmVH9{k5R2?f<9!&}dJpg^O^0uM_l^49aA@-{hZBQ0UI<4TdBK6uoDwkBof*h0~Wa{>b_votgHxdRsw(o(cx7 z8{Z)&l%v_MD$%sg^CikzP*czK$7`{oW z`CsWJPC*UFp_7|NYvR(>L^N|7=M_DN>4U5UKhOw{sos7I3x>(r zYHx_@qVvw!)#;sSq_BFysaRC%F@U=|P_f8t@nn8kIaacI5>6@mJZURXo>D6GJf%P_ ztLfl6Bv}s-SJ4!381i>8EgC1Y=3K41HG&CO$JC*Q>J<`*>)oSmD{rz8YZIsTsC%Ht z_#cEgs2mn`PoN~#NwFQJPA@Sq(>B@yO%~Uhm`TiT7g6z>Mj#D`==-!>3K*9PF^5Q^ z%Uu1*ittqH(|*y-FnW3LzTo^qnd4<|l2Gt-x=YYQgG5jI`yyd=yi3f+u?a!g%O|%TQh>;9UliulAc5KT8uwZqJH3 z3+X!k0v&JNxazj+hDvBn^|KLY?`|A<2g2LxHKifAflHwwT~=8vXgRw%ICT@zV0dtU z9yQ0lS3hLjflTV<1*xF}w#ZISqI>i*nUJBJLy|Ip`=T4huZ7ZCV;h}z!@tsKEAh2+ zVIP?`G(JOh>W8}m<_Ax_sRvOmB9V>1wug9~cgMHA)xIbvQ>`9M7h+{-w_kJrjq_>Y z04-raX{jI!D{gP}YK*Z5NOaRMSXi!h0&zIY4jIH%&a%BKEHN-kWXYCc{ulCm{z*V* zUq}jq)xd|@N*{?3LhA=^|A31Fea$Ez6WDb_-r5FM=0lK8tXDZxYyM6neEI%WV6|}4 zxWo;vLrz=X7?igxi1F6@jga~OyF4&q63 z9C@L5Vf4v-8rx4n+9g3HJ5|aEJ9tyG0}JRG>8Ls5P2$3c@lf17Ac77XJV%(iMmv{5 z9<|^h+MH8(yucY_1fGqv6DzMx-rT`?wg)*bBbT`}?BA0)B4a6|&aBc)3uoDp4VONF zk5;1v&B_6H=Pw5klDCw&RXrZ{GT!zmL|M4DU(=tihSY%fkt5y0_SB#bKD^`@pUiUb z(*i(Tdm2193@5I`e~K$p4`GO598LGotdVA^V#&2DORTWI0(`a*Ab{siU&DNnR#=j1%273$hVndP= zbi9$NYO}>zeo5R3kn}p!;TLK4q8gldPjZK-A_Go|$*Up&T<5I>*gL=t!y`*N_F^h3 zjBR0WHRSpMmby9ZLAyY9Ff_mw9ic4(kzHpC`!4i4)BZs|# z&r(NQ%Q?J2 zAyAA-Z(?DG7~BO<+4dmyTfu_@wg}IVR;qD&T2HAA9@73q9nCyd-B?n9Uj#Lf8t_eF zYemTZI`OF$C7hN5CwOT0&Pa%8>0A!h`RTzyKe1E*UlQWk0g``I~`$h^Wng@XS_O2-E z0PhHPjXO;s*jA!r*c~`kQ+;j056w{cRlyf7hPR+obyQZKySNv{1()<(oIuH=w=&2SK7s_fHpEM z2g*H{A7hD*<0zt-UTiDzXIv`Lrf`lQjPPoSf*(koeplXj|4J~i5&W4>MZGB5Wn!S_ z%7}>c$^v{1{@Ba)clWvTwvGf=DyAl-Lb#}$!bmGEm(2{Z-Zxb$XX`r?!8IsEHxPiS zH-vv9?TLf({nXrmxX-qo7Sd!QKq^i>`Ku(H0T{+#*!p8)+_sYl7J=8TqS7c0;E`2^T>tt*IZL9^#4Qfic zSCRt|cXuLaOP`3v0{w};|1KtNaOAr@F9djclwUgS4e5?dCxo4M)HH+P zUs+Y+mubVIUJbT3KTL`@jYubapgJQgqhiuTSpD$9F=J%&g|940Sc0gg+liRgK|kqA z&sSMT!8iU%o^?gVllWjA8W{5bh3!yCc%Rh~sy<~eVb}6wCd!aC$5cGBU8{j(ja~sx z!QPBB&b}QeEI~CUnN;tsjj>awKzi$RvLiXDjq&bw z`=j_)*-)PQ0w49uSA?qAfw?F85B4MN(Ey6?mygoeHka1SeMTqG#@t7mAz75OoEdMq z*x*xM>_I)~Y?xu?tLuzLz?*yu{(Vro1CmJ8H5N=Siew<`)GDRSn{*v_xB>0vQ`!V~ z$s&I%mj(AjTyvAdOZA<8TDfYk6+BA}AHa8^==|pll)wB31Z?WMz44Z8ZB#PDZjGA0 z2jb4XB$-yNIknDzbsCy5XfI~Z6ZWErn2gw#T)2WMwdutQf#N+LhQf<^Zx4%`7<%|N=|lEfNW|oY-A^=6 z?zhz#=dL@Y!+?(0t(}gCn&fi{B^`5jzIIyZ+(j-_nabwGhlTEydrUU76GR_r_vQxB zd)(t-zEgck{blZmz-R6$d~i{0uM=Ta*k)rVDoTGfALR&H;}R$P!r~=`Yh>&0l2dv? z(x|y6TknXymFt8u84-bB@1nJA36xEK%MUeG1!AdBC_wo_QuFrz>P+E+=4K0C@~O~q z!SJ4nbsJo3yuBis!xg0-8?g6HpakgD_%bX1Fi6Vvmd!W*;)g~m!e&bWWN_w zE8|ECx}B^J9gy+DgK8{zid$vPjH5L2hd5>&69jT1r)TlIfh(E0UU38@s@ZEbTq^MR^4|^z3Aw`1-oNRiR;Z&oSC(7EvN!OPw{&%P~J*B)S zE+L$^azxu>WpM@h0Hr|ewZ~#Q3r+@#MR2={?*iWb6TYE)H19gDomG>hJWqO$ILf&7 zQnhTLJncifTs$T^I$6fD+__!)r?Pd3;=v-&&whgFYz-?9s+d+D{4m2Kq2k@VAhs;2 zZ&#>&;=O#X~{0|DkhL5G~>iOc*@g`k)Q z<5sB~UW|-gOGo(kx92FU0iutfU+}Vq++`^I}Xbni}=rle=zndNnK!_1eqvYCRg zJc~c#|6(SmK(Q)|aUo{u(+OBVsPe(JnO?b7%PB-bJg^G082>*{5Cm#i(lUv7VV^*# zuWC{t-5(F+IXrFr2Y=lzPY=8&O#LmJ5L=0H@D6shE_HTus@7f3YnGZPX8M+&Xou(CQDpm-V{> znI0yqau^F%baZW#vXZL*!~Aa|deGE$cypeXdRhL!nkD&#$vg?}99VdJ<_e%aOVA4P z0CFWHCaAo#WLQ-pvwZWC_gE@~J-qT0$P{K-sU*7N~Q zoY*ARx&-h8gkdD~_~V|iz)#cIa8Z0qr<;%%>fUV#Pd`>wJHf|vFM^6w{&|*6`=|DI ziy8+%$#Gk;LSF_i!-7;8A)AczlK`tS6-Hj);yCOQCsk+=uNHvUZuiC5yuuNrCdzNZ zfK(qj2E60>XVQa^MV`c#&T{W#FGWM~!J)J)25G&7Dn+iP4$K*YL&L*74v~5PXGmg< zg11juUR)75{m1i2mRd(n{Ru`6Y+bXD1I@Ds8{A_Z&makM4yc;_qRYHjjjgDy88FI= zec&NPy{w-J(s;o}e`TlBefJ0Mg-hnEJ0ZU$h8)F*7^RPi8ygz!bdren6wGvZYBLm~oJUI$wKZUnN*QYkiu1h^0$DgIxPoLT&-s20M8psPd%o1Q9w}Z9v&m(769*+7u=3EaFy0u zRRhrMEufpj-6NcwA)p+l#0_1?A6L{u&3C9sh-Tdd%(x8JZq6T&DTb&7IE4!~^4X+f zwYHgmkbv~+pBoQdyjx_fm?|U5vH2zN33SvOem2_2Ke_$%=76LJkc64M=wq*bD2JQ! zA}UPr0Tb#}4|j{OPlOX_yH0#w!=+&FRSr6(4dgAf!PXsW8|FCK`K*2$_Fn<~!n|NqUSf%5l+(AVE z28ZYJ{>k=)X?9N-Un0|x+nw+m43TREpI~3}n#LqxIVR{a7_SiwQ|K3wJ3dN)7>$E+I~(5Vns{>9}Bf+~+~5 zdzI>cwjm3&B558~56x-px;IqXNJx5xDFvv?$+)7u$9S<(?oE0^fIj^tyRzbui6y5$ z{(C)|!1OS%Lpp^8oYWY_1=tJ&_^{$PXy5u~?#o5*r7GO~m2JVms*iYKMNGBc&(~Yu zwW;%k;m3Rd#4o~%=Je&|=x%r$pUUcGR(>g*V~o+xo}@RXW{i#cd#2IIeu0)u5I*G{ zWo)?8?c1Q=LVxXUObD~^YmdUnx{SIk9&lI{o#(E05C*cVV@0kO0yMyWv)M`>eBt_E zo+dsqVpo^rDOxJM9Ay#mG^+jOpbmIIk0h45R_7sUgBaM5mieN(hEc||Us0)`N$nrG zcg$RLl_Ur~O^c9n7@719;?&gFUK(nzgc}54*uh5-$e9qk30g}-4nNrYKl_b!Y|eD^ zYr43}qvn+EpTksP#XpLhLhj<&14uB?lmr$3*bp=|Z^k>y18zZr68YuD?8+nGuV`71 z9k$)#ZA52elOn8t06vLSxY2*m9*@eXp0fPdZa@{bcx?3>@(Tzqn6>@cZC4A6N5YsD z_?W9Fl3LQG;2!u$Gkc-&42>5OozxJu&0lTOp}SgX4PhbtB~H^q|Ip@raoF`TU%+PS zl?hWST+)d_8=+O{i5SG4GjiYB!8k33KriC8)ePg?#)6fDKbv~V7vJwqKhT)-XiRVm zP94#04-A_u`rHDmxNa76$FFEb{G(GKj!Ju&wE<~E$svO=x|QaYur>y<@WKhNOP2*V zMoJR=K;3AdJTL5dfRd|q*#kAWZi@URbwtR+St|G^2bLgsVff*2_e!CehgIk67Z~C9 zOq+0VGawqdUmVsy`S}qVXfCvL&0&UJJ|L5!m3eLwc&N!FVA$Ua71k>f8JFoOp@yCg zkowlQ-?x_nWDhNS+&_0smbyMQFPypPyN6c!zciUx@sQ|7EnT}*prO8B`5~>t;a07F zKi@@MNqch^-rkItN&=WXP~TOI>+Sh^E~)@TPT^tew>}gXKOj>zrVb`b6p$Hl?hJ7y z#iN$TMIc=aw&77Cy~uu9SR+_L@crRZy%iIBUQ#@<&C{S>7ukOmqZ8+zRH*8hV60$Q zX3tA<$}L>d)r&~BhUi`ljv@zB)`)#vwGD%C4kQ1RBTQdDeN@qFbuvu~wk<&`GHP#6 z*x11KK+T@UAa%ZkiU-zR6PznaC79jo3YQAD1rNVV=v%S<-VUSgPaDEg%IykDgW`Jo zjzF6$bKOX2pR1fxp5J3kp#cg5VSw;)aUuJwRhNiYkF(_SA5q zRY=;imR==5)fx%*xA~{@&HuVoqz`vmsFN9=5Mu%*DqiGzVIAKFh(R1bDwkVY!O(4D zVWg$cgwIo=%IY4H6@iyCU|P%qEoW~HE!1>@rxF8he&He2;a83U`5YnKMhmj3pX8Dz zD7Q+Fw0jnv%Jj2%M)|s}HG*s+f;xJeKO0ituVPuSKa+Vw;3pZ<6*2zXQ4c80R-*gh zCOdz90Jlqh_?26&4C1*mIjCOYKgx(X++rHe3xaW_v&9+^pa(3-7S*fL_gPyEW!8pg z>|yvvO#1IeLz8N5(@!gs?oYOS784gK?yT#1|BvE@xcj4noFN(e3ezRA4yplCDZd#p zg7??3@vHdNAdLyfS4MUb8{nnEffLCvanOuA9Jj}~ymcCZn3z&JT}5uZ{R^84nTvcH zG^f67SSdvXfG*Y$WI|?`h|AsTBO{e%nd(sUQv-q{$O8@-l-TZ;E4h8?J8kXxzoZ@l zpJa$0ts0=TFVqL3&^hA&bnO8}p+#&T@LgMg8)2!>F8HZ#t_x+)lk|m2IRVj2`paPq zA+@#HY{sLqH7*5Y&?E2G!&$W$SvY)lU3=omLn);afQI?8qYBK;Y_ir{)sNTV5U#&2 zLZ60x`1Cts5>neakj0NaE=^SW{6r@wz(?j#7QB+?#b$!t2jCGr9-j)HJFF@NJ*t0j zQww}NlBqVjZit^2!H?UgYJi9xFJop9S)#G-Js79m9r7IlQ9JG(q(h1wmNj^0O@GE6 z$3o~7*=Xf=e;bO^Lt^dCJ+rm`3HrTl!AYFG1<$mxh&~yto-i@i1r|=vA;$|h2d^U= zxIW*>se!FeBqYO{6|nS)OF7Pok^R0b^^`Z?31IZ+RCzt6eF7=I2l}b_Ao&DkL=jW@ z<~A-^a>lFXU5M1=^npWy4Bunen#dX=#B^L?3^(flTJj1UYwncZH(%1B*y?2!*fa($ z{JdVB0H=PH2FabYDAw5Wm^QrY&;C8{40tF@|JpYlZ57pK7)hk> z_e+iMNzEgR@UJH$4F&`-;Xt*9AYg(qCKQbeP4>9ls97ghC4k}qBzCDZgGefdN*3UR z(>|?eJ4z9D6;!87QWo;Y>_)yj;hplVi?x6Bv5Qy1B`40G&$oHr_Sq#_9w_51R5-iZ z?gkuN2p-$GI9CQ>JCquVCE z%gMX(-_-;MlZzhkIK#b+2jHLBmRy2TGjw9{4c&!HkRiSH#DF9o8OFkbB|kAITj@RB zMr3kCKG6%g=`^Ap!3o|GKrLUKqfRKMWz<0D8mvyR8ikm&7w#F_)~}YEVCGQ`E9FPu zYkskC?BT;0op)RUw=xYPNSv&KhtN)ZS_8L7*PvcsE533G*`h(w{F036H7A7x7ANaB zVQC_$n_)F@9f6EsrUUV8iVi<{G%OjUL0R3%Z>*JsH;9)}1w4xgD`7XOolFHAuo~z@ zhvU;0T_zmYXD^0c5p+v7_R`R)9;Q3T>fG}zn1Qh>#0dPBh~p?=veI%|25s;r;jRku z%E}LchFE0a4tIU<#1GTdU7)Liqr8Yr`SwCfy^faMLgy=`R)lgCkwG+Z(6G7yN!hTA z(FT|Mvp@J@+{#DZVt3u5%?REQ%OczOeaoK36{ZoNTk*z~;#pexzVnA{`p4xJ~nOb?9!7(~7ro)cix>!^H@vNQ+6}0hc$@U3| zgxh4d1HBarN~+$z-HaVv`8fe*f^UfyCRSPLKfP;p@D&@bf^dyVg9x8vbhPB^2J|p7 zExH#)-NC0;Ptr#k0E)>(7=Eg6&+3D@_s7Ai@b9&Bs1RM;Ni}dcZ(DsOCH??aGQQ`D~>b zaLzCUMr1x@PRAX{OQwG*Sb)uhAhNEoP-^!8u zd1sS|uf&rN2xpIh5op&-mD6u}7<Sf~W_GVQ9edXc=<8If4w)F0y|_ zM189s-y$a#)1iV%8JY9(pHJUGWmNr$-F`dHX3+NgyKzcowZL^HiLxRV)Q0*j;~=k6 z4B0>1>WvWvDjSnGygZ%F6<|E#ESRleLwGI(Cqy?>Z5A~>cuK?FWB~+0Vg;D6002cm zy1&XDHlQn1jETPoi(DO5a*5U@_*J{BvD=4vN4kb)63bhUJ)M3k0>EZ#0+gR3v5jKs znnr6ee6wqsIpVmr2d%BhiPn>LEFtVj4sHSjQzqOEgQh`PjD$4i;l@q`B}AC6mU4zJ zOjJmgx{*<3mMb5xQZQuL!8v|pTXnQ;-WVmhl*DLYy$wZDP>g!y8aKy;-r4Rf!(mfK zJ!EaYwe^V_qoGNG;GIQ`x2rH5y*%;`a-L;_#gGL}4Os7w7rUP)Vp3yygCOx&boT3o zfbcob_%v`oM*xg~5F|WLPzryxg~eqY{J80AfD*Ll<4mzGK?DW(^^ftF+Hml_6^LPM z{S|C(ON=BW=*@`Dcj zR*$$-}s#?~8%P8TPtGRE3!ni(V!)19AQ0iMb}H`m95 zxpEKeB5Ey%Mo^WQ52jia(zLMCl(v>cWLtQW@bC7FPOT3F}G*xw2(QN-_i-29Ktn?`E5hD-yAP| zszzb2nn)>8ow0b}rmyu!*NG<4{0h{&Za)a~ItSma%t+z^^{S8NLe|e0_AN95cEZYM zyf5+Nw@-_h7iqzQP~t>vW(+$3U{gO|g%w*M1#mA3_(*mk_*qG#XerGqCu}i9s)cKR zbiq0#op6aQqY$F-xUh?C+_49Mc=J-tXYpJkWmb0P28rQRj117?+1;Foubx(Q2K`(6 zJM%0em&&KDd^Yhj&fU43VB7^<|4ygk1hPs50n*dy@J~lYA!aLD>q9VYm~lfj@*UC}~Nwf&u-T<#4jtfg6(<3z3w@-o?lZ&ZUk-`(u(zxdS{dLp30{Xw=nIfwt$enb7 zl;Wm*zX-hF!*KCVexz48H^r{Nf) zAHal(B?-NSj}m}$X-nz&8dLu!V4l z5|S2d+-rNWfEf0!rxA)jiyEIRJ>l44;%Pebz<3sqZvrPLiJ1AOz$Zmy^p z01yTm>-z+h*XmYxf}ZoqU*}Gq=%*2j^@jkQR4q+*n6d8$)a_|`Y1({*QGZW0V!ix9 z>@)&dQ=zl2RqwP{G5me)HUh-2F@DEqV5bG8z5DXE%iaFxvo`BslfrVeC5n9{Bo!j6 zh>zmY@`Y0TsX~#f7H-4Gli&p&er(1St@cpYx*SZ}k%m@eoljdT`N zJ?ZR?#%~((M`Ln^kHm8NoM+rS8@WW+CY|-4C;nbSE;y_6xz_s#^v|wTPU7C>?yiA# z5(%vlZDG3x$=T1XTTW}mQMShy>hFBo|6unIbdI?i1AR9O1*O4Ro5SntkRy42vnfhw zvS}4${X}H5#B!SqXC!2g|BEXMXa1oQVt6wggE+2xl2{aU5B&lcc+8XJ(+1K!(*U&- zUR@C%5WU<$E=J+g7EIub@81{c+7uCJ!tH^wGf^1iGB(NU~PCN_j-2O1KXPYltT z8U0CgyEH2Ex}*BGPyK%;siT99vSoCJ$OEqk|>U%(b4cu%k;Xt z9>pi&zuh#e|Kxf@np~^9QMX`+`&g42aQRf9%MUQHFfE|$NOTtKhGe0W!j60gPa68t z>VCH!NzN)A{+wv!P_YUC+%ueDEGr1nnhUSj=VF+E-R;WMr4ZE^LWYk+G)j0$Ijic8 zc1`dG@*UEpQqUp3|5hJZuC$!>r{@6RZDP+hKatD~H}QL1r>0*_f-e=U+POzNl_aN@ zk#3FyJP;@ipQy3hzv~nQ>=y~54STa1EY9rn$^%HP`)otUK5yBi3ks^6^mMz+cJX0`!CfWxW5pQ5?qXNK?W;FBL!pTJx~=fnA+qISmC_*mc%msN z6e`?TpGx-z+|+nUMF<%kgI!<&;R6uJ>>ZM_?Y5@7|Ge-+)}9Qx<{I% zm4sn+Q`-xMpNG+vrOFH%XZAVz$Ln1*z3X`zdf_g#8U5ezQWyzv6;%teC3QLborDY?6VX zj5cAw*w}6rVUL_+)s#~U`=Ym2Y9Dp`bYD4rKBZmDeGcQ6DoJpV61XW)?hQ2e=l}YuFAFMbp4tyr zJrNt(n>z~oxu&o+n_`6jNlX5a0KD3-wEcL_#A|3!rFi0ez zmb5=HkNK7odz5UqzLqs+DGj{H+%vEvDQ%ouM*z31#{JTeeN(O6@GAssyhn5^hso&zPIDY}Tl_8@g55{wDdz1yx z*Tv&|p31WJOE6qQSB{54y3vFQgqetcpP~!X@8sgDM z=^7gvl=l}}%)nto*}vha+pxgD;;)`uxzI|d#xPVy)&~3Ndq3JR)Oir#lp*v1knl1q z8t{zkX@eLXBiU3lQ-(BwIRO~~n`|-P$OygCeE7tZd3e`Dt0avg zcYYl)Yj8u^QVlLg;C5W=Ea=&KL?`?kw%*Fbc|};gOI3sMQ^4-ANje&}50s-~9e{uT zij$Z#I^)KV@lW0A_W)vrjbObBGy*oBSP!v#;qWtgej0Q_)OUa(bphw3*?XCDN*}nE zHC?Zx)*_J0LGEY|YRswK*C!uDvmr#2OmD!%CfHeLOJ-3R~G7W_kv@$s| zDJ-#M-~@1*%^>WS;Eq8y_2LXL6T`-WD(e~zTre|PH!Oc&A{PCli@Kn9uErlKSQJ;I zAHiE1i?kae8Jx@hNZh@p??@R|Ov4VVZ8{`B9~%OX?ztFeYvx7?S;yagRZBOT)g(@*Y0Usv2W=9((5OK=SRms-BjsAo3qh=nl6V_J`@E)39^kpW zn7d`zJ36B*Co#DNFCxX#&|1Xl9D5-s<%ZCQOvLqT1l0NV7z>k+;)3qtSwhfAu$6XT zCf`bCJ1*S#Q>X!#cv>y_iM?n5^*0hm_aZD=zQo|)EY%2n_Gll?dS=m^mUpf061-@n zMxmVHl^lkqq8~5(e~rw|iK#!7vyJ<@b>8D-+)Z*5iO^4VBVRJ$2uN<3LUN_h3`I3m zKfWdW&M1A(G3cU-LGs<_Y_UP&Y)h2YI>yB274Tf|1xR>3ZoAs0u^XkpGnWAd_qGf3 z&yRm-$G-xVZa@&GfTMJQ%z8Ekr%^vaoHpZQ0i+2GzNS3z7t&>|NB#b;y)@^@ZGnJ& zhTp>5-m&a>1FdRUL~p7uan`^#r0!o+s&3i465No%oj(G=c%5Fi;9z| zXHndDLCBIc8Pvrrm^M@6{S8-5iyjLk9K@+$kDWGNU-mOM5-fqfelYcUPJFqmiw?jD z9HZSxe>ndIbu|Vh>Y*sDuZlW(h5ikm6}%Ng(H0oME^G12YF84gD^)5^8V z$H*qxf>_8ke)$nf# zEHcxdglqGNi>S}93KWA)-%AA_Dw~onBcbPIG|_Gk;N(%fTkgaZd zpR84d8A5C4Wd1Qv-?=oY)==kf_nm+&cq_66FwD?M#ZfaiMgTLg^~XW~x5GF7uxEiR zJp8i@GN($h>Brx#DhBwlNc=(yHoWkoOl#vfx&+P*%H4uv!lhSrVF4BnOSpx;=+zH{ zC1b0j{0&_UW`4C-ZSlw+-lhqz7CThrIX@cfyj{F*1~P%_CP9ieTZ*8>V*`~s9fon1 zAvwfxB%-XpC~84JL2B(0i}tr^BOAWdxIx3*cTk8mr^1?rUrJ5kIr@2kX3Yk{l;IFU z_%5Q3Z1CW;DS#NPKKXYYG0kcf8H{^g_m#WTlg_t!ac=BkfKv^I@ay*2ZO@{JJ8?a- zDb%BcE-;2aLTKqxA-{r!R|NWX(G* z?xex!s<67eh#-8rO5lv~wV=gt7rTsOZ?O2VJ)A0g(B;=kz{CYytBC7jeNq{TqGV&q zunfjrdQgfROQEDuFh^JK;jx(cfP9f5YF+%ZtP>v{pxt&bZ2FyU5IRC8!z z@AOa;$-l{ko;jYv|D?@xbKFHAdpQV8BsFnX;noP{Cd}wJ!~6?=eOJBKXhS38hNwY6 zX?KOd@|`#LH0g;h2wnCrYGAHQ64_!3k*rwouS_tP2wZFLd*1ROb0`^H1>0I~tFhwt zHG63i#at7*F@;q45sZNLf12X(KX593BUR5$Z=W32G@*gLeZNA)7H^7enAmXp9V3BM7bg1q+bH<7k}+eLYNqDZ zbP5*l5jMb~_O?y34k!BjdVTtnT2J!71p&p!WF@5y7mO1JtPVJyhU-B0hF&IgOFGW| zIC6r5zKy!RVch94L=Mb6xZg$#p!_?aJM0lS#yMZ3C*cB0)P>gle+nn6J6=>~1vQeE zxq&ZJK#CpaRY%877zG{(Zv!BMcYbJ)*kU{=-yIA$ZHzLq@cY!`mon4I7h|5F$#|e7 z!Ao(n&lbDPj72Db_aoO_r#w4mU_X+?kJnrc>J0jCbv*>*l(aq)Q^OHxr&$M~AHn9< zoP%D3_DN3<4X|p2hl-)m?cC=0J)X*XpY}abB9U0U+2`6wk~H47c3`-ph(l6Z3t>A% zAM02*`{|84RwFoR0T{_kuSfrZrHJ z=9X0SJi9bA36CUftZMCb8~;|c!IMu(X0Aybms`HNKc;X$lE%{ zd(*Dzbs{LXZ5<@r>a}1+&VeWXZ^^0GqVt7&4eC$(p^speS2RnNdC?pI{_^Q3v`B0y zSY3L7$Vy7pQ2iwL&;8hO=%Y`-=7fu!r;>6dp~c;AgYA06kyO)21; zE$|TjOj+GWu|_Nw3K~2f6n3~^o=ZmcvUIzF=AW7eYVIvFnH7V8{=UiJC9sj!YgMl^e>QfFdHcK4Tt!@td>TJKH;zLD6Uj|NbH z%mCKc@$-BEz%XED0I}f~b3PBVBBev2a=UQ=_Z8ZmrOFj_*#5jh zyTytt#9f35EKIN8$oeZLMnoXak0P`JFt>jdb#{=P$*n}jWQc8Swxrv`Hb-{aLFd|A zkpKXF!0)f^25R6fMb21es;^y+4zLZ|(-u-qyB{tIk2jk*HAjmiMirz229q{W0daM&Vy%*#mbgKG}Fs1{V=8K$gFni+_k~MJAqkO`;#pYkd@j6&3J#)uBcz-G9URf`hI)=a&vs&+hN8dGNsm z_ffsOkNLmNgy2+N){`rD7eyP)U#!4Z{;bT%Pk9nJ1T$qJV8BUE*iCbdTR>&Sa+*KA zrA&1k{%mB2tQY%$faSt9lJG0@X)&k!M&`_8e2mS2Gp`gbQdct!CQWndDV;#0awBhxa>^DC^8E zdG%1U%`iE$=yQ!+hH~4mLuw?ZKCd?R5rPGk6W}^9F_X%d1vXjT%dZ7nLMXlVLv}r< znUwGAdl-QrF>z=t8rcj&#O4)hbM*X@rlUAx&by6Sc3d})QJ2+xB@!>Zwj&OXW83*d zjbQwEd(7aAmaI0lM7@Ug^c{+)?n$cEF8_OjHc>Xh7D}_HGlJSX9r6`f>+*7{;PdW6 z^b9-|i%ZeUd%kzJem9rmPKtSr$6lPn5sQTvSYvn!CnhOti`8T10?ru=pS2uu(QpkRBQsvFR}_J{m)pK`Zm` zLrR6(GyT`S$zpXvf=`hWN)@}$EuD46d1IH%2mihDDpo5x>_P;t>cW@3a1+n2-z#tS zfDdwdiaD{5inSca2$d2jTi;4_8#=TyQ|gL!JuFFX}W znaW_uFTOVHspvXs3;W?LY07l0P|k*cno+8hWrrk6+0YgTnD+B%LXKJNYc?yuIk!>4 zI%h@mxX)#`*{xbDA3C@Y@C}gjTAw{w&H*&6aiN0DP>5@G24Wha${UMXo1^B+kN|~# zD1O9tvZ!!rY2qF$5#aRwE&9=`T;TpSaWcYW+XP4ETF7w zfR59wt3;@974GSyV3Izk;(Wg|GA{5f=kLaX)T>`L32ZBX{f}KE2bxe;SA)&g#X`^P zh)x8}&>I7^0BpvkNdR6!vgJ7_p;d=RpSERO6PDYJ=PZH(yN=cKcC+8{*aVi23lUKs zH4^G7P>hNkgP=im$c*ggI2B5BD&QTF-W6XfrTZ<&-)% z+|zJIcnjYvfC7F;)T8li?_{mi)?dl1zi40L3VB8H@Ke{x>%jhNTjcZshqLtTBki`R zy)iSBECrCMlsO|VdRYXjXROh9KdRI2ulCmuNTh*!ympEY2$trA#qEcykfO6|Yoi^D zO*4l`$;p$|va%};xxaPtdh!KUi68PSj^KYX%5!!y!{uaeSA z3FfX6fgGU2wRKb4iSH%^6A;k&07(Tp?jg9U**EoM;O(jrPj3=Z$gk}eC`R-6_Jv5G z_H>aQ|M}BQV6F!!tDAbvXtKA=e|=AxQqZ%?mMJfv%qEF5p4+IjL$F}gJZZi1te44K z0_ht9KiBey zmqul_)Ya*3Ci#jVyZjr*(Z=FDyOnI@u-iB|*sHiiUrvmF6i5XJ^e$5u04dUTtyizyW@ zz{;)kv%U0kSN8}BW({%CPen)6(CO{VtLtcwd>eI!q*gIAFSC0&;t-zNQBt778YBNn zbtR)3NYti0p`WC#OX(xCkPYHg%tTL)Vy_3UdKFXl(g9n7G2s)zMS}|Wogfmxf-b*R z0@NYxN|B;!4PV^H!rwV*7U0-LnV;?URj!5A_yf<{E%es)BX^WpM~j3GtS#Iu()Vnp zUUJ;+gZ6@FBx+Yf5?F4w52&6}6KM;Y*gXz_XHZS`o;C)njC!f^{hLVr6eDj+GjC}8 z>yAm+roi1RX*sSsIZhxEld|M}TL7osjPP6!-umtgF0tk0(dZz1!t3L}+Vhxe*TpGt zO6OQjAnTSeF%-W{eb&H!DFV{EJ)PDy1;e|jy95GDWpuA)g}Gv$Sq7=M%(@2v7zMfZ zpqh(cOh2&7T)W!qqnfl}mm9)=N3*c&BqD|`b=SzpP2d=mw%-o@Um0+uqY9m099xKy zO8q1U`o?A$`B7e_v0Lxruq$hZVUDAYB46hBNd^#{nIZaD@-lXa$)yDhF$^TggYDQ~R*zWaoNv!Pep~2S`&qvtG3hLVy>F9z6^S?Mr zjmWI6?*OUmcAb3jOI%BQYfP5p56OP|`NpbF)XVb3%;@aBfOX%fU@1cDd-Z#jx7ZS) zZvh{6)oI#+n0Ez?QV8fl#GCT8!N4(ECs|cnWR7ku_;$J$>2WQq@;D~5CBG{eUZHU z$N9@l-kjP%CubzR-9Y#y{lNNv03Rz`4bu#hJ%Y)X0Xbp7P_-vQmBiH}noyWQhw;%r zF*7+fAn1VR>O0r6f3-;~WK(c=DT!)}%}0`e1Ls-GftT_yv5-8WX5KnzC~x@ATQu&S zEib-UiRKR8by+s~I$&~6Z$P)L6`#h0wMR0Xwn!UUaT)F&D(i|KLwsE5vPGTEw}A{G+6{&l{A<8OmqYgS^H$Y5 z@U6EJAuys|s;U=cVfsh`r2>R}{Lxvnf2uRoO%xBJQ+zAwy{^E|Jeg{S6f04d?cX7~ z7Db+kk1_IcPU|_wr00Jl!r4TrPN47)Ct0yw@vt0+- z)A-~zlUL(?{RZ?#aRqb;B%q(3)4r%DDouVjndIcEKnxj1@r59Rz^iPvJ@S@Lg8sx! zT$K$LA2t8UM>?Yp!-2-Tc5wO46t>%YH*Z#P$(yc#phFeufZIDv~rRT}_@ z{rI@eC`ZiZqhaU#0cTSoAc@lDjLdMwLwObT)P4cNnnw4`RrdagaQ))e5?mQP-PO64 zK?>_LZQB(HcUU(!MD>+=Wx=<;9vEOu>B!a~{AvE@oD#?PyPf48NGad}Cwi#tFLB*A zKILlHc!LBw3thk~(*VPTIUyBeSVLGhUyPDNG4=6{?Z~ z5V=K$eSxznOibfPenM!Gnf(zAoJUcBO{Pcf!V}wUb^j$=@rtJs@mqg)A-Jq688kd4 zg<0w=K2wk(2S-60pExs*plpXgJOnko_N#IXy$FjQkTDD`#vYlq^*NL|@c-Nz$aiPH zKVtv;G)kb76m=!nUV3I`-GJWG{>qsh%p)&Pak;ZNq~d_!Bn3*JN08yD~d5O7L%sJ7=>)s zAT3^Mz^p8li>VY-6V~8ktoSP}lf>pKsDFs?bnA1Zm@uy;Dd|&RE!?Mvy4s>Xfbrn| zj4^$lXNa|Hz}ZYYN2veO+!Pj!r_Clj7~TM-g7aT_?08V!2=1A|5$}tZ&P13@ibFq~BZz9`6 z7iW-s#hTei@P;pm`{ZWw7~89rw5l{Y#EcnaQX{4Z7Lwccm9AQu#6F#tU~y|<2gpEx zFADl69svCS$7J#-m87>HR;z=TEKrZ!N4Y-d_+d4*SHr&jtxK?C&d$V3BXw) zj+OB)j#wppPi^klNAp&vQN`tQ)SH7EuuS==%mr66LA=x`Aa~9j`KVJhXShBd}wOv{2+r74+`P# zNy`JbZ3amH2Umr}A>eizbAb-0?_ZL08^^$%7IE0E=KKjw;NQ~#u$j}(vyD0PN6^}i zX@qSDFBNtvXQcaKmsb0ch>|uvltepQ`dI2EndP^4GXyGUE_7ek*&48Q;?S4e8t?~8 z^DVw?9aiw$CWE(iEl&r#EC2SNT$61Z$0?HnYpQBx#t&bOkb#Iu5F|xM2WEj2Y@&Vz zulYwGHe>EBA1#nBYMMHHV}5O6Vaa>w~FsDX(Yc3pgAW zx%qcimQM(k)%+^P1q>}X5U`5}kfT@P8!~OctV8e!lz^L<*}6%BqdgTo8q>R z4GqqIUUuNArKUvY5nrx1?Pvr}i$OLt1#&y?2u_4bLAop=l6rz>Dhu!^el5!6ef31! ztm;C;1i<6XXy*OqbiLWUPs7=U#fNASGWKO$@z3#-y@_*~UV3OJns&=8oslVnHrMFP zIl4x+SYfYR0)#@NrlZ;F{{k)w*;Yi2lT7yeFyD&P6RKw`gbM!6VLCs;U8|O5qfIb* zSF|!K+9uo(0*!*^sRKwYf909mBjSZ}`c!tVJ~Hy$rR$sz`6AGUz)xv!#JthY#b+|A zAzLD$UY}Ee2Uy#OzaqH1Q)X{2>911;*c_BAj(Y9Kegi`;%0&v5x>7*c>Fa5n@CFGx zm>m(&j%E$JMM5nS&6ao6{l~Y)@$~LJd#`=j^(#}ZhD(L^V*2I5YAy=gzZyt zm_WVWv+Meoh|n?P!;9x-7^Gh91@XpdORkf9H*lZfz6EQ7(ye1a;Ly=Hnn zPChs*e2X1uL}WDNhP6*eEIuv22{At0qvu9ITfK=sUmWom=b5*_G}I19FdQK4LSuVW z$vA#(x?2yJT^xczjKMFngu>puc{`W5?#`(PseD}ak(p3tT9nQB@>;KbxYyHB=uS7+ zkX`>@HRf(oq~6WT>d{SFbWOY-$y-Nw$>xYV&-yvck8=vKok^<6D}_ul9AIS0P}qtuI-Tmk_`RIDZZ*?4IsrL9+ZqY%(hAA15YTPTioMwOARWytQEOaw@Rd zA)+-!OzI+gM}wb^z6!0=K~sc1nVeu2Gd%_bYvUK>Vwl1z!R2_snLm^Os(;9`grvlN z^0pPdbP|JAS)-={uliJ_%-)HbXPm;;`>-N7o8$GNC)aeL?n}D4ksUr+|C?~smdQ=- z=G?tVnE<4WUbt?ga-$agacnd;y^l$_@`+I!&TUo6+^4Gm1AeK{L1Z2+ zi0a^*5YJaLu$;IM^7sO+w$r-VzS-HYUd9^}Z`du{H`W!+7(aDC*k?(HxCB0;QNbhb ziva6Y;4RS`L%|b`1>kNNZ%>H5+OhpfXIE@Cpl_+`Mh&R=`HIe4Rb!t@=U`>J3b7|n zdo-Zx-Bt7Mm+%*&WbFuC2Cgl>Rxvt`2M>8-?oAi8Kaa&PBk3CPKwVGX7|Sf><=>{t zzE?Kl0;Q?csM*5Tu_jQBVDW^{kevRq{AW*b?vaZ+Nj^!I8I=dE=Qepm1p3qrnr>ns za9ZNYp3G9~dc=!z<-`V%{>^4iGL)zHH8CgWqm3_c^%#4&_wVN$K79n=XM8+1mw`r{ z3(4uWyHD}rG~9TO8}=$teT@bFJR<~0)PMZDl78>=Ue<4`#6na4dj!|_{5Wt^Ff>X~ z+|gdhGjU8cq3+o89{R>A%1vq5F}Om#N)q8zco=`r-WgR(tCy@ zS#2lO`V^kZGF&!k#5%_>SL`-i@j6Uv?3!?hco_fKT~1|ZeW=3d#itY>peh&tffIZB z_q_^*TtqZHbsu(nkk;Z~2L?9L`5C#U>}&dE&C3w{2|NV#*CF+250q0ZcNXh(WJY!u z!BEP?3a_Qm6eUIzv2s?ShxTRG?9$&|SsH>FhfS{Z_sjObftApWF!xZ6zk`rG@n`dG z$(0JpqV?>leE}pmr$yYu1;no)SCyxPn1Z4754(RBn=!&T5h})HdOqL19piJe)f^CQ z1t4Ej4J5dM`}>$&`hb+Qh|dtrlxr{z;Gf{~i*H;cGo+1ysiYs>24Vf`JS^k{zRsG7 zreY84*>;hW4CwpE<*#k{$s!;>(hFf7pCcFs=W%_dpdY ztyVdo6{O;-@*#WrlgMm%Vg3i3U<^1Wxg*;(IRMw>(Hjo+u9{WH>rUg)th2S$cOzm( z2U-olRyZXFBJuJlld@K^1T&)|`ncldtM)4ui5zy$oc9MzqI;(Mx-&_cWQg_>DN20n zEK(}@wV$fZqp@?U>le1#4}R0^GU^MH(JGl-iO3=e~fUv39?E-S_H+bu5Y$W864 zBJl(0Y65Hi$2lV;ny+T|-UbP-zTVpy^HwhsSC8~4p{-R>gTzmZ-poiA2dEQQKzu4P z|1D$q%3}FkGT;nOLy3Ud0PC!qb=`ktl<@qZ(l8Up&^08jioTX)?d=$vzA^1us##3=u6*A>|WzmB1{s zm?bi(v%NsGBkUKI>;RgTT8I88bcy>d9z{$Uty3ynJwe|95;Z8y>Z#Lvl-g=5<{XYN zZ{!ifA=JE9cw4ve6TG*zU$`)n>rxF@O;IAm5AM_{5l%`PGCV1d_v4N|NHqK+MzD$f zYv;3WozmdoypHEioH(|lg;?~pN53D?$Td?Y8C-HjY9G6v4tEvT@RbF@T^VS;+`$OTk0jJUG<*kn>@> zJ^%rJo5>kAz?l*Mta^+fCk>yhNHE0EtMOW)sffOV{{0Q89Ztv>SeA^;P!JaI_+sC1 zJ5AFMLUqfr-8lXxB$(6DKJU(*9()LeysAvRow1;ytSZl~+a>()b&-x446nlNb`yE+ zLVq62DdD2fXqQ&LF4nUWgRySniW0~sdd;AlD+xTjybxu@l5Fr3Fl>XMqeyKpnFT2> zjx*>BfZ?>dK76j{2-9FTT>N_X?I_9Hh)Xt>nNf$sM9;3ei9P@+3OJ4glEPa1x!8hV zx|80W0j7Dl5lzIkKP2-&vhU@h{cCfr+A9F2#IN}5JA~E^(rf|H`B*Mm`#TB!J(gII zP)ueSxPkhuv+`tRKI}H4Y46>%I^Za)i7fhf^vanFbuVkgML1C8pg;94P&^Uvj{8!k z)&$pw)8(4)@ofdGHY50;^xkO!_9V4Csp8sf;wQ9#sNeoVVIsgS= zEG9-3mW@{p<>?qYouleCPo;7LfpFR^wBx&z^V2ksRi+DqUb*}C`guKRZp7Oyi$bC! z+9apo6>-oQYa<$*m4JI1XK43K19F^$zr751O~}J+NFWSm)d zIqQKoXk4shky!}SlqxX2`CEyl)+LT@m;)3!)5V!WQ5RFewr^@tEGdSuR*fKPPDu_8 zzR^4zMhe^kY5DaLS)&;+N#X2oj*xDNtu7K|S%BV%eM8E2xknCTw4clo~CZ>11%FT^#w4_IUrk5l<86af4fIiG1rhMCshI9GlI+1{> z*ssdZ&h2D}yp4f`6}`@Zi9tuAh55fD4|;^BQTz=)pYAinBN#KhGGP4-&V?VV{n{&k zcy$BpEc;V%gz8SRW^VV{qV8UiAP$|jxT{7LhBw=5gHYx7gXG2WnW~f6%8AS#X>#v= zT*>sDn-suS+wrn%D-L>#(BEDK{!ba)!{+CXw+NV8vWN0tIRQn$a#snopvaUAm6ol( z@bZa=%`Z!`R2B<((k-c_f9vPaOcif8*?@tAU~1nF(b+ ztU+f^Ghu&(e{%W7me86cHRVLqB(STSO_zBQX8wTT(cJ_ldB&?-M&-wY*mni!VEV)Q zZUqbmP=g|i)F!U#t@~6y=)|HB#5xYxrkW;=s3*AfOO#({wYYCRqCSJtR6%T!-vIP_y3^=wnHer`Yag2pzv=<0$VND&>qgINjLbPYhWMytG zG1l#E;E?GgoPQfgk3O83P^9qjp8w2sxbCFOkKA(9<~Da*jk}W^03j|JoA5Bf(fY22 zk@FQhxK}=w#@VfM#ta~G&)C?)6z+xHKD6qZao5vT#>RTALDwbt9TDGoTQa|mfHES+ zXNjPp$|*ge^+Ba0wA3c0Ol-E`8lv0ePh~~xGMsW}TEM=-BO52dKe=G}c0t(&?=Ad` z^uDZR(dr2&TSMoYe$FO7!@Rs7>sNM>?s8e-q!445*!&nIGJY6r=6yR)i5ZAj!qQo6 zF_1ypGc79l`RqsD$hhsRGqTe(Tq+aSCFWBG*$ZeR{N4N9!-GB!(GeWzELGb~o}&P4 zn!6Bk5NEaIvJs<&8E-g)?fAFG=4$fr80l`=ju0ssTlWAW_09wHq@%!4$6c#szWWl= zDX>dvJ4hRNW#@&-G~jBfH@-!VFVPN~6pD5vxM1oZ>iDu}CA~CO_(lsO<5c3~_o4)u zeFBa^6hJm;lA|SpXzhdz%?dR7xaD(`q-Tl=I6?|8A>*Rp{+Xn>Es3w{rbIOVXH+w5#*K}t8IRm7pl#44&3#whl7Q{ z@=X-0gwV{_Y8;h_^s?JYN2AUP$|a~(CKgDS5sw`@+8z^1Unp4&;6GcHlh!-c3-IcM zqvBaUZ_w(~dSSD-`in^EdQBh5dz`c((6=Ds9LFL*fVj*?x;nk0+8+rs!x_ngm1L%7 zu{8D_SonKzYldDkwLKmiyRZAz_;LFkpES=BQd(M^rOqmbq zzla1~V)4CMc-**mbFqG6iTf{z`**OOOuVsVI)$u6n`|5@LvFGZl4hA=lw>Wt^p>tp zs`@YQ&TNp#lzGlx2KbY}?qoY1Ko72~yu$u0h5w$)KhK|(BhiQDUs>83lz|o%1f>)!hEMd6ntbPOa079KKu@$ZfGhlXV0?-oj1a@ZANL%L7h)o=q;w& zA%($R49%T{%isi8nu!f4*c2>jEKJdzIRM7AS_&plEV;cF{8isq9&B%+^{IfTD$0?( zy$a3efAS?e7ZxVViqk6<05`6+BHaAa&6=igFD8&B(#HDWfNBgq1s124G``sxjG}&U zC`So@P%EU%*9!jS6y}HECcOB^;1Tw?pZ;Dig#hN(l8NL+uh9L~En&8^N?hAuxCU4( z^PCF*Q{xM_ZYELCh&^kOHe3Cj9CKYhrQD3m{zodsQ!7&a)I)Ovo3qLHTo7urFzpST z*o<3=GtrfF5j)4lH%T#`DWM6E1m20pm007Z*4Hb@u30~pUNQ}V!1io$t&0L=YF~>@ za)&vgi1L0(g_)mnEu4r zcWDP#Y^xSzTDf(n)8_9>y7)HA#Ur!c(56CceEd(K!`*YNx6CEN(y?zG^%8vT@gle{ ziq~Ig5CXJO+VH}l0+5v_fnut#o1ogT*TVtGo3CvTZA}uZ%s7WtMU*rh5#(@*Ri9_8 zxwq*Q(&ADOVL~q@skcn)lRF*s6a(B{+flz?6fH6u&kzXrrS2CAexT|@!LzKSZ|rAM z{C-V4JWdk>CX*@g%Yb!Q?FpSO=HDd8I%;gCPF(72&v}GKrMLe_{tbW=R3L=_DIhxY zrf!YV@VqVxfqeq-!3mHp~fM0q$kKE~>1y{pJw?gNQg;2z)e( zxqYkA=3jvc8N%VM_W9LFv@X+`9~etE6(`f{cxSfhGlP-k6D+LKBp8eDkX}IE*JsRa zg24b$F!qspwD*K9)R#Ntsw4w%?leZ-mUW9A;$e$Gbt6z@md$7BRTl!s)>^Mb$Q;=$ zvR)`161%VFy{KZ8DYU2LPkB4TR+3gLMw_!Kp!&!PG9X~xNMbdnwHTlxK6%5_nJ~%n z#(m5bcwaOzHbZ{_@jNbW${zRHDz0}`TK9G47%ufrXkC#~UpYo=tVMq0%2Nwn(Gm&_ z-^|>Y756x0F{}nN_Uh4#ELD~K%olHeaDb~rSwCmhJ3l^>XWQJnVy*j&rT1QS zR`pt92PHjg(1F=NW`D@yo;8BWT&a3N6Rv?K^?mLw0&{&gRC(B|v2N6+mWoi9_wD2o`fcZB6jVgcTFQbN#ye z2=VHL%ad}_GIpu?6gL_TbdYKMz?oxFrOyh>PO zG4ny}3SeY9<0EG!rYZ#^(4aL5Km;h=aTrMptWz_?7xUmoAZyCK%`G^HlaOhhe9|U% z&K^^N1v@68mS1I70v99*mGOoWF$6dc&_f>`v8@ZFlg^^lQ9l9G`eP=MghGe9)TPkl zz=u8*s0x?m3vns^uf_gF%NK!gT@tY`-X$2h9?@xKV-0l;DL$u@@#TL)GjCn_EdA*b@^07QWU0b}QsXq^be3hth|+99%2z>d2%+=@2BLKx{MVpJO)uadg)DB0{+Q%d1!L{ zy~RjeZs=9LZsqA6bj(unNsgNnmItII^9?8r4l=w_0VqQCOtOwij$bXr3aB? zv!a{RJTTO^+$7x-6M##bT;WU4Ls`MA+aaC~6EM|d-50D;yhv)^Q{-8&tvz|hv}g+F zEfj5AOIKr#Es#7u2XC)@F#{p_LHot4-mbG$tRB+V75?gF@bv@h<>*=Nfl zkirwnNX7)4Eh;Rug1$i3hkLaH~< zP-#eTAB2O~G!2FePKu|C(kO0lOZDVP zxc#N+Cq$1lm9ugr*NI$qf^-erWxR=T7cC4C)#RoRwrvz2Za0tHp=`j zS$jRjGhT+8+WX!{g0okK$Fk$4xj{4~m+B_y;1%m&Kxf)9Q@EFW3cCoqo0}Lxn~A-s z@Nu7>R3Ub@_2Wpsq8N(vXi6EXpH;>E6%#APxbNuIockqUF)Bx77+=zv$uX#VmcDnP zg-~_sP%8O{S>}NRHpHbC3mXN^m1~C`Y4+MEl^2J7Ukl{Cw^&*3v`6!v17VNqmdz^uGk`uv<8ZlN?op*hlC}b-1R<6T($X za6e4sdG7v<2*mx%I<%l1^}fG9#tQR}^8~X&Zn)&G`yMfn<p8f+e~+?SnxKUAZ?$Q20D4N$YUu>)s{_}g9Kr{>d5u@>oO;InIjZSd*kY( z-swwvdDh{tt4$!6(K1F3&3H785|k23Im-Ahwmpgj&Tc2+OSlsxzt6C+WnonXEG}1v zHtd6m*FMQ4eHSdtuoJ4h2u`e&MN!g)!iGz^?*4W&K1zxXD}=f z_7)j4Pd5n{G7GvlAJ=?$g#k*pPHFm@**by zxZ}cXgH}Uh6j!feH253M;OvOt-EqOL2X;nq5#!_aRoHoD5|MfgA%}LuZ;4t^6P)fU zj!Byv$lg?DaxO89&)rv)B zW;HmkF1VPG;EG>0SlHvEW`%5yknA>|WMIufJ#LvdCp8fq#ntl7CY?X7*~DAzdLBkx z+6T@ZH1D(rAIyv`%i%1x3)9bj^k)|7J&87O7EQ`#mxQaF9foxIyonfx9O)jf&7HZX3J=q(_=n~>&#LT*Wi8}#(YrUWwyKt}idO{Af@J*y4h`KS zJlsvs(mgt#5m(1mw=hJg!pH0J`bkMv2QdX!_1^;3ym|bg-G&raf>^rwc^dtvJdU2P zp^cyz$1Z=e+Y`i6-VV$J1fl@B9kU3(-N-5$D5@9xsmlb$-h6x~(Kx|VU1J<;p7Q?Tbl zCc|f!>|4b&+6327v`|hwUr?~Mobx?THud@mxsqVyFP%engDrm$A8G?ACp=B)oQub3 z+*GmvREycu(8GqrXqP6v0r52g2}x36e#gD}r^HjXvJ4K-(_*l``@b&$PKV`TDlHJh z2(*Cz6wC@?{oInDzP=kwxt`z?ZuWTt#Yd}4XNBz zh1y8RhfT<4C2boPv`0zKtu`oQvhks}mKxE!x|@%G2sk1+r^qmMPcKnxz`1qTLx#+n zdyhP+EKTsy(sy&=a1vN!V`&OEJHF0!K>64GG^j+U+J=k3B$MeMKa3_6o86ma7IhF= z#FQA$i+l?r8wZ<&IY&Y#+-k&fXVTo0Wp$0h@lVV_RtOZ5fb>ElUl)f{AoQ29@~$lz>* zEgY{O@a1YXgL?WtYZwurOx zhL`A@WZgYNosjJwk0##h>WFZ=lOtwVS(wuXkrD0(<^(>Tt9uxMc$&OUl%LVGPkFF% z>oUrf@)4DxvJJ5(-M`PPueCHN>!aMiQr&qS z4t0q#3VJPFP_7Qp%a^1+J!&_qnTC<%Eze^W+2a;Rb6zQVchmg)-oBGt8miq@kF3Ks z-fP9ckHd5?ZjI*I#LK8Y%JqkLFa9*1YLv>QCx_80kz!;CXX`2Yg`QG2cKVX(70{;m0qH@nHzxR9uU3`}9bfU7@JfQT|B37mM3QZQ%>FeQ$_$RHJVL@qB8p%yguNb%%Q_{^hjTnrM#seYZ|#zUnt_D$}NW&0+|Njq=a)>q#>TfB9JrdWw!xh9nf+=q>w6)Z`#89gVDr5 z{|a#mZLU`{0c+aq{=dUmB}W1+kS^iwXr<8(vrNBLom1wp zkb_nuIoRE;hPWA0D%#ANnIOWG(ne}S2ClnH{9oOd4ti55amJM}(2bzeqH|xD%W}>1bou?EBvU|+)m>E{M3N2?@e01#lf%(=3 zIW*VC_u#!TEv(Y!_73MxKq2Q%4OT`vh-_VI!CFB{1Lc1y?C0op`~}02If-y7dF6BXKA;%(k#RXzkKT*#=-` z*LmvMX@S+47nto`nNmyq%Df0=a(@(v4tGP{8EE#LZdz2s?lM`Ua|^a{o%=wkPf;gG zqd}r#02U513KibY+RrtMugyb;Bk?;Nlv35hV|41VUEz4J1e@rB#SzA~+|2DHMWi}* z%)6-pnNePN_N8elL20h7%7U^nv{`!$zUP?@&k{Fb-KFEgS3~d(v_`5d&MtMg19J)P# zox~mnnqutHCk(i2 zOh&&7`MxmXw~+0moP~jH;Asbd_Jlq5m(5AW)Ou@7&o+9s#tvl1QYK!qAzw|!T&nTV zoj&xxQJ%Oc%oMLWJ%5eVPdj`>D-clN7#!OGh^!ziQO;SRCkcZ*kPkLo>5q?5ygG!G zs9xNB+50gUNaiOC*(d|~q%^!78eo4+`lUoqG}tyY6{1E8o;fICdyDZK)B8 zmdQvHv=$pur2sDK_ygtAjxd98PNEbA*q!{L5y9($G>{IzoywLjSJz?=Zx>?5Dvel#%#Q(9^duM=yRcfIu+` zCC^YW6@eGlXFllUROmt9tyK1$mW5Pt4&;pQRj0>zY1FmR?{HSe0$aLsS1~VSpxS<3 zgY?%uqdqn&;DpS6bOf<7JvAUzm=gIU<3NK(K5-dSNLlPUd#(|(Z%31hy5^upHxl;@iJ|m-lJkPkB zWgMo2Bto$gq8J^;GZ$S{y_7~ZpbY=(f=8V?4jr!h#(GZK1QYyflqFi7^0D@nOKAq< zUf2_qSzqZ&>DZfh`J~)jt@wCVZ(A5tr3f%aIZe~18-gzEfqBW$d_=>QuS}Q}%GD7I zF{s7E3WS}!c<%BG8pnKFXVXPx>9CZ$-UM06cH3PkEGZUn`Mz6jBM$Fgs5BCM(Y-a( zW)C1ZNV=nLJQFD-E;G~eDV}) zsjb*W9DfaBS1f*WpTmE7C^A8Nt9xLj^a><&GnDYgCm=dc#|FR^Lol#~G9a0P;--l< z+TRePEd}>74s!Mrg|zGm4c|QhbfZT#H?baJP(eZ*W+zNo_6v}7=7ebulP9>)eN2yJ zN0iRq3vY{QL!R;m^zHjp)+jeE%MdAL!OIN$QMK;NvCUsUei_EaCUM`p<(7XBGF(|m zV3+d~QIBJrY zH(U1F9jzqfPk@XrUihB+cfYz@JYNtAV9*+CYH^A$yI~*u+ za|;~iEaH3qYwL#;H}G^WAu5}9iVvf=h#OFr^;ODNm%g7K=_H7+nekV*B*57tpnc5o zIlZwkW(bqMK&-@|L?9$lN3{`cYYA}cz}>`o0=>J|Fu0Hgcl1cupXqi=$+#{LQ2|=s z(HMFbX>O47c$$?cEOQobDp>^Ue%}X!1}Gz@vTN_6Dr+}wj#K7P(ENNwqf=n|rWzO7@el$9RH2o?;H2VwN};~i-~T)ojuc3_`NDB? zsr-(k#Oy3_?c&XvouBLiwOHuQ@wQ_-hg+BzPfiJIs3T%}{6sKp3=1gz`fqdVqaDG%2<4bRyLf`YP-+L$ zgK%_vf!xpNGoeG_GsSBDrLeA;WhuYYl*Iufz0Ofjvz#k-arHL8A;bl{q~l|IpfPp8Fi4v{M&4Xb^Lu))>lsdFnFCJBgD;Xmb4py`ntBEgg(I$U3(@ z>_piCbD8q8w}<9Sv08zImg;e2ieHd{@K}s1e3Ep>0H*#PMhNeG{muFY>TQQ+|75%9 z#m2mtVup z;w!i{y;5dS%o0laj+SAe`b0#8rcs~|0^55ZLt2s>`1=5<+261;1%o*h&U%(iTi3A%RvF?S8(c|^0|;9BU0IYLf9~@ zt*V4cG>tJb7(3$d)=#0myyQpp;!^JM`|1J>@4ucwkv#x1Ce+2-dP ztfkD7gh21rzmm_TuTFLXCLQ?%OXUt8B486Huyj+89vMrOh5*~%W(`cBmoe$)BF7)e zWaisIMxx!|ug~N^w~Vmc{Cp7tLjt0YX)5NGz>*Vh+>&Hk!kD^|mfge@jn3;dNIuls zI+W&6ErM%aeLtVLZ;o)VgB*Fc!WZV< z>d3!u3^e;O;y~pv)qN_79g5~`@~o7HS&Ja1qc*bHTYO@r>8?l8WpBWxQm6AYEpHd- zjjV&)Bk~_Dn5TL2EE7aK<5la@B;$M>$rG;X6I+szHO7B);|pd?mmj0N!c=W^Yf;O( z7Gh8#D*7b=E-p))ixR9eyoL1?<=f*Nr*dqzW{*R zT&qRxx>(F$oZw?A?{h^5fJsozAT<>XUx2hF=k|IC+dD}(erO&oN{!c5 z7f_5p0n~CpOhG>BrFP{zo49KX5)5(})&&jNYZKkQ&p63bkmdcWBs>> z;R4h=@rh%?kx#=DaT3~vQy567W&ft-lhF>gLjh%Fq<{hTlun0@st1veLj!`_b@-l$ zm}+5=l2^y;J7(D^O;#SzfQ)JNJV7mYxzpXGk@%(gVeYFt;q1}Lt>>RNOu>}l5^4ug z8iq9Io>L9cp5IU?Q{@J;*yzI^vU`(mUnxTHpy{t@@PdY5-9CAc`_u}sCrAmar-$&+ zV;|1(nsI)?wYxtqIwY>@yT@-L90GDOsm~63`Z+oNx|?}7A-#(Kylw)1dGh5`jg%yFQf?-$9d}k?&$xon$aEIRTy1% zIl-l&W#Tg+?WPz9j}Q6~4{(-oT0r{O@1GAOHesF4UsSjBrWpeCy11XtU#gM#5mS;}0O|Vgj&@@3)m~FiZ^7wS*xrA)l(FxYgb<}OLf|C&1 z^P+fYDz9L8e+Nj7CAG`pN4XMt|IfJxz6+bm1@XT!yVp8W<~UEfpALU0x6v(fCmQa1 z6!K93BKD>0eG3-3tiBbOEUgVMhjy1k5_xbZTdzrb^FoS0kv5%OMR*3`Xc0~lh?=SXN!EV=UTc0;3ZTIC^*p+YNx2+w!mfp7v^u_>@lT~KzsPKV63w_- zL&RXwbEmsuqco(eGaxW`zEhDtMESetr+w@S`zk~#Ilz-e50CKY;0raWv^t#r9PsnA zLd~3i6s0wTE7Kg9MiY{Z|+YwG8f9s{8`4~L*^%`SgA?nQCO>-jZa zLZR~}Betb~@FE4of`Ea%feJ}3#g5RpyGcH8Z~<0629Kpq?hNn*`f$ej(6Wr9o_`nfaHZ2Hp8;v-rDpAcK*|+Qnh}plu#dU zTy}-Xop!gqFC~Gq0-FYXnz+|~G09*cucQKfAD?e>ShOHOotQ6o*=sr2_Z8#W6=`NK z70$%>sazfvmaziuOJ_~GagU*9=7Rp_yxq+;*dOml^U@VU2;(OrKnU%9h+Wi5#9<;u zw~rG^WbeJSt*%3hagadQ%v)uYcbM(HHrbe!d```bDXOAV{CY*KVHzhpVDJp&U`ReCvYUnm}!h`m*-Xj zfT(Wb^{Ox7w0vUz}=MHQ{dY=DIkQOQO zz$fyJ@Lf+9dC>(}A|1F^j@s@&Zte-U8}<2K#nmQ>;*h_~lDQ{ION*R?qc5MJO}o*& zz#wDk^;;xhW46=~es%^CU}LB;Y_=oQ-wIqCuR*qco(BkI3O|5_KI#3owR#=#@y&e9 ztPiP|CiKb6&dJp`)#R$#EM!oX@%7hUVc#3yb@CJxba!Xf^8m{%C|WO@;t;=@W=JPV z{Uu&{`FCbfkD(zi{Ad66y2s0QxQ~~L)f^U@#_r9SVgusgO1kWr_7E~P7Da4UW7_fS zFD(MJ3%2{%uXB89lm-awT67k`3*sJ~wyT zZ@;#i<6*M67*eC@Eacd-T-(~%|Fauu!QU8xxlCU#oB^pa=bVP=Em}xn z0e`V|793F@7ll{tlE=$8V2)Fwe^1yyc6T;wpc(d#og}9DeB{GFY&4tj%=7Q&(L~M=gJVeNmKA1v?y}9Jq^P$?^}uW!V_dvlKuwew5lZne93dRQ@YY@ zQ&O?Kxl0itLX1UW%-bWXK|;|2YI8t+KUVM~7>z<+w5{4@z~^hGQ|zRHlcTdanX zNRvmqOzP9%V;_Qm>r(xOHkM!h`VcqZE_V!;?a9_poq05%_3zp9Tm~7$#ls-VxR4y4 zqDU^P-Pc7>6WvpG%OR;ftJ9HEstAM3w-zf*LUHbQ_X24#FNLM|gdQJZNe%MQX^{12 zTRjpG-sVAvPHeJwbZY7QV@HI$x{YhJ0^wup$}3P&?XddX61;0 zVSJuhy`CQMi2^R=7%1WgKOj$(T4qgd(qe4hc*z|1XdKYM#q=N0SnE{Ao;3=0BZM*x ziDt;s2ZslI(cXV(yjJH~5OP{SoUKZA^xZUDId`M3Cc)@y-ai>jk*2B>leCRJfPIgn zDm_d+mI2qDj2BH=rLWr5k}D%iv6j7YmvU3Rc66&I9#Cj0(w9(pXJX@XuH6dNt{J=c+QXz^Kh*PKW=GvROy*Y+DuKX+DHZ5m25thI4w z`2~d)XK!WGx2(`?L*Ra{R6h14~SbwQ^$zn|f$|J7r0Yac8bN z?k589CFwY@mi;A5m(sQTc~*HbG4x6ko5omJka6$%PHXf9z>e5a&)dp-EuWvdL3M_% zNPQi!Cm!la^iQ=MW#T91;lR@6!rS|$=e6XJYdEp}de?4TIuNhEOK1H;2?m^Kqi}a> zlD(+G2X9tW^kfy3M!waqK<9GLdMEDhc11qRi8KCb+bv_0Y-u6Wbc2ideSlu}M2Yx;V{275MS)LZHD}1~_62!tB*@q8%xbD|n7Vx=S&419 z1C@A+AT(%e>2E<73SRmVbgg=-to$BK0u5qvYO-Wka22>}3!%E4s-BjG;B#G(`;8n9C1-a>8~{pBanF6canM=sWv7R2ga?{AbdJKJyqdMB)c9h`JM z#R@zC4z<8rR^rV9N3WflL4Siwib6{uXCZt5p8q40FL6T&#K{i&xckTvpd)A-#wc{4 z2vZlHKXgj=t3Fn^#|MfiOkTil6Q!fdQju(0v|1RrnQfN@k;BNqu_tw3fBYDRuJH4` zbegRDwOc;B4q1_X8(~O6V8e(~B-){K8J98bxqM+&?DH(2F8_jvj)E!BZ!XlTKtb(N zuko%%DL*LfTQB=Yos7>gCh^3>p)r|ISKYu2lk`S>so!)^17>8WeQ^gnAXu~T>B}UY zDxWhrP&dAN;~Ad~m8EA%j<0zQ7v2RUDlE z(bD;R@%H2Ex~CFR7eQf^DAZ`wR^l;;=e5IG*`0`E~0z{wd?M(&9m zi+K%Dv{hI-EePK00N!JBvqm}mj%;m~r1ub-P;-AyILUHKHpRja;$@YsonnmvH;u+e zKd7|^5jaxZ14{oApD-BwL);U0&T)EsZrAg8bN-Bmf7}aZg*tSnFm|QY zsX@mqIHsy%V<}Z~c&uZVX*E;&Em7*H4XOF`H{i2caG-&7-aI^A;hI@F_6(JXIh7OE z6R!^uqM+E%4oMUo#+0E*CDKx~Ndw;o(8UV*DpHI%ReT8uo>5?_gnghU3ytgF7m!Xw z{*M1WoU3iG#angn75JyKHHFWC9pIi@Ig-vfE9Y-FR+|T|qwV>+q|E9IAazU=fnw(C z&n}0E08An(@N;=FD$SC|~hbxM@w$L$ZK;`gqLW0wE(pul?pXafoT{t`M;OQ8qO$IZOZ z&k`2VTcFKmRzI+xOvf*nAKulxcZcYS^V*(f^CIHdW=kw_AO*n~WS|hM42xqejK*y~@b}xU9w+s!DNy{RlcZQM8?l^HQR= zDLj*Id^Y2L;A$PX%+ekeJKqdrkZZ-rsr5W=-5?Al)Jjd?cKP81bOaRSxZ{+%B~feE z&Zit=iSv0JclMq}&mfm|fZapRc;z2T(??fPH|*iyc~>OAUAd48sez5N@H38<=t&$r zAUEIfYE}Q?Fs`XhR^%<31`i!Ir-q^+h=c7;lS)RHRLlfS(Dr4imS3MbxOcjPz)$e= z-jSSB|JkEv+^X=_Lc^s_Q5Y?515q(OU_|o)I%yaJuY5(N^aiomUz_bt#~_8Ui5 zP%>UzQsl1J%3(p+YhnShBM^?UX1J}xP7g$1%L)veCz=9fVO2op!Zc&|rRvb6g72eV zv@0aBYyR~y49L}3Cc$01`0WUjwZtC(4vbgI6*M{aUGhiA7AX;dN^L#g5ZMX24vcBE ziqGX~V(8hgl$MezrhEq{yR(r2`MzRL`eTz5hamTIhx_f;oi{6qw zaTBTwhR^$RvIM*#LgfIkRiX^mf02R>}tjM`FM-T;uzvXj!Er+`UjGd0fc=a%cUF8oMha4c0Au1jqYdJva zZg(Utg`kz{G zQC%?M-; z4z)M>tbUKkv3~H~^)4PlwNh?dk6CKNK0a?HE|ub#&ue0P!#t)^I-~jyQ}w2Ze_blN zfATO+(V6b|_^-~m%%L{2SSTIF(a2 z^6Qv(-k+yMozT7ciJ4+zf*!>+f+0%5Rt?p?%SDikligPQqbrZDD!&_)=YQ%y6aq`fWHV2f zGT`b*Son0OO=C*vb&nj4F>KdnLGw(U3`?S%#I%pV9UZQfa3{&n$RaLunA=qR%}IOe zxq1Id$2qikMtP!!*sK{CG=pk;ei(@E40@FUpJy*H#=$=aod#p6V z9g4#yXY-RiJ2KrDVJy&-m=g8jvbj$C?h~d%O+%OG?|+DBmbkABMk2xRFD5+2jo6!*oz7;1)d-t-B-^ASZxvRH`1WLT6NLQWj|6zWZ+6d|(>ztk-GaX(iBmlK$wEL_FzAq=-u}FI5$ixsji6TR#~i(nZ5sDi~u! z$P+d@91dN^Q+SF@ixn(_=gLpGzLiWUUFEiCq*AJ3xJtj^hYlQkf*NNtqJm`FtT1l| zN6eBVwX4C3=TSsb3bWD9e<8yhy%C>z-fjX6pi!9{t9`gR_E;rnH~jjjOz zdNX2KvtT51Di9zBq?BHJ;IKmUM&d|EZ|cF*=b@xP?uu6-QbG*0)@!XpYhnw3YcalB zU5(T&4^gl$?o9qcA1Z*?7GM9yuAOCB0DCxcH}r$8bTDrw$qU#7tmpVfx} zk};8aN(-SWoiLdC(q{yrg?B@)o4TdAET(3{DS263 zikt`Hx8EhXxVEqDZCb^HHZtLTY5ExxN)Z(+t5Xt8bS;2$KqptGsF+M#K4zURLl+Go zAQM&q`uWjSBrxISkFEl;l9a>z09yQWxM97`^{q161i8i z{>oP7DLQFw+b*Ha;`OfNfSqL#JOu1u7HT>+){~q`wdTqy#*VhFS8lcaoz&%1=2LQNsEL(;{E-=viyaXP zF&elHoIgEvD42$OsU*-y!P-U)S_5n@I z=hOWZvDLoq7;T50>{E+=86oS4tSTvvqsNsgy5=&Lw-#OL>Yym1wXtY)t;osR607=7 z`#&KU0_C||-;)+`!~5@KYYtNV=;9T$lPN@3{$(0~q-4H3%6{MciW}TXgaY@9=%X== za3Jasp=V%55Q_mC$XJd{cyXrC;G8fJ2LSs2Zr;kTs=6V1L+}ZNTi995%a%jMw}K_?wluoP49 z*l}oN&k|D`QzMqr@nRPktR)Bb-m~>El(YlFVz$vWyEN^k+Zjt#r3u`_Yy?_QzRCQ- zZaVuYqxdtmbGXMd(JP`x^}9kI2SFEkX?Gxwu#ksli{o#gAvUD>#651v!QxIN&#puJ z)9R`^hT<(iYl)z~i{TC0s`o9L;)1|orO&hCD)+8JW&+BPotU4a31L^-cM$}0KrA{d z4Ipl?RRh)`a%O)JQfPWqCi*5LrD7Ixox$)7(a+L2>Q{hgN34TbKMB;E8^PIXIQD^X zAb}kEryZ3<@XN@L%R|j=S0HMT*=mN{x6I_t@8$i+Ak)gq)>1XU)LcnwF~s?= z+g3=mRC8mH?Orlc^BESEXI9&q7B@pN96gQEGQRBwN!zpl*NAuDzU^jB4tCsD6dhj~_Kfb~~vhA_4VuG{B{w zOu8lPuPKHD+GMwM!|JpxL7dT7-cvkoB(rDJe8#tjW&=u?m#N8Lt2o_7GH-Bu#vIIkvL#%BDP~ux%h7rvlFR9(Njj&-o9cfX9vH(xXCAf#upO2ySw1fG1q$ zojRDnq}BY9eud1XqY>mxp7Dm8ayh@-*~`BDhYM=+fpYJ)6SO7CDPjMXMK~Sj=xXgr zbGMSpsYOw1qTCiTw&diOOJ9)tMm>zo;2|2NKIbqFzL(cE z0HQ5^>^EaG-%GE^kiAp%_auKUKK?aIpJ+UbXZ}YNZpBY6w$4%WF@pcN323O*SjkVR zvW2bpFMBqQ*TlB4h}(n4waS^-`>J(OFty*uuNbx)H&!Oxp&Hll`P>c*ylP^6UUvEZo3hk7k!*ScS&I~7+uG`u*j~*SE&5^zzUO=WGt&plKfXjGtA*KU-@g2F zk;6m(bT5lK(|i$$16;8p(vP{nJ%P z@+=n_2a5HT($?X@RAE}5Y;hT@=$k0R8~uQ?NG)9OGZGJ`VLCQgo+X*G)*nJA7(y5? zFlgJBDQ};ye|F5oTQ=TEnsJeo1M4NFh~MJPgy7K=Yp~EA;j}Mzj=hIX(n3Vn5H6Wi zm8grn=U_EAZ_jnO>OkK6c-PWb%*Wl-+KnAP{xGO>1?gM!RR3h3byXJ&6+3Z=bOYAe z88#H@kc{K{5gJ&0D#=@jJ}^kFkyP}*HJ$6f7C4J?ZuVB;ygCtu8mndnYlLSuO~OeH zhnSB;F6PZe(yJ~h=MQG<{r?wf7iVPErdJDd^R)$teac7j43>1K|JS$;=MwwQ z_#Bp5+(Y6kcc!9da}PuQTC#bE_CQu^<#>1Rh=-`Sq|Y!8wjyg&{%r z*hs?AoH8vh)LNM)qnClZmBmkcap#b$t5v4}1aY3#g*Re%NDm(6LT+{>@69;=Rz%-nVB6XhdNX)h~Pb?1W4dao~F+7EpAVCxAV5M z4PA5v42!s@KUFs;J=;9?^;u8)W>xPjQey_0KO^mkJ~VIZeaDWtd{%ZJx1L}d6TG`j z_@v~a)#pamve)0{XSXvCmmjiqe}2FPPilR?%K}F3W5vfyCpKNSoKiIEtQadqNm#m9 zM)XPH&3VXsB8T(M;ci%ehHMgB*=Hr((tQ96&(&K1XX+r?v0Cs(c{SxO%`@?2%=<_s z$5A$~;EV?*=wo*sUI<)YGtDQN8=YgZPfzQTR;8)#L%InwqBwI3+S^R`@dBv$4&_7aQ_+jZ*uHt?93atR)GT*HwgP4oC zHG-pQ%JEKUVM^^!fo2DCgKZ3EG?3B#)V(Z0gnzFlOWB}~d|j$_Dg2?tNKnD61QvKm zVI0}v-lJdqktnY0Lv^#I?EzO|y@Vbd4INuSy*31TS0gKwgC7k?@#su<0!HRh4~CVc zbZi)$c-{Ko3KwuXFq#SN54bb)*4Sw*EC@O3*nNZmx#|lTF0siL$XDlla(6nBfwZXd z+P0(c^6MF@;ZS-xVoj zso_8vVxNu`iF&Q&+(T@00V9-zij820-C!6pXJ&}eHeAT%iumyHB{VL7S^bx^x3{A; zr0tz+*8cIU@oFf8!#+(#u;_!pHs8JE{7-F~F>%8BvxK)6urIWTmq2hGhkr=fbuvb_ zr_d-p`2%JWZ`40iSa(Zkpt)OTWa=Px@QlN4j^&XbSu+#|p4-F$XZBxheosk~!Z)Qx z-$KL$11*3wuzlsLo%B)TRLyW){6kFMPRF2kVgE^&k`ji5s=ffbk`e5xRO;Fm_r11* zsp1a-f>H`>5h7*_snbYWR`WjE7WQ5S1hT)5;8Lgl<1qtlSLmsAU!Bj(iCt-lDPTE% z?dgdgofCO2MPLvnvY91=dx?rTeViJzYuC%b>wO|XgwU0AU?`GC5N>2*8kytLMIXB5 z@;rqVkb#}ABycHS;{9#1B4>SBF$-?_G8?_(qJzBaQpxJTTvRvikzxHrN@?HA;RJFu zH`J)#&dRYP%|^FPuGCj9K1l#N!#wN?1p)D{zGJfDhsuKmniumkbl#{{<#(wC3XLtd z`ppofDE)M0?;jG@`(5imqx~Rp%=SEVuc%txm-bom@)I4{s+$w(?8(1odlR?n;ps0nlr+?}0Nf%O1 z;uOQs((()=9>P8=5YvD~?6mRTHDv$GM;TfBUTy}9Uw2#?+aIjBP29S7;PO2_67u<6 zCPbBHC2E?hSIJV-c_(Q@-sz>xvZAV7;lOl<&6Fc^IGodoQw6VbOciShEf&S*bindj z#w^p`Z=M7rjS9=6t*G0K(c}n)-i@Q$VlEKI#Uf!GfdzTIjRvOdM@ZE?g&OVImtbC;-=kk$qNVmrssjN z*eb=dH{?@}17dnsi6Cy-UN5is+t0T;BIP6&>9K;=A|K?5xfu6n4lIHO!U-*pkq=DB zs7?9e`9A;|p8o~^>wp{9@D4%63_y5dfUu5tdvn3T{cs)F>hskRBf8yB*DY~LJ}K>K znpumRbL=*3dB1pY_bvMmtI!)bB3!bB?!Z$I16R2wW7?SHtTn_Pc~d9!RLS9e4Jxn+lB-JHVMnefUHUy;BObCY{_x-g$7B>ZyOWKqWh{=~K`}GuQBiqH&ohC_W=s z8;d7*oVfAtfbnQHleCkoNyR2s=zK5B_WUVF@EuPW+-SI~t`8IM&CIN%*^3H^p!ph( z|3G5YlXQrRMZsh9MDZc^07kd|jJOfDMIVXa&YmI>y!Sw%aX<>uszz_kfI+5HQ=RIZOHjwaB9V*BsSmJ1v(}c5DgM5W#ar~IJIM-;mt=z9k16Rk@u5aD=sDA z>#pbsK84K7u8PKW@$uGt7%&$(7tPdu)c7p%==oTIErVz#x}tPfn#b-YZ-7LuL=BJp zr`{0mPsAm~R4B;%+1NI!tmJlSQ!0=GDw_d29A~P@j!0$T40O}aQ(Y{j8|^+%B+2Gc zsh~3W)xhQypNLcT!tK!v-!d3js7}c&TpPfs=m!U(q2}>0P%~qb5!%?b)hiC0_8l|4 zDf1M@fY}rna;34^h;K^dO83oAl0>2ALVLSA2J4fm`#R7G5k7Pd@O8Bl%#(&PI>%Uw z>Z%VJX29Xgj3%ne&&&unpDN6P-;*|WnTD74k!4R3aR}V{NERd-Gvc)5=NkW=A-qsq z9_KY@LPbeb)ICXu*kWU6xtPmzBrdf1X|NeAQz!)y79NjWYrcqHj7g>x)qIMAG~NhO zW)aZePH1u}bBo|FG>yi8%my}uH0S8JeWaj7S;D80yNe=bN8@TM2+B8)w$u0q2}+4x zj+T(S9%^bq9u6`A4>yLtHz+@Jhqly|fYkib2;oqEu5cU>i)GB5k=>^#eV+!>P-o#9 zYKFCZvY5MkGq81NhNZspmJ9oyKr}p=Hua9Fp|Z6DST(b_wMHrQ*KQgbZ{hFR4bD{e z@#tkdOAjNX%{+tyO|EiP;a7HN^F+Z<@!xCT5gzP45WP!`3>$fl8<|rkd}hqzh|{Eq zt}~H^z5jkXpPM*$f+ON=K!{J%BFLt|=Kbc|!_4baHwwX}nvDBq zD#L9$jYE(x-yKA`ucZvPhUAdaO|_|9I?8QW97DYX(~PD5lOCZ>IHY{gIF2aL+E9tm zEwgX_3g&r?FP`xw zjlFsy!LAP^55jHBxAfp2YxP7s{Hm2(vk@gx`G1yomnf&eQb}3;lR&`h6A3hdnRNT! zb+nLEnCrnSHLvYgahA!ULu}8{(F4#ZO62U@0h=(~qPl+@h~ITekq zR8b3!(xdHvuzP}p7Y6#&;&B$BMq5ziFS17~`G|9-M%bS|rlnUjDd3|VYp6&`IMG*` zYl=S?n1_*Z6IC}>B2vZ~2o>?m8fz%_X@EjmlD@ky+?e5D8`mVuzcyEXI$vLb0Uyi! zl+?Ld(q>I(_zTUhD{JpoU@E=q&HrrYuH(+H<$Z9JDGBAy>@!Ce?1|h}uq5gGaQXSk z{7XNKaIFI&#Cy(ib2TFPtup@c;3yseyW%M&b%F?6TyuxcbDthBHQS2ZM5orLNhFUj z*jAG~;CbZm&g!u&O4+&Bl_!O!T8EjdI(>iQDJ18vb~R5SjGjh{Ja>PC&=olpF`zvn zI-FeII6+t!sP#Yv3H*fqMCdSZzswC2zm1Y*juRT{X~T@5W_jc&Adp@Y$iX7*vLD;x z@wYyxT<*MlTT4aWGz7x6$9V$VxT-183#FStLxy4(!}A;%kV&ZKwPx*mL0IX59 zGc<85vF0yE%mDgoPSJ3qcl;!}ImG7FI+Q*e?E@#18B$ym>?(Wg%o7V^E;7M?AZ>UOf@>;h({L2B8tA%K-mB@LttT5ZY# zO2(*UWbdq`IF06adQk1}bmn4gRAy*xTMzFMYT3yQlQ^E|PZ8T3=|vUMZeSH@$V+m&2=%+=spxMv(aeiDOb|AYhK!WzO(AB?(%aJ3=*|-&ptd_gr@_+8}*u4r%>r_ zJ=-$A*;Z+)n>OfgSs|~oM<97U#kXk@i%zst%62P?3S)**J?-PHimHIfM8at`EDci4 z7G|vdSpjCg-1#5~bk8{&Kb{+i_^SWTDi)x=I#C1>+t+Io2sNX6#o;Y;>imw{&n2$s z_(St8s#2GjSQ;}W!_Gwch;eNF6Z0W;>z$ibBkOs%}=4^(u55$Sz50 z0e0>%z26Z@l1r+;&Iur@dJ)RrL<={CnA|sK>cvGpN#UWA%&ufhhC_`r8S@Z9;=q32 z9lwi>2(A5?I`4J+53Z?InmKd*F4$!r=S6D)$ny6^zm@m|J-kxnhhS?DE{aA9Ow7s5 z=5Ay6YW+xVfK;d4LzpskRt~?OU3oSKch0ExTSXIucKq*G5}0kMYqDc|Vjr`@t6%NN zIB95$EDel5RO|9Ga*ZCQ0@VI=gVv{1>~$LWRLI1 z2r!^D{_N9`nj$uhQ-xrTa3hg_hE&{3zcEX7tUS@DAM=URB}P!Ee?*w1soOCV6~SHl z*d{ZCj(KztzpgD&A_*mtXRDv1w%)k^c-gZmSQa@#ujZXBu=->KH<;4hB;8igcpTcv zzc)g%PxZUrQbFA4gq3PVbS82KO!VIl4+$`wEvJ~$K+IfO0w3>tdhu4ms;)OE9j2CB z;%ur5vBmf$^dpmg8ES{~N@>^$3?Waz_m`%9nErADEmpZ)MQo9Mz~WaV>HzCJvn=#% zTafI%1OI=zrPaQ7>Gk1{v$;AP_V+?ZbLPlP9)p6ataLOR27!^Hb|Lbr#0pYSIXbmx zw+Wu89204zR&U!TH}3T`+8V2-j0_ru!RJFYkp5P}>2qkvAaqdc4w5uUn~jBd`)Qp%TVYflIL!F-TE ziKBQ|G17b=JpB)iw%KltP6XVe^X*Zc z@l^h<9al-X(>{YiUR1PF<>babum*GjoKM15)^46F?I3tOwn!AW-TUXlKQ^xjDf;yQ zE};41Czgj>!q_q8{E~F|iRd1ss%w`~a^AJAR_FFwbf-MBbBG1vS`9u7k_tpLobDoA z#Y}d~xX1TPY0&lWx23_C<2$ZCMJ5tlHxkm})@ma;GR{%CWI6~lZMRr2 z!dA$ZqW?I-<~{IHD6G7#sWBSNYMatYIwo#sN6S;zb61L#*nDs zIu0s22mvG+w}>-$)K%qB#z?U|?;}XD&M1o+B|p!X7$2dUo_po%-41$1N-8rf<#f1Y z#g4lT*>dHWk-H4E#jx{1Xfkv9uhEsFXmiG1HuQ-!DeeCiN%n z>ViiJ8DjzflzLL4V@@-C8h#zjG*3pMFXHQc@RmU-1v7Y`3aQbdSI;C;+*(ZoZGxw>B6IiMFd2VCRv0l zj^TZaPIQ0uN6n4ro`|^s2~Hp<4&rbZgPT9aDLQ#W$Mt-T0p+zlvv5qSb*e(*O+q!wEJ4REI3PxeGNDv z(Aa~Pb2s-xUJTSVylW~atJ9v!B0@mDI$MUJ<+hU*=S{(kpzn^DZ)h)Uf?^;;yg0ZRONc`Z8(SkBZ zgDV8!EqI6U^6$;%?)m|T4Q~s__DI_z?2VzDoGE<51RusZ(c)}29=o?RhD7$|lmo6i6G8Ki_Ivb6q zCnfw_Fj=cdd6EW7_o=q?Op?0$mF!4EgqpO&@sw)NY$RIr7#Y=a`n9hGRlB>3<&D3c z#Er5)nVX2UUl`rN{jgc!iQ=Bi3?)Dqc+rK18I1sZ+Q4{gX_O(X zwf93^bYvgamEZ=BUzWC$$eziw|3Z;k$bMvqkSjx-iwGWGoyXoNdci#H0P@)&zKE%@ z4LyigF8{O0{2`$C*kVJ zyXtg4CUn4l4uho&6v|UwOHWliwbJ&$7AVByGZTJo*q~1~udgICym+BD0mI&^wH+Sz z(pSS^&};Rb-oCB$z5s%_Kszt7a8GlH5kD@+{vyw_Gr1R$`%;zi!$Mk1pi+8qyZJ_L zAeY#da^^+pnmvD1{hik1C)u1Je~y2->8|GL4lcpDdP^KO@>U9oHG%z!l+XkSvmG|C zzE`mCpt395m;F3`n85<^FA8TL8{b}T%)Sakzq4E})05!pE|YSZFVW8uGMp#bjKmDp zcd-Lh(&O%z2gy!nY_~zYyHG8-%KO0e9FcDW7yU`-r2zkK&b#WfTAv#CiMqG_Bx3M} z69qbg%G2y3zOHZb(P#C`9UrX8>55_tN&>@{$-F`K4&RirG!_F);*Zg$=mg^u~r&8+bv zM0&BcU1I0x^`T^9u>q5D4cX_$=;6Ejr?9@^3~W!>g@aub)#!A92bA;)Xi zw#?W^TP3ltkZ{iZWsnNi|1oW*m5+WIK$kJ6HVqDt_+&d5B?C?A;B5wM?r-PdxPlJ^ zJ9-x^X`1D}mo@7dF#(ZTkXn@F@I{1zSN7!<~Tjq>_*}iQ`|M@wN z%(}QejRRtYg;M6{rWF<&5L(8fG{3TL5c+m^-8-0(Y|(ML<)?ZVSS*(e(-D6i@(2uz zqr*N$fPp}`lUrt`*rrGs!35Xp*f4Q8e-%O^9`_@J3|E*Sm;+Io%PirE#!)%5(n~?f#lE~bP>Iex2`e4+a$;uao|Lx;Cn_?& zeeel!Z%d{pkbO@!?Y@XPl!kHKI$9w^;H>M3q^d1sUzIK~(MxJlg`XYAF4V;){hq%B zdE0)0U`Uly=dV|VuW3hME&AL3_|ARW=cWf@i0>x$bO6@ z-Da%HqYRh4z|8vhVLm`MIh92hztjYSZ zD=o&xKdb{Ziq-y}D3!STFvKn=sQ6e>I0y%^;mOldy4rlMsHYQ1unun(zR6SVk3Zz} zjIbZO#~ZbQCp0Jj4@cJqV9m$RcbNm7vFTXJ%FjZ#X4;iwdjRHEf<{1mf2ISSatrnI zHD)=QomZ_JaCO^6*%zbzEH60~NCrj6a*=-<^=mpCNvQqorsg?FIlTNCrRU6b9@bKs zoFl_k{a$NIl!n-0e&&OXIO;D&rJryC%>CNiC}LN#4^aB_XQ{~cHHd!-Lc3KNr)BWg zwo3vEi|fLX$iC1A9_7}N%(q6oJuTx0Wcu@Wb2z0yba+V?@S$uEzzjr3dMyp9b@b|3 zn0+KSlyGRB2uU1UdT-?W^&?&eltum}EBP6v^Z%#RTLTSMH#d!=&TbqxbcPigO?Fgb z8t;bDH4~7}`i#oBYKAB9xjz0b{%BV6)V?T3jdx+Zq?4W$kS_OGqu&su=aJCXI@qSQ z7vgAX0=tqA#9VvZo*vtt^Or%-wgJNVA*Yy%ijoY+r4$f;zGHf6$dnxG$7j;77fW1E z6r<7J)f!?wXn!)Ft5yr?VmV_-`R#{sQn7%yChS0ENHCt0jGheLIiCMuxpu|#?_GFf zRbg+i+J*Lf6ygi&l}0j_wsp@4QbLH<=?(?7b2BMxfelp2=eB$FU(?yVUAB=8o3qM1 z#FytPw>k7IE$L^175tmdj--7QR+&?s8L>dNPHnqyc2nSaZU%*AcXcPHaj6~Q7~`qR zFTQykQiv>;Gt|yiK!^d(oW}a4sQUMa7N0(!>pZnl1Sx(jKJ;!fQjd5wwQ|7DP9duR z-W;DEuj6>kejcFM-%Pu5EyAoSdL#B%SZ%;4l#ppug$V+NM`+^lG{!snB&D>ZaLzHD zbIipl*s`0M#S_s4(XIU=e<@v=DCsx1|L@3n_NkD-V+CBT%***jDs(o&X4zr&5i%h7 z56sV`7X+=8579XZ-XuON7Q^tSSjr_!+y-(7J0!A&yo;_qgp>E4&NuGT8uCj9+&L;T z9rb>qY1b%yx*Y0)BnDtlr}St14pM{N8$5(`q_n3)3jvx>~6DCttLN z9^WSo7|>cPUTd#GXbpn*4$@{$+1gH9s=Bw?m>#8F)h9Q_f*%VA9Tq;xvcy29dpr?F zoj9_{1KLh#Fe#%gEI4c^&va-jU)oWt_;7XpU0Xy;jJ=f1pezL~EgYzQ1B$RvbtH6xGDI6}Q4GJ)Iy#wFcz z*EP^{8fMSFblnyHl&8k|gMI*2?}{Fuy&3nVIhtZmLm0;5il)i*rq&{yFCIzg7-N>_ zUv!;0#6*kYD!y$Znd^Fl5Z+THGMn6QYwku0cyfT&9q&7Fr+~m}{~nbqY{jJAyU+9e z;6HmyoaK6zVE*a}`w_HGZ8bhJpto$rS+Z(9^HF69+?Z-0&;+TNP>6>axZ;`^PwM?S<6JnN^B|3f_zN$=*j)bttiyC?W5I1}R&ws(yNu{HdMWsQ0-pBG4 zdCUyx?J`9@P5J^cPrwwO*Z<<=+Ui>(4OjdE&Gq23dj#)ul%nd~2-E+qJ6D~9dTOyhb0g7s2)|xtzoaAOvqr#BRx(0e8k995 zs3=)T{?dgky)&s}AROj+99T?e>=DI+6ove4q$TLvZmkkghIjcqsR2wpU|_SpvcA>} z`)ph@tdLPOs+HoyXK?{m;}x>q?Bmpntp7~B#ckRT+0e*O&Er39?+{-8WI zJ*6Ib+Zj94Io7Ia!C^gih7BWutgJrbbrHf7$P0zIG887rHYyt?FcR_4hv}MgAMd{c zhRb~zZbZNERn}X`p6%jTj&I>Cm{-QC3?|t|gRJwn^*3>@t7e4%nw!(7v`K#lA6JQ; zg6Q52Iz6LE2svqVy;Kd-F>oWyrv}n^N_H}a9r`rdIFwq%+F<{z9m^%qsAT#KvuUh) zR1&$~PZLReGM^w763l(*&j^tT?QwNHTKF<#R1|MG$sJW)o?VmH3ru3FXWf^FYCb(uQ8>CsJ$^!oMpRttr3c%jSJ<~OTOQ`0pqbot@FgYahv6h~?)LfoooG66 zq*9_{{T|qhJLh0f@g^by2`4sX^jy|a+~!r4%1G)ofQ2EhR1rtENwUT{C88TXf}jqx z;T1v@G1)-U-g%975#-Qj61fWR(LffM#oGO|qE#PL4A=Neb^U%HqN0q=yH>kYZW|1d z55C?1g3FlO#vh}#Yx1I%aJQy}p!nH(2PD3Y0d$A}t2n0ojDRAl9f3oVe!>&Ut6HEY z{20WeT2b)Asud_-+9f*}VIQ!ixfoIBz&Almog5(YjzUKgoWJJe2#13J2F|pjci+=i zWUd049u^cUv*oOA(#D7fYytI7?FYdz2Vc@Q_QhD@fvayK#SvPu&GW{@w(+nfP$tMc z=QkQKCw-*5CH{>BDI$#ke{5s70*!I^ef;uoBmyJ6l_JVmcwlqRIt(Rf)nx0&@So_h zMh55I?(?h$_zWP=0!M0SsMzuwGvP=f^GI+kA<`F7i#_c4|8i2>>meJK_?GWL3}SKq z*rw1V5drUwxc0gqm9U_`Ux0oeM(O=gx<=2&$VI}`%YTs0RW(|*j-3rtfxcSUeyWH?J?sNeo)|I{ zrZC%+(7ZX5ds}C8ajGMTnD zj8S0OdlbF!ivvJoI2QRwodh^zmYA3_P60U0bM-!WNFJQzyLMhJoCsq9t6Qa2?9PyJ zpjHxoZ*w@g_@s)0@*i2AH_~A_OcIP}awGO_Cg8rOIg#C06+8v~xGHerQ;NlhDSiVa zB%!UBZ$;2VXJEr({3`dILeCXi=Df_x-ff{+5pvuY?YFG`;rsw}4n!FFZ15Rr9#5FX zaa|B1*&vG$e4%;9877I1#_f0MH0NFw<%MS=(k)jl$8n!csJQ(cFd4!;! zYJWPvQK|h$cSmB$yf#toC+4ZaS3Ds#gXbTV;1xHRD4&OI8rN)yf;^={ zHCibX;~r1+#ZgXdEIAS-t8WRe{)hljE8U%O)x|gawz=#AB@dOPWWVDb^FTI&>16;P zRWDKB5Qbf(9q%0<)o8;cGHw6q13vSs>eJmvwC%xdd4T0+0~qVKn|WkOBpRk!OH%*- zBEf92B+9VXYw=&VrT?@M(JwqQ&BgFHma(LYVLb6RaUY8`aS(EE;bB`H@n0suH2KH?*@X&%el=3vS}ENAEYu zprr+5ScWD2EI_D2R4l4TLUHt>ZRenqvkQ0UaWx~WTLay~7Rg=ZyJ$185NyYX+$aFN z>wV`!7BA4N$$=&d+(1@{?su`toh01ZB~|RGWs2Lz>_D z!n(C-Vlr)wJhkt1jhj!i1GA1w(CTHxP~Jm;!*f`vZc547c*LHxV}5V&X`Lia&lW%6m18*GPzy0lHsy_Losy z*Lpq@ZgVhGdYzU|!OT|``Yrx+q8yi5u`zqj>(xzL zR%dD#Cj4ej4>qv#OB?BzRHIzqVRMHG9Of-|O&mh^Lu1;|^mvs(3ZALgTeG6Dh>^IC z%VLSst8{sx`|xWX-{(wkU5^)zK?$0MNaGYY05Jz zjVasda(G+%Ya9&3&+PR*lYk|FdP?;Z=xHq^x(V(UkBx)phS`U{wWUniXtferpQlFgAWG16} zh~qcZ)O-<}JI+%$=dy&LxD`U#XhJ$;%R(vHb3a`?<%MJa5^i6|m-4NIhL*%5?g>GZ z=}!=WqT59`h2pS>CCZuqB0qAuLv6C>9Eg8etMo7%jAbH+y&9A2KeMc|%3Y~V0=b|3 zY+!;WCk=p+MFr~8fhTdX8|j2WJRT`oOnR?&Dkx<#JxadV_yRfY?!MYKA%L52wD z(r9{#UK1Wf{CPa^H|3nF`dD1)iV4D1C!vXZ&8ViHWJ7@zzst`DD_gN6H?az^7LbCrNby0^i@*V zPL|Y=VYeC{yHO}L+c(Avzhv*t+kTEkg#B5FaqVceVCZbyUvkzm316?+_iit=L+24M zcy-eRBZ8mZS3ZJ(qZuSC1_-qBVm<__*PkJg?^?$rc`ebn5)u)qdfOkLAQVZEfVZgH*owMCE9k9JCLsqCQj2)# zt>Hr0aZCp>3yopg90~i&rns}FB3OObG*({tXWO??>ve4*U`~*^YqKC(mvWZ40KlFi zUgeBB(jwuk6Jk3xfBIN>Ou-7ZS*~DXAS_l5w<|GO1W_Pp>;%(%o2*V50e0~IrWHrZ z>MmIwZnk|*8>^@LnY%0_7;=zq1bqZ%l2i~Rg(cZ!6M{PK&;cKBk5fZs+vy!akYL+| zU3eZCgW6}uU>+UsG7p?4PFPs9b@SK3f8%z2$}b3$F-q5lq>@{S;B07a<@N>HRW{** z?5R0{xNN1N-dYjwl$Swhzzxq{Vk5}@5yRMDdgdQxWq;WM#P0WKqRk776;PVE%0CeAbfw5 zoqL`+RT&^8LiU#m0K+T;g-QnD-q7s0n>k={H|uYgeGS27BtHEVC6J_=s$;Nh3C9Qy+`m7Aq4{6NGGp zZBRm?r;hZ9AJF3h3P)=0CyS}v<=*i$Hybo_3IwR04`P4os=bGYdm^k!jAr# z$|mLoJSZ-2korKCJXdAG_IX-2F>#6(J+r zcO?g!u^;4TfSIuh7Hj~#C+>SD$RNHvyVLuZ9IFl|ytGvdRq&r&`8=9vKGbaUQQak& zpsmhEgO%(FFL&4#7$fxu!l6%q{&xTs+9>h)&sbc| zwbG-oDaS-?Wo; zmrXTj737fLSGh+l+Vpb1#vQ}1iY9h;L)7LHrq7!22T8x=)5unXXuDYkabom#5r=TR2VLjcTEEGbek_~B#-^*?nc=!gusKYVu^a|0;lo9CBI3+u^5;*L4dU%{ z>hUVkxz1iSr2H_hx2h9$EJ(LjC^=94Ay$)9!#NO9iBo`c=JO~!FaR$nM&q?X5iK`j z<-XuMp|xTCX*DH}Po>$g^L1P`?)l0AaXNz{FpYDCh-=!~ux0Qu+%2QnKfy&H+7SWy z%QSp~$wdER?w+nLysj##nSvHUNfmeG36zIZ@%=2MqD8#ws;YFn>Y_Ws_nY;#piG#A z)d6&GzZ$vGtRU+Mo4*53v%6n_knX@0|Bn$OO8tgS$Gf=lE!b@eP4642hya`Mb9J1S zN8BIP{^N0Swlgme{9 zFF-W4)jz+*fR*Q$@C)qH3GX#|qRj+$@xaP}O8Mfr5~n}WcW~R~le_cqxUTnDU43_2 zH?PM)nukKc-TF%kF;80x6Hg^g*|B?vP+0ZD-se&49iW+dk*y5bD*r^($UptTK=+fn z#K|kB4S&mN)RmgUSc4sM$(wj=g=}!3MSWkl{FmezSQk>L!PLIsc&+`A3ibg+7+XGC zm1jy5eMI8G%3C?CRNt;M)+w-}PS^AB`FBP_Y6Sol1_-7retHp|hx^O7atDq-5K`Ky zE0gdCI|%GLlm1SytX{6As(<%3Ew^^RO=%xk?vhp$2I5Pj!g(|Cz@HCMFgyLr7MeEe z1|^x~`4esaNUvV0s&p7U0-@IIt%AQ<3+i8E>;u32kP@qVyh%gA4WGn6tUwt-3K0f^ z4dRqCIOr+sa|KCU_AdU4~H&0J#Gizg@W?T2?ACJk>5eYF|ryIi9+od z$~Twu3FBVFXpp*#Az_}aj^wMTuY80E4h~c(JQJ!VPB$34!BT51+hBQajCn$?oRsI3 z&X;#_f%}A-$dG+p@T}4_4F>Sev7=ce=CaBeQRfJ~vlweutO|aW>DNF@mp(X__F@z} z|Mj)>77IpTnReTjsP4#!zgscp3RlBJv4*anc@tq>qEWR%wJgxK5RbHOk52L4jeGjD zK{OonAq8)YzJ_ADxf8-s-8BYr3BLdLQrpyk|GJMZ}ua;PtizjY@fD#&4mpE!%3*Awmo>)!A&s% zIJE)Ab)0=qQ;AC$GD~n;0UCiQ$%44I@_&o9PnoAETp27`M3g{|hrN;w&|vTnnK~On z^xlSH3Zob@vP)uhduDjy1yGMl@QAk`Q6cp zs0-(BGqiySP$?$NGrAK3*@g5|7I=ut_yjziAQZaGWPoMQ+OJMPz~o`VR~-n#%wsq* z(!bM{(8Gn0&;qp9{( zX5>{??mnQFX*YF3C+s*GmT{+B#J=Pmte{?^GLP9VG=HB_NKfk$(1}_5Q0Z4;TNjoT zim&ag*gvTfBp-NJto+;9B;ICaIF`~kI6w#_D&gN$hSJvhMJ4$dGc=K!9>{?;m1AZ= zdmF#{rLSGve~D(=i9mAeP=6}}ggg&l8W0!fyZg1vWkD092$Nl6Ck1S;`ayIMfBhux z=(K#xh~r}a1&Xq$CUk=RCPv+)DW}L7HdbDEe^hOav;P_REF$Gw*;9yuhtq3}Bcq4r z&HTC1&e@k>bLW0{9YmNBw(}=1> zZv)lca%ImIHuI)IH`}_k0C_eCcJt;9moz+Abxgp8%niFq`iE$X z{b35?iuaM8Y;om|S|<9-WP^MkVT<{;@azh-Kr01WHM@7v)OG*08rGa(jM`x7B5rF# zKIdmpi+ygK-nCsh95BoHV!*js8f(_)ob!e9)=9;YWS>XL4m(m*f9K^s+)?QhJ}fDQ z#DtZ~>Dm1_a%yQ-d0*VOn#r%T2zAf!2Pi|z;i&mtGW(ov?V?h#2>$j{f7VVQ0>w5O2`OQECu|Eu!SU^f> z-$~6?Vy~_NiPUoZF48-t@k2T7HRDrHW7k+d;&Hafd@CKETGb1ML^2byB{k;ql;-mL z5O+nnLpCMvuVR|Ei|1l*Uz0SM`OE_B6(YEFI4vn5wliB(J9Xr38t9Vo)m(Gh)H!iE zDF6i!;)YT#B=1+_y|ZY;+l^1RNnzle7Vp@w{T@lAqO(#_dN&i`!9f?p;igx#Ae0)j z%t3cLY2oYKJ^nlu2->?;BN6jJ+2#ic-T)~;*1rWwc>>W`FooZ8E={OnrA5FH z7m174trH&p*mfUGFAJ1voz|kMvcgVACFI!}!W~uax?jo~xzgc$Yg0`_?27!7ox=gE zRy@+@%Ezk|p=hcat%x^7Y6jtkXX5X#U7b5b=#R`2BaWx>DRw;MziWfv0D+>^(iJSz zBXA_l!Y}h6->nrVtEd`r9SWz7mGc_y>-+C1Ll6|HthS0F!>az9Gnc8cpIkkihXRf; zC=?-@B-WUi?!mA|3e^hmD&J$V4tCoO`2oh*r-JXbMzchS!qqo99*rL3c^_kp@$%;! z`hoK2YF>6YfF;1!^=Cv5p$uhAf!ZODh_4mseYMXQguviCvogh*btL_*I`RCgKT1QCtsK}a$TK(&_1ccqWA>Gn8w z*^}MORtE3uXIc3wLQeVok*(w8lAluNRP~DK;D3O2nFwRZ0TEoJrIPGI#IF*^OzMZH z>gtaYTGszj(rqWbBH(5v1e^qZP#aX<*;ManwK+~g9%AoTD$#v-;Bu82S&KT+?=#O6 zU(pH4piqRa0 zMU}n7UaTyfKk$o&HLbkhz%`?TWsJl+kK#Pc&lNhv?{eByRMBWB%czQ)kS{2!28-vLS%c0KjyC)yFxQIy+N#WuSp%>T z6E@IjqQQkJd^I!?QJ4G8+L9nQ8Y^N)PY-j z&H_|9fFqCYE&CC)P*P&tmtZE0v!Or4GoFqVl^4Ml>9WQ1nb%Bx`$gm!dvmCb$_ zU++zpp(Kqe`AP43wiSr?0U9p0V*yXZYdAgbPn@Q)N}QfpbA=t<*t+ecuT#t5W=NTm z=gg<+u18_d-C$`HafdcW*}KKH2m9$RveY7y70E&4UkJQAZr-i-CK~~`tuYwv5v#kq z+l1#W0N4+Ei#?Mt){l`jq10q$gVF~MmVjO*NNBA`rB6Uv9BhrQcf}_SkarQ5w<&UUwuI<#wB7f^tViwaqco4Ageh6&TL<~+aA zHVNBw1o|G@(Oq_l%>SzN4f3{}iZCzh z_F+*QNT(=~LK@9o0F+1QQ^nMOusm1HE$#a*zus|NV}*ymJFEQGV~;_{1=MOK8!$cn zE?u)!r|6zxaIo=Uh8!_||NhzJeXs$k^PVW)|DN9Z-iD7^ifhb^V!G zLipfN?ZgAT9X`zqWyHO6k20`I-%?qG zW9mL!rDzq$y!IBn{0BOJm0$Of^9>Fto9kZo9!XQwN_bESXsI=8jn+H6RR_k8v@5|> zY*hn%%>kz?&9z$wfHxRR4n9Hh09>L0yEBIg(Npma`k1L9ZyjHhnYY2t38H1-TBbvP z-wlHh5-IV5ZUI$5{evF_mm2MxcgbV0u0{XzIW~;N2pOmL(W+*MudZZ6iNjX-adEsh z4$o{nCeXcg{+EGzPzp&`0x+cy&M9>G9V&~dqS3c!Plj^ulwG1p-Bo3|28hxpXgCx2 zi63en4GwoaFJTok*#o=XK(yMeAW({Ji6R@~8z_%p%RfCVetry|M~;YV+-!XkkUnW| zjyJk=mzJNYhDJXTHBDGeW7h92yd8|+kUbaKzE2Fwx7YXGdPrKeBzAaTt3ofEtATwD z5QhcjcDJ~(e{UbDGrz@8nfg6bS>jAu`9TaBw;^7nk?2heU)M1Pj8ezXsR(~XDV|q@ z9#Ig1-a$>VZFMW?L}HP4*uB4_V5WTitWBJc+TE*2=YPtnYVi3pue3>b2&2P^*eDbg zhDx3vNp_Xtq4vuxZ5zMpUS&Tpl#uyp4}K7WEWj=HE~UEp=tvkIY9*cAv9-oJtd*j$ zDt3pI;{=_>;`TV$cG=zgk5p=G!(^6`Yi7W|Cn;d0pYyDVsH%MoTVZrBhF-t-6e?jJ$&cZ zSv?T@Ah`U;T%7$cz*Xc?D`NBFvV~EpNQ9Wt=`<4YeRx*Bd(?Sdj=`*lXGBW%x!BRT z7d*RIto*D~4EyMhUqvGy!PS7YC!y$L=E-dHFP#s_DZyzXoZVSw72XpDi0oB}A>VRB z7el+z;%iYv2dgzY=(-4(6>`)&kt}-Fm$Y(0uNh_XnC$j1shVgLyCom`Xk%feIyUd? zPhJaRaY@a$7munj8fAqJQ`?U*1&vm{Qt$fZ+)Kp;=U!-hN4PPAEzP}#*$5vP8A$od zi1oU_Zvp~!DdjRrDx|`ipFt2rDnbck^AUPuvYC3J1Yk~~o~>hz6yJM{4Z!b_@W6dk z6=*^WxirVW(Q;03Q0Kyh{OkrgQN2CNVSq@>_2jjtMF)>$YT8e-JsVCJr(uwO0<^>} zQjYB!2L1$;ct+s(x1Tc6(zIBn>nUVr@NZL;IXd90AS?+!^I;8!4BONi>>)4ilnEz_ z@3p8Wq{}T4p|a@{W`Ee9#G_!Tfl3Y!DOSf0-Z%jF+CUdK#02mkPPWe^eK8TG6v$-Uno zwrIb3O6;U54Eg7NL1Jjoq(m&K=0Zk^->iRd1|WX_={9IQ-t)rZ`*=PKi1tTifl=N? z8bTFJ!5cf8E1)UO+TbwCG%TkRN}kINwHz$Xi@ zxLLVc;l`l{Rr+8qE0gykOO9B!*+uIxMcCWH^gd&^ag?X!H(o`|Hb4)9IcH-6KBi_wce@$p2K_(ddXy=!YAiEG1LK`wOuz0HoZL7P`sLF6z)rS+l!E!4n4Vgp zCq+5O3~VBmQJM82Mngbm3o}iyJ`1s|*s7btzXWT_KYLKfT&A07dWE;CPYo-`HD8+| z*|M_ND9=eXwH6vdH>^ZfbU>O$9-U_$vBVBw(}tLUv$rX0HKqbcV9Tc4`RHpN>A(eB z1JNq?;kh6f11%bV!Q@U-M)G@Sq8mrw3#FgO1>+=|A})Ks6ulJ6oV=5^E!vGyp(9k# z@ZG^7V}qFmg7BFzuWZA_O2`{V^{L?u(vdQvMCs*&rfn^*lq3x10NgQW2^SEpaz?USBFhH*JirrUl+01j-3{++tw z7MF)4UKF?N+SDTwx*bM?Qkcn)O5z{rQ10H@aMMj^S91(lM57c%sgt7^9(ezbgG~}g zH1U2P=>%ScnLL(%^l(VrReBEo^df{4`2v@}m{)*yiAG$)BkCOdkUH@7lfF&mL`12n zRNbXrs_+2MhhREAX&s!O?vN<4&=NS0yOF}pt=qX@w?D=bK0ayZ#dgSu?oj|7)Pve= zQe~g0qyeU08>@2A!*yuWcfy#etQW<5$%Xk5`ENZB0BdC_8yKo!N)#f$ z+Y^{c^;`+pt`QTEGScq2;UZLd&e%HIz$GJ*tkrX_47G2;-CpL@hJzqQ@NcX+ssxj* za$f-u?I(Nw;NrYjm&zP^1wtFRQRXP3rQ*gUFl*)YVW;m*to(M&hX!yhV!hw6cyk8> z+NFLpxoG|}RMVnAoz3#s`CoH^x@Ks|vy|@3ZDLz6H?!XbQ(7exm7o2C-|@Qo%VUSZ zPDXdPwNj+8sRmr(hc{H08KU63Zn_&Y>NK^ZD*XkJ~W9_5-o=fE1{tOK?Y8Dw%Eb& zWuuRYczEUjewIB=OB&UPF*h$O9jmZFeiHm<(&{=7sVGegypl%pu=I(Q2(8LlE+;z= z;opiGbSY6oWtB-2P$j8P=u&%}r`-+I=~Tv3#-u2xgpoI~v=h!e%~<(0L0K{?rC?ETE$H;z_iL^=_YE;3P{L)LvJBDqym3dgsryQch_0Yp3W zv_{JLkj2n`)ZI^`fVz!Q2ZJ+|&-GOB(S3>Ln+ar}VH+K^V;*VYVEy~0j`BBY1EPZ0 zsGT`zesv0v2gT!AoI)3_Zx&@1q(~NckdN{qOOuO+n+*l z*Z?cU1;hao9RGLs1v_yZUa;@Y&r_QYp7ik!ft!MckI1_mL@AwG!PR;D&q-D=LJV*bn?Kc50CFp7M2E6w@mg0Y1`(=*Cw8_4G;*M65>70)+MpAR2!kaB3?_rH8UA14iVFHM$m^eNM7@=}rm4P=U>;7j1%B(&16h@hqM? zKf$!!U*=bRSqc-s&t@U)b8!zT8skLJdR*@oC%L~3+{?0bsyDYAcOBVxO*;vK;{>e& z@_6bQEE%z+i0`V}R4?Gro>W;Mp~eG8ep5wo5h};qat&O;WLf0L6MQ2>mRpJeb;rhi zbFAW%MYt=k*%PT+x3*3p>{y4J12RuGDCU1O>Gw9e+CMKzA&>h_*wUZ#@=YB0E?Thv zQtC`}8Su#bFx+DlA~D-7=|nM%3XM=;Oxd>BJ9k-jYwtq7p-mfa7|_GEGtB-AmrN+f zt&!1P7!!a+rYJPE2yavS0)kwlRlMQBT>-b-Ba$fwycpNWLTJ85C z1REAMK&J^sv`$Ya1ym)c$DqxYL|_Z%8P*LYrkQI& zAf+ne%r!8~c&M!wx^_-G1QoXplN&Ay7)-KtV|u$`r-d$U`A^_j8WZ#$uUvS^*(6D; zlm)@%&L0tPj=DHuVl_o49IFL!)$`G#uXE4KWr%ps=^an=T;h=Hp~MYCRXEbRVe$W) zz@3yTr!O`28K$_ss~31~LomS(8=;UqJG|7lhgC^}*kKzh$PLg0Q`3w$xrNazJlwAO zx>kGiM8|1Rv-W2UJeTojhcK)GUiRt{)hK0WgsCy6myAKz!d}Mjw9$GKE3=B{yc-34 zR-DF56%TZm*F%!`+jt-kei65Xp!*HV{j*)?dXK}rdlSKM!|`BJiH=P%>H#ZCn80)Mpu}U;OMZ z0$om|hA%vYb$p)SC=R*P$*#$=Nk<)}IERbI%wvZSZ%dAg2HLf+(9{2lUyfeWZ#n!w zz;>EtD4=ZfuhA{RcMT4~*tBj{cLs*j|OusMZ=`lxJ>+6RE4d5x9gV|utt?b4nU z#Ql%qot@Ea4Q%-NyzEOTz!b^uXHyNTuA5E-0G?CdEZJ?j|IWlhRMKbyf*X3kJWRVh zX=Nu|5tS%x@EtRxr+tU^G>=-5AHF`%tTc7HkJ=!)r@kHQXM$wOhmR^W`RoLq#+!sF zlcup)V1s&%{|N@@v;12l6VEq{Y-|9Ru8y0Jb<{CRzoarNJhGvZS? ze^3YE+T;AIwSWI-%nm;L~_ddrz66#@f?1Q`X zQBiTqLlR>sv0gg}u4AiyDW>;ztg*IoK;W%!25xsRHc0Tpa-#HrC_ z@&U50_Y91&>E zg7T>Y}R*oXRFCP^UWay-;e1zG@boSjm*EV>Eos=p2M6{k+!S94$o%Y)$w&+z8 z6FAi^8V%4!Xh}nBVwqYfVN0s;%F0z#bpIBxrds((ojg{W;jLrE*;w z$H+#Py|EB;b=wR+R=oDi?D#r@i{s#}G?D-67v$uX7o1x=9wGL|MefktYyq(%`h6pj zGZfmzJW0o z9=eNVOJ|WRmj%ZdGUpPcw-%d2>0r)Xzts-L`NfhGR~)4Cvru!$x_eD;2v@FP53b*M zm~6LBWe72-&NzKyNzhT0J;Z|AreHU zBbM8rY+gq?vJ{BQ9&BUx**xIL%<wH(CpVA;}xT;f6Yjzx5rdq|B$Gmk0Wt+CFum^Kd6ib%L_thUOxz%2?u9 zF1E&qc^1>U&9v~9^H4GAg%wG3IcXcdv5yv~D*8r#;|e@1^o39(YOf8)VuK%&i0UL) zg6hES7pHCqZ6jYULza#PDpOU+g$CWz-$|8z_*kLR%wlpa!;$T;)4#{#N@oAPW;oDb ziKPj$GU%->t%{64sK#lcz^YTP?N8QJyQAeE5T?~0FYEgOCjr{M3LrPO<$J&3uA~%` z{5vvh|K*5pI~;N-92Wl%GK$W+W#X3?H#B!`(Qh;Q@KnbUzGk?6h7x~l4+1vv=*OB) ziTP_(p?ITyT3-UCMhlKy<29u0AfQhG!jZz#Yb`mN zqA#q+F~mt!o-5EwIqhc(ZhG;_WU zg^4bibjyxaLVIpKavLLk2>o$Y)ISRy!*V2;W9K&z#*&{vBy7SC{w7MD@jSq3YkVTt})g@yz@PlCXp2h_0MG zeggT(_A{rP-SeU_t+4~@xF-ucDO%P8O2Q3n@SrIPbo^TZ9Ey-Hx>Vqpj@G0**wW|j z%cL9LFxIvApn)G$AXFa{x}r~_3}->+YOjp1UHvtVN$d=XvCMhV^+5N?qMH`nwV}6$ ziGBkZ16Ur9pB+W=ci*1`;ZMIcmE*W>k%P{E()`199orphN4I>;2sJsp#O^%hPCZmr zNFds1#1kwWEFgTf5eq%1-5eO+n_^!d!d*av@7OH|8q3vib*^5EhSzB@PxgooU#lL@ zm@9_zN95naTP0$M&A_zyRxnn9Onn2iVtWV00??_CRvLit60f_dom2G)dl>d#_75?x zQz8#sho|gOV2zH>X}Zx<0uZuzYEL_ndDin~QR?c~w3I#VHZ@ChFG!9D|8hZfprsNW zl$y?|<4xka$;pF6Cc;Q)bS#A6b@`KRv1t~PUbz>9#YT#6P}PTEw|y$#j&m`S5D%s3 z91T^px%gcvHoKzH3Xf!$gVZs2>D5Q1EM*FoSl!sdFihg)u>cBwF4YfY#_4q=!l(k1 zx192!A~Bd!<(J#Ah8m@0Ap081`=;JBSy^O60<6rPx%Szr89eL}{i4Gev(VP|dA;Ih zoy;^R6-t1+yvq~m^Qits3CD!8@zGbT4~=Z#KG)f`LuvV%g~K$lp%C^{vC>~cr3myV1Iv)+QDreLuE6EV~y3JUA=~Pt?DCi!UhJ921gv zBxn)y5_0Id?f3M$ip_+Z>a3AKs;qxBH1|kfQhkMiEv-72X7pL#oP*Uw1JyjPr0;~> zsRn;PsHnOvYAvekMR69=lcd93S)CS&^4?7k<8?RuLzA!9e_0fPZ8ufnW_lS}bss)(- zHm`J}AO9n6ut4@EYX7;zN77u#bw9g{wJxs~RQ8J%Rs~0(8pzNj$eEv-ovIK;n#~A} z)J#6|~9%Iyht4w9uy<~St&}oB$J8+#g zJB5oQ3&pzx^Tb*L`??zV!<_VqS8bOl_iTM#sM|i+uUroAsNAuhjK%UC`U4m!9DK`u z@3Qf}IQ`PErGsUT*m?P-z4?GnIG&3H*g_a7OPA|SM0*{i#mb90YkqM$=^JtrsnQO;`TS*$Oj!(^`D(DXS|LC5Ag&(4(Xf!U|u%f z+BOf{^Z5w%jcXpzjorF0xg$B(-UGoAey`c{bHC?Qj2WSU?6YvOCdL}xbnT~Fj*k#v#UvD?M zGttwi5ghW|Ti+0_GiapQ=u(YLPC8-na?TcFr_tcPj}()8Y`F|}B?`dlN6j{}v3;e$ z6%}B;?GSTTO-4kh!wWH~rI()&SR`&iG6K3*jdPVi+Tq-OqEklrm_cG{qV^K0-M<-a z0s)i6N>FK7-D8i6mT-Y>t|X$|d?voRm%*GkCjhMGJI(r+CS!1PG0Ljy>Yp^$^)5^1SPV(J!{&p*g}% zMbO<+ME-|D=p)U>gK48w=Zg_Pb`3TeKkd^>ktx_~Q4{yAG;v^nwl4HT=YV=C1GiqV z*U~=+zScblAT*QyAlDEy<|P`k{Ii@sP~mafRv{a25AaD%C9@DzL{kBtLU8|e$+*it zxYTF5RYeixSi$*m{T%2OZe#^xUQr}@Ga}zFEed@+vhww+*Js^-W%3@aqkU;6E

    w z4G%l){_%Jo+^e%Nlf6}#eoF)bkzplOF%y=t3vCB!`o@zQ8kLTWFv!XAc4Z4n$H^{R zkcAQw95hV|G9ToWd53_D|3wn8IyNQqqV|+I{$9Ho4JT!FGx0Hrviovfb5`jJn0clm z6HgBhfP?01rh>imi(e3EF8_`R>`R1%n&AJ#u23!}4r#f;j=u*4Pt z7f&db8x5A^*D>jMBSob%Am5bX5Iebjl#Jg?YXbgH!sK81KafL0Nz4YgnW0b?nWBf5 z&pbH#SM-14%ZVjbn~y>nj?nG}DZofp0|xB%y}=9^$*k}ub8UN^KRO=2m5wqmZ@sCq z!@=j1{|J>DaJyhBxxM;yh$QM5cNuSu*k&8CGkhc#$oox;!{>2ryH0JoInj7D+>%ES ziDP}t8+{j1rqGluBq-2k>?F8d?HQ3Xpb3K>%Uo)DhHl}rqts!smjap9QjMDm+0UIm zp`2Xd#TKJWj6cJI^B#pO)?2n3FOJli-h&5uq-=WX_$qMThL+oZBGOjWk6zra0js25rg$SNj_9lR!9YejfeFL)2-teFBQMy2uZ69K0Ios ztin7rxC+Xd%A1>Sl!yXrgw z)>r6482(0_se_^djr-&Pz3iuJkxJ+Oa*f)kq<$=YOG)T+rL};V;)mSEPmP-LvSSM8 z2PFu76gpkF{gMjy6?_Qckdsd}Y)k?c(Pf-2-lbyZJdTUB_wf?*jE71! zHMw3+W`U?EhoYqLcEvK?J6Np4l8_OwFaiE3uE>JzPuz; zao2E`@Q*Q4>#?d>-OHx%ZRK6{DZ=p#03L*qv6o(BVpA$jpYmB!yO<2`NU<5ttpd%^ z5VoBKz*AF?21kAjFmG@4tWWxb+`sJ499ljIz{6W%o0osH7rofw-7u1I;oaP9N4G-| z*%LSk1vQm20Wyx>u$3awhK^ub zKO^z9%JZhxSZNz^bDZjAu&Am4cP_9Qv==-Xw2jrKe>N%!y;y+p4i@%Aw##i7WWeGFRfejS&9~bHxwbBl_LLkF(dxAybAzW393=WjBgXjHRzW3gX*qYM zHCAxb)G9o>^anw-qrFpjn8>>*H9ZMS_a0zsCN5#LsnD)vfg*7-4xE)vzxxgeEk;4D z%c;NJUFG2lO7mCMlqca;>t^XDgU$0Kk?n{x6%r36dgY~rv`hDKPq>s1j#5OGxx7s- z^V3rl&vh|l*_4~RJPPf;<6WMy$ki(Q0x`}q>CJXdvqY8_QkX{@U|)nbK9?`=$U9U| z04;xZeLgaXpX|U;?+d_I+%tNu6@w}FbzLzH8PaO!`T)PHi|A`9w+3BI(20pb^+;$SR5#?*Z}!Au>hazayFX?s zFfSv>&urrj=+QW=nn2>|jaXS11hykgJH!!hUv3=33(Y_I#o4 zY_MHrwuoXxtsGS`%3P8N;_0k0E_DbgUPdul@yC@3JBRo6F59&Kcr9u3P3!;?=aisO=8@keL*hALN|KfboS)&C1&?GQ18 z64@xLHd|_fk=1qlCtR+a_DNNxLc?^-IxQqLRSkoHKog3$4~~FueW|d*14d_;MWOK1L-@znWomJ-BAhkacrf zOD0{6GRefXdJJK|H5D_Aog50Z3p0z}Oj@rhdrm-0+vLUHjthC#TPDH#acn>EcHl_=7Tz((+5*u+TT3($ ze`D=%{f;87DtBr(!Y^X+w;GM*E(waKByiFsy0!Mij?RQAs0kEoy{2oA{L*Uq1 zW$=PWe4`Tg#!+oeF>MCF_ibs(PGZ;0$MX0a4|xWjj(~EF3;i_EdL^ycm4<-mg0QJz zq?<|gqJ1yuqCK7S5jr7m59wCHy}#Edo?z_I?_}YA|C@Bf0@6K~b&K4m8J4`{s*-hBLrXY+-q*UIDE}hLAIeB*m+odIBn+FJzDh(&{J3_FSxu?=5`d zZ`RSsU45Y5wncAg$g_!IUpn0BAEocW`%Ffo23tV;)Y$br)1*2ZO#do_`X--yuHfS; z+?P%W`1TSWbNqpF2m5xFyt?`{$fO{4*><$ZoQB>W-qHFvL0R0gZLg+$@x->8q z7W&pR@}IdU5gKaWH+n4oz6RK2Nsn(PKe{j+0-^u|^`#G&=p~@2$w& z`8RAc+mijR1$sV-JAFO}cgkRnef7nLFuBRYhOWO50_zg9wwOVy4H-yQ*U7aw74m0b0(ukY!q9&;)U`b#q?UI*UWuQJU^ z-xd0K2yywlX?0)my8S2KleZW(()OM-&o@T2LZ{O1pDW>g*|*xKxg4&;K{CXU0c`e9RS+ zV0z6k*MN5lqONv3L-j}8lzJl0#D`B4?kZ9dFA9kVtI@_biMI0;82$faSddjpq5|ma zyefJ;5j-*%BP!hTW;4?wyeh)0k)erj3C4=pP+#4Jv&AmeJ=VzMeDW}eMZlae=Bmn7 zReZQ2s)tEU>`S6QxwQbs>SfW=A9gAzNba!D8IaG`K6W$5CQo(<5 zmIWLLJw3UkiM`3iz$SLIrT}=RlJO6hzs|8|LzO~YJ*sglI?TVMuM6M42zmj=Coe;5 zz6cs9C5Mkj;9PIN$cr=8UVc8J*gPMN&!JFMG7`989u*&IR63n;^Uol?4CEQm=B8KV zo=fs|()3jy?A9F9f4oPhZX^A{z~^Lesp-A(5a+=2h?L(UdXGh?WT#YahluOmgehG^ z-&<#v{KB9NuC7(B!O^}E2TrOX{LWZMhC@%%VhN#o>zxZWi(n!8ti8qHL=`A5AQ z5(k?^En?Nk-0(lvusbr12B+9~`Jh-yIgId{q`ECq)oVg#o)OxIb17ZE48yLytiIN6 z)8C5(Eur@|dQ+I3n~5LFwQ<3od;8K%b|5IaCk9Fz+Bg}5Cum3QxKJ)I%;@8s18!0q zpCcLB5N2UF+U5IyGQ&}AV>+M=GCqi3r0O+J%`vt)#0jitBGn@GE zM-`u~%_z1$v|6-f9f_o6#t86p+|^dhM}Akc8ja11wj+JRR6irCX}7 z7zSAFuOxWm)m6R)?ZcMcGKftb?Eg73ad9|$wl3$rv|T;B>c78m6WgN?0?cpJc~2o{ zdJLi0YXA^wHNe1sa^<8H*|jDp${tM&gpI}Zlq4oAM|*x!?X@3h%=x7}7;cJ}H*_z? zXwt<+pcJ6pZTkk{d&ku+nt)@gM2zrRB2M|_E-(=&fhm(A5XP;3s>ZgC-)a;X zhDRl>+UBE*|3sN3<0yP;uM``$4LO+fBQQ^A{Ha%Js`VG*gN~?tA0&heTDC5hmyhAc6*RpD9ThlfJYwt#5ZY26a|_E7lzwx;VBWCTJ)9 z3M<}F9`W31HfdVl@VdRW0}TsmWrNtODPLZYwjk|5{kYqsyO8V=>E3+2Bbldlc!0l+ z2F@TPrR9Ck3`Q)XQ#G=9fY`dBxkaD`?a5{AK+gVYzLE-aVqo&LD9>3M@#2-#+97hX=J_Q@}mNS2SuLKNrwG@+nktJq10=*0bWC};OMbv<2^snyq4 z&jlOPkgNeK!ew`OT=f2;QpOhYT?+JQ@t7h`5nKwKa{z;DHOVd%+PBCx655J3a_a__ znW@q(3!z44(E%Wk;R1`oOSEfY>J>@hoFZSpR$F>Mp{3v#11D=7nXgwIP=CDb{Ygj!dh=5@q?gH8Oqxuydo@-W1Jb_n{FEGAn!Po$fG(6dWl8bD zQt0|rRvOG}jVYo@_$XtJ*UT%ILhc!lAc3d}sAQ+Hx?v~3$4WO^`N4&Vpx|xx$3D6< z$O~GzrM&TtLs3u`))?)fSUBUO4JCu^%>UF@dq%H}=q+I!kZ ztiX_RfKC;q_&hUd%L3${SObmdT>?-(tR+oq!Y=(#@!E;>K-);<+&ZbW zE>5dEz5m+p63yLM@c!OS?$MmUYpgvtW*w;e)fJG$_o%jB2XlRv5S-#_5Sax=rT2!+ z*n`pW_KPeFq+IG*WrjH%SD~ zDX2r*K^qnakq`+UuVCcD(ib*AAIUbNG|O=n47XM@#cg#0n(1CyBVQa4^vb~2RAH~a zyH`^+<~ip!?@%Z9HJ*0^e;*mWx`Kn5Q7W}it@*m(#K$ifm-VhiR491a{pi43ps$K) zUB4*agHntjwhqKI>$I+hCfXeH&OL&>W&9eS1hz_#EjryTTKCPV?@MjvX_re-7@&lO zH@^s)_jJ!pWqyIJUNTY~=g^L*p2aURvB^r!hYUSBsUsNXl;tQQh1xq;AACIEeo2>N z)=@LO8vaMx@5C=*F_ixMdM?cv!cp`7%~91avl;wXW@ya9$6%E6F)>IQEQa*mz8Iw{ zY%xQ25^4n{4qOOhhGX%fuYx)NynU#`%EU!a+HsDaka(?63+T{>HDejQuRdV|H6yS} zWdxGhMIpBq5Wlx6V6qGJyY2->_ln*oD^Ga|(wA$kX!N z?wc)1x(hufDguZF*cRs^b90DD$S0ysz*L#>d}*BXi4B&|3?EjIFZv0-M!K(S-}RL7 zkc!YG@S}Izp?pK>v`V=+xu1ZVgpkK_*Vkp4xRjz`T%9d)V6O2)bK7RgRz`d5c%M4d zD1oQFT5v+BR(FHovns_{fc<-p4OutjX$^NpW)JcO3^v4~S^dQs<{LyXOfyV#w13)6 zHF5d^IM4yLWdsO0i!S(tAc}VsR4PFQq7%d zVJ<|KJDP_G<7jF;a|h1cd$|o_FPLCVg5(*2^or2BlPdy}t#f4EvS=XfH0~tu%_5?G zb!)~bp^$L{rWzV00VgY1$#th#gP0@(v1nk-Y>bAF16+i<2l5b~VI*~pg}W#*Nxoth zZ14E%Q7xF5oKqUnAT$$K2O*KUW;boTbzTatI;wXoNX{E185tjO?he}huFyK$v?T)9 zhsDWcPp>~f^N{50?zIs3)}7_`f%!^aPzra$1t6RK_%bTKyaXH|DJqS?nIsO^Z8RIE zCGKtX2Pxrl3w_zrRX+6oJGNln>^s;#m;8u}+THFb>&}mPl{;gN?9U^h^A4 z5h2zyAPV)je(!v@YneQ5n3$SAyvz5+6_|pub6pTWf_`oZRY}=mdcgJWE5DDaGf;$p zp3W|vGrD0Np-29T4*9DRNbD23w`$1*@wQ{zK zkm~YSA$Nv6#7dW=%5w)7{AAK{Z48}Y$sdQFRfGCzt|t-Q$e!yda{z*UWkxPr8d|>7 z!L&Ship|Dm0_-o;B2(A$!(oUb^k|luwpnc`1m1agi11w~tlg8&ghbs$@VMM~M$ReZ z!YoAIn$U#g4RzF3`F32Wb6)4eP!4@18viLBurjRkWkU`Q0NKU8Cal>dJ4^x<9_>}- zZS3ybQD(zFsF&OTAFoCg_DztZ)!Q%D_ld3{&ksW<>E)_F_dI}2Kes|EDr1nFo(}qw z=>Rh+tA;1s>xL(bu_6AVofexE{-;xiytrNDaMg4Nn$IufpuCXxjabvl1P{2^U;6hm z%Uh*`qvxQ^F(9PRDR805J95^Bju|huCZ;LuU4+~5JF_P6*&gvN@y6#XiZgPCA$(JG%`n(D`bzvGQ z)9sP(9)l$ld4~#HL3Hx4^)OP2)ggFMwR!Rp6nO%bU09m%EN!V!N!s+2+|Mg|5$NY>u?n51CwAvD-%YC-Ot@Ge?* zRD>z>osn|4G!vB?gJ0(c=W)1m4FvHgucG4<@310ad??U81if!-b;tS}pj5EzkCn-| z<#+36hxYy@@4t6jFj2}kFh+tW$d1^GG=t}%P0M2w3G-5bDJY!UQ&ZmjG&6rPHL(jm zQ{<+x{jy@FK4EEf)}?wwz8o@0w|RMbi=>S29-^Uq5=y8WFaF&!dh2wi#_nsS;rnY| z*(ZNmJeMVlrp?v-o^5(tjN0f$&XKje5)NR#+;A#Xb*Q+NPYtW{T@#J6^sufvES0bp za4~!Zuuey5X%@1$dy+{GjlPAb3AOc&SV%GeH$ce0H7DLxUKfDz&7UZ-RO9A76DQvx zm`FX1zCHK8sbUHW0r%YO*Ev|yI&yc5QAPiS*BJ)!M;04OTq5qBu}B&SX7X}CpUjg6 zaN}foxt$Yix|j@Z%2~jWjRIS)z_JM!hb0_kMmy}X$8F=2 zfDRwWhB-I5-=*=Fo)Ylc^`Cy9n`+W%#!a~H@wafeaLl&V|J>h@KKy(OF0F)!i|GyR zm-LX&Ki2zt2o|-UsmI@j$Sx1iG)}XvPYI6l)$gxk z!`%8UpsQYIPMAPqg2Pu`qsIn_BMyEy7a3C+(KIX<-g1-4ex2P*km?mf@4s~`>~;os zKp^izr4m1X2h5EeHXTtAZF5D{t0y9ZFfn5mXPU_oR~4M%%l9#t{Jes1tj*zWCWO># zdW=lf3gqC@QCicGJF*50H*|OF0uPj8os2$h-T1D_~H&4-2h9Hjzgzd zc4p(0CB;Mvhffon`uhUrvvOmmWQzl%*66J209Ax%D}ilyut8T zq9<=$2Uum{ENaOxx7UoK%O<117!_xHCypcV+wl5&Zmd=gE@3nM5`2s2c*a`}Do_uc zbw|h)>5|z%1d57?wa<&TRLzKaDBgd)L1j%lvq2K%>g&r)UrO*IchJOeTYt=F!;k9wZIFuBj}W(EgHX}VBxiWMSP8hY8w82_m_PccecA?p(N&D;5O zz-8V8zX4Mgz2XmuhkHc-$uhwyg2Y$#D00Gxj3pME=#tGUL~mU}X1-XasnW%zMy4wg z5x}c;`qi&?cisf7krg~R@7GNO;}7GGlr5a<4i(qR(Tv8J#u?IVp>i6#5jPdbCyf)Y z^~r`551h=V1Fsi$7@zxXSNtK9=h$frj(%cJIPOH45Hk{$U*1>e2YkEpbZF;>3|TuD zg3viw)CFn^G)Yfh>f1wRge9Vk z@GS?}qv4qlhJ&ogigRX>F1Ld+vEvDU{Ws=_oC0W*xr#xdihQfwhM@3zUBNXvl~+O} z$b*UEIZ!KAm!AQ9|ByiPwgf1R>QX3zW-e(eXhs3e>M$UBUAOS8VVx|`` zxED%v@F1yj-V9!f@Z|n-D6ZazawL8%@48^mt0kn-IJ*OF{VtRU^@}L(b?3;$6Qb|7 zlTpBdF-r4ckY#Y5%fbc#^v^Vm_S~Yq&q1Sd3ou^Cd3A^)tZrn?%$fED6sjkU*^Tyc zZZh=xbf@B6<9>}^#hM3bF$mguK)9kfhPX#1+X-^l*n za7R*(z`thZ59pcgw&NrO?ZJ*@WtqoMd0NLdg^JYf^rRQkdlVjJ_f1fe>!{k+^La_E zuqFFxFf!oP^gV>}cX{mzMqM&U6EpZvD0IxcwPhE0Io~)hQt6r3T&)Nf|1FXtnpR%Y zo;=Zr#0Bw|TPTD1cfzEL3f>uf^xh--({8e<)Vcu^U$F2f zfBmjrPBwved7ZC&zjz*bF&d0YVz<0nbSNsApfiUA3+gz*3UV&-XXfI}2hX8`lQz#C zY8B-e`ZYez8hd}~R!|pQ&=x~|ajc0E&L(NGls~{08sh8+CgH%t;W`C}3LMsTy9q(A zwu=4io6MoQazYByRGjXqf~?R$(`_>zW_}WrF)DlQO}WxfI!EqBd6y<8%LI}oniSF5 z4JPD*TK#URK514ZqxYz?R%^y7giQxOyqHY)O=bEa&ghWm3 zCaDA74t2@t2<%mfJU=vANz|%zWykt$^pVfK^w6tK%Yri3zOqn5QCI(ejQ20{nNiIp zAbbgcww20oh$I7`2sC6bAlQJx`jlA`WB+1RlYP}O;8{7+n6$~z)m;r(k(F#9*Eb{s z?(yQBo{|BvZZhkwF#{`1eA3a401DS&K;%G!ikzbY*Ouig)Wo3?6B|*(pW3Um(`B@H z!!FPaU)7U`TZSLKEfiNY2@0U>j>9}Sm4pvV4F~KvWO=N~Q7|-L|U7_ ziNMc;%Kk{XnZvTjW)ENp8Vx1SvkX=~KlT`a_IH*A_5X2x9rP zJvFJ;u2~Zu0jDbwtGF3FqQ-DTGRHl3jIgx&3Nz{aEj_2$Fi6v@aUt?N(YH2CSv)n- zlE|w@;04AbcKZgZqER3>P-0ixnF!#d!=_KAv@1Ui^82QXK4s=+FqEi;+P&r2q))E> zoqgqT6Pab`)8XvmJN7}Ndw>makdLWWx*GX=82+EIM+VrG>c*rN`XK|W9q~@NQaK>+ zn8dk0EjotkVn|m^hlSNJ0bI6DjS;8L1ouJ&l2a-$R>oe+O>6kn+a#%G^H`<$n5nC5 z!>x&F3oWBEyxU@9=)b|7UZxu82wO7QIkRpzMYwi#iH1JgsPtL1%maD&Qse6qxCFIR z5piN{)q436-srnou?TVvT$=SCyl4(Jp{WhwY(+3et7`de;N{NePafCJEUYAOm&31X z@ZL^{6yHSR)~8F_=O|x~vm+m(d3^wxZdURe`ZLnl$f!V=+*8PaPFn92I`K3!R}`F_ z)tB*boAQs-+>>e{ojQNU`)wD-VtiYM$>FXx%^t4DK26EblR@)Wt}>htfO@8{vSB|Z zzec`yY?CUf!=ee*1HuD8ZMB}F<>G`)xd2ON358kTl1ggH2;*?C@ln~-V8SAfRb%;`0iZ{>s`${uw%Ak2%WX>qIPe+5m*+}^ zg>mBVH5j4wiOazXLE#{9Zsu4;H7cRSm%h18q0YYWVM|k3A3Cf8PIW-yVHO|26Ves~##6^KLmW7N>nq7g9D2k+;>;Wz>fLbtaToJ+w z@Ygxt1YmC`ZcILIdz#)8Tjt(VrYCu14aqBRTUA@UiTKH^l*y%OoC6+h`GN5}(pm&YXzg&hxEQ=DRa+4N zn-%hOY%0CGWOp^js%g^>O|{mKz_2J2GfZrx$U+6N*yP#mZjGFjt%MMf*Eh=f)TxH{ zpjSzbV^aI_uXJZZy30Xit$e>ker_7Hr5D;4Q@fdz3YqqGB1Hd16Ga4-8@RwX1OA?v zFM*DL!Ym+rh`ID-zx7FDT-a*!^YUiOJ~LRU9_>fX=TM3MKN@go()~@#%du-iV)5*W z_4SIWzcqOr>W{+GX(IiDKZTd^bIv`mU}C(oi!?QG)I`KF(~xdAF-8v72>e#J^K{xw z0@=dGuTtE93tg>AkjB>uV6)n7egD?(JJC@o;mzW{AbBSSIJ#t6xYDH?$fLdO=YK%G zLHi--)90a13Ny0;npDp}F!?-=SW?av52}%;Sof&u!#!0eIqNW+b+o3a6e5Hj?{Apq zOHxwlrAcH&sP~xtml)k;?%o1OB!Z-xj{7Tcek(x*nY_q!>2ynO{xh%l2PRwxZrq3# z;6xKXFk#8sNfw2p(xaat5L~XU%uqEqgFlC%(QJ&GgLWvGHyglitk*)FSkfU8C{OFC zcIATwebY;dI;}OHZ1^;Jmmk|)eTGSam@MvujIjQ}P zHM2zbR`jA9`w#+85E8chYgp|%@Nbs{a1#oXW&g3Pdj8y!Z9L1xvv(C@fB1n?aZea< znM<7N?pB6&_eViwIK9D#<4iJjqoMWn9HqA0GDgAe&&r?6w25Xefq zDs#dx1PWu_ZYJM8d&b!5=b5*?^iChCsO7KUkH1w@5|&wuSZsnT$Jis|y&r|YrxSP3 zd8&!6DL6Js_CzpfkU9Cpfx2?m=2(&64>lVrWq`=aIax5nM3Yo8nSU9x-?g!FiGR{M zRO`CLEX^5rE*e5J2D>M@KJC;UYn={aP6{xSnoDzQK7~t(%eGWZYh@APd3AXc-M-%b zgK5y3nv^PT@slTvyTavgQfBQuKpSio9hs|&7Q^!NfHS%)8`LgR>wwnBek8rD7!bEb zd82}em{^NpQ0J?paqrG7j_f|1<2s*?8cm`G;>7Q+=>L$w#kKH-lR4)MgWeskHOEjy z5dXmk8hBqWg49oFH+3q@(IX2RdDZRkyiu_S$y9io%(^)g0DL({L!D%7`0}8dqpdYypEx ztLS7+>*X!V#@=mtJvap%)Dkx=kIDR4*>nEdz6p}8^=9AZm61lj1e^o?RM@R=0-FM= zGA~z14@b-}9C8fu1jdg0Z6su^?R70`W~fsAKaaiSk~lvSdS^>z`ycc~w|GXBT(Z4d zL+mxr9$qFI4t-C%^H(dai7e)wXpFDoyB24KS~6XWCL8R2wVzx=ch?11T5p-!@ZezERx~mO|$O#gudlxqH)CN}nd!Z9}n@AhbAasXw59E!UXK#$0 z(x5JsSLStw|L=yc`<>E=B8}nRf3?HzWDg~;RN?Ri4u;K<&vI$Mi7T%5-62|t10@i- zD1fvGd5_*Jw63$lR{UIkSiH1*Z<;^w0y;sLFdV(qXZ5YaQ9}EXG9zl+(jaByihfrz zUqR#T8R)=i(tNao3rl0<9(}TNl?fqhk1W*E&WuXw+DfcSj{0YcC}0HoC(MDc^zos+FL0 zm-q1!3}bqKo}l0ZE|~TW;CNIK1l+z*>&Q3~*J_jn8@)+uT~3aFMYy}TBAPJx9E==Q zSNWJN4VAm3DCvb>+C@!@gK&z?vBpVs(u4(i%Tb=Y8qoHF6_BIrB=1o$zV23-{!HTI z>=A*=MXhA;y3@tpD?Exfhz@92?Gg_2<8=S2?Ah)hmKAysdWPXkAj1xCG4r`y|S`GHZ?zOXOE9>S-tc`KsV@lrrB=ZZU(U<%=&JVOwD6c#I6U;%`F z&y%CD8zn`*sYW@X;rM-Qtj&m^m#JRcK^hE`I3Nm7x_ZGp%Gr8c(Wl>#r(D-+YM3XG zc;rCFK#W3Riu9@Ia#JGyRV9sXEP^Y1ym{+n)7fh{-WEsmU*0n!4&DPOK&QOB^xMMn zOXC~EtD~+|3E#SZH1bZe{a^krzfP|LK465$n`!R)IW^G%$`%i|npgxuc7_V95JMPe zzfg*;%;MQf&K5h&d;mOl)K)AxYm!_TZ*+8u@MWZ>QJT~=HpCgh$Z|y8YJBC@Tv{EC zetVB#VIrA8PhWrul@u>>Ws-mQKLKC0O~gPW5iztB$v^-#(C;46Y9!6(+z-I!WsJ;G z+}RLKq81&^(CUZ^iz5eEPFalat2DOUaH55=d1i=euGKL`kiHq`I{l$!%4P9jhJL64 zb+s5!em$82WA4;^V6UthHDng=om0r;8q|=IZfb>2P)IE}L2#&p2&;@3Y*vMY(M%6HU9cPrvNv9OLQ4y~BkR4%?3BTwAC!?mbXA0geyuqENI~kKJl*77R$K1A9xNZa!7X;8O4FL^P{NkXPxY&1jjBjc~lFNb}A=bBFl_eGIr zE9$1*^*w)$c8k*yZHW@yf0UE#3r8H;a|YFh#MYAVgaQ$9fb>b0GE|QL?gV}V>F6~b zUj_k(kaW%d5c{(?f+K@}x$O8vAw(EN(#-x$21acY{Fo=>GB9tD$7xy9?#Eh`Of4PS zu2$f6V_M9is*9(sHsKKfZ7BBCJ}$1ka1_aKb8`b=71WNe)2+f@-Eq$AqOSDk7H0C% zV-B-eSTKsrCCFh-1&7kL8IlFOw3;s>tMZDW-3sV8qv&JpQ3dW@#-U@&k|nCj#=2g@ z+}!>MFgDpIdfPGmzzTdl%}j@<+7UeTM(h&++Z9fQ_o2Ex3$-%;X%G?{P%d_kdq@T2 ze|wF~5wh%qL+Ceh768F3JQUS$&Xxf?xK%=?at~kiZE_}Q;I5X^Z#SyzrZhVLqFr{{ zQ$&6-wydQuI^6AdSKbE9J{Gt^E6H<*6deruu2sr22E>mPWq(eBmnmOblkFz>NUhxd z%;0~H&$>TTjKcq=igvCV!rm#LJBvh^uSQ(?_3#58hiW(vBb5t3RT740`yki8D` zaolE2)$pF?wqh+l(cDV*zbwTulnF}BvT9Kf^cD0~mmE%xqV$AmB4W1ep<%wK?-9SY z^C}>GfN;L+bxkgF)Q1xlj&NO4Xt<<(A~-LB$uVY6Kk3PXb>hDS(Je^xw`dm!{0bO* zzvqAYMvGE-7{X>Kzj85B#z|wY5zp9W(vLM&-%OFCO9~=GpF6P^%+{!SaKV{&dIMQ# zCg{GlR8;y1Ks`l6#YP~Q0z!&mHB#4+WncRNehH`snSs6tlF7hj{t#U9hLuDz5fIV5 z&pk9{RfzLL`USdJD_9anKB~zZXJ1OA@J-vBw)*sUeW}VtpV>6(=8*8;eFB3^I_ znF_4AjoF!gGsB}6B*5}2506KP>TY7V@wiMoVxIT%8#++#rY7EJvw*&b!drLD`2uu( zSPIvVXUcDg+Z=IaP~K?)fNqasVF`KupQTUozm$dFuVa?KZLIZ_m@kkYL(T1tPW$+u6vBeAn9sZ~T;SCuXXMBDS=>P49 zBJYraU${kxxXHeXK=;rvcbwOpTlejf$^s9Hd>haUlPaVGl{m{;1zvl=? zk=a4^IJJqzbVyJXznm!gslpZD3MQY;m8IYbx@hMzh^WnUkjIZoSF+!0UIagIp;C^B z#*JVg?S!CY*xLB5h~7{QVdn=q|H(uNm$5L&l`RA?6uW~%PzG3_mSt^n1e>(c+Z?rH zc(BvsCDXXX(2uTvxLK@JP+si?g%tkPdtd<<85;duKVe$K0hW;4R+4Fg?}N_lN4M5N z__MxZ!=WT4TMIL(Ei}MWp}t3~OC4MeJo~RG*lJ&UrBW|%4_!tvT<>BBNH}O!^4F<0rxG%-G57Fm7Ee zU(rUj$~BdR;y2TxVh8S?Q#F?bd1JDK>rD3D8DH%x6YiOu6Wkxw1&Z%s%j4X1o`fCD zUg{^Ezj31-B?;AqKY@$n8WFsIF~?D2_`6^WM!dsPIUAZu$Qy?4Rgl|pHMUTD*pvv4Wksn zGNYBAapNLWIM^BTqN=hJ7Q0=YNG(Y_m%;D!%sU%N~`HPSLzVFVpSJ8^&T|vtcA^p*`typ z`i-u=pTqJJZZy{vZ=ckctwo1PdMwyewz#Xima$;8r^7sb9f?VCu2sYiPW1|EFG}XC z^-!q(uu{@lm&0kA5-`@UgEWW$Na0xlr5n~%xsu=p0Bkq5MClt^(f*3%TWl_MhZobB zK2QvY7Ich!hY`YU0GPTkFA)G_iHGMlZe{kuCZ#$PV~^_agRujFDL?mtE4P1J|9+*C ztQs77b&hYO`+!#0Fgt?zL&l6ulMfzrqtcFtKP!z?#tB-`O-x=GWsYxSKO0;O6j7vDS5v=y|5spjV8^C z{3%Sx;);Do)L(JT;L$BS>{GA6nXZ~SP7gQ5 zlAZUwGOkAM0h_bY&Jt<5n5c6Twn5u~RlH->X!VV&cv#l7R47jY4v<&PVH%^U-hV8v zh0BV(x4lY_SSN+RG4FLzB#t>r#$eI3QY5>Tj3rgQCjc+Q#)HZT=EDu-Jq_j3uO>2o zYoj(vrb|fhjkYnS26KUf@O5zjdyyh$WM=rT0SQeTp22?0dVEvUw(}GI?A9L#nzAzi zD@<}|>OWy3J-~Kt9*Usy7Li4*2>b5*P)y`jjM0LI$D`1B$hu`H71epW}k z+p?E(e?3&`k*mHM&BA|jEZFe|%Wn@CaS*eA@AEiMEcjrYL^&nCzasim7(nu+f|9vJ z+Au9rp?;Rp%)a#!YG=_pD(y$6~4M9i@qoZBf z8m)1E^@j__BS?rK)>EJd)!-J+M8yC`cG24DAp6KpOhEm#aeg2PC~(xOE$`V%b2X^# z1N3eHrSv4SjhVE{le3I9padv6OlmN1U2Kb&>~RXerOmMO_MX(5_6amP8TxZmuuyVd zThz>4tRN_|^HV|~<^-a~$1fi4&0f%>lA|nX1)P=ys4?AcF+H1cZJLv!Uy{FkH7N+VN!hXE+td4$6;5^WR+y~_2d+&ue(k)aq zVu;sK<@?cTg+w(#F(N|as5D*=||0fTPv-a87j zLUa52);djV3K^!KRUX`6*^8F4612v|@nSpx24!}X$s=o$_w{(up}B<7w4V4@mn_a9WD6P=`T zmk+bNB`%QMG1*{gW1+Xh8pKb4UM#sp9}`Q%{u9QnojSM*4aX!*sX&NU+^$rG zGc$Bam(F@O)Y(7PXfNhW)efLo<7KP4;}1RP6NCDYT0_+6my zpBPnZG7C3zmoWyRQiYyq3))qrQ<1?37lFOIIn(4vuuyEphrqK^2;bGEGhq*Rpyf-< zTM@FhW7xgrn?^=itZ3=p_T)74 zkeA#Dr75y%V=;I^Z0S;c&p0YjQg2nKS0qIzo@wO_T7QZZE)mQ`6UClfD=I3)X-0YD zT_CJNUWf`j`CjI7@D#a;RF9o_yFJ=wn@d!x1lLy5Xjg8a`FaNgwd9zZJ}NuUW#{;d z1qwy*ks}Q3cGC~~b993|ESkhi&qUTGui`WPkRMZ?WEv- z!c_e0ku427mMn+#Pp*Z9V@+srXj6bnZYn^)OU9MRrK4W!(r(zyf^CR9aBMy9^?c%ZO38IKT$<@9-Li;Jcz9RBi!h&caF~upGUTG5YzyI>g;MuC_8M0d$)l?rwIHKFv0rf(PiE^#58U#>!EzJ(sp^*g8H*_?ZZjNV%F zpxT7o{xDq+S?nRWWPTBbBS17XyvGGr_CJU^PF8RRr2@gc5oy_-v8H*go@VqSIBNJy z6NKdqC=fWqlHYcWUVSDc1|Hb61p0y_cbSZRXPX}TB4&RzOQB_Jyq2F#K|iE4a_{xR ziVlhJp$vbYCRg@U-A^J7DPg?Aak(@zn5x9YouKx3JYZO^`1mii^#>1s@X>zNPc3jk zpY+J1-Q^kyOjHl4ej@o+-eXSWO_0SX)Hs)OkQB`|_(dWn1gK>}1Z(LN{anQRHA5AE z5>!QiBLqt>GNpX3)u`pfvHvp+dBl%hHWNbI!(fsoV}>G z@VUVSO$0JDSQY3e^EU;gAfu>awwvirkVhq&Xj5u?#6P1+wzfvnKEJtc&dsGPS;uqN z()W@`)ITi7_=C9yzS6mhESra~t8VO4x7xb3g=PzvW0>e9bl1O@N#KC{|Nr3LL^l<$Pl{ITy_0{lyS%NTr zU`(s0Q1H7f(N9hSt;Am#c;fj5Xf`bpbDY zOnHx*Vb@X*-lg9!H@E8v_*yUZ7hv3o?ew|`xGT=m*$tW}BbLzk6%;)P<0Kz~R#7})A>K165kKEu-Cv~s+nVarzB zN>H~`d~pNh>i`3|XEB8|^Dkz#YBL;ek%Rs{orAKc*zb1X+?qek&8=4=6aOr(G3;u3 zqPc&zZI=wDRq!NA%pl61|79bOvq@;Yc~YJjN6PM;lbX4!g1jwcTojRk3Lj%VK~Q!;^KnNgBO*44`!SN%^4?JYZoU%e%dX2YLZ zh9*1WM4E;S)7@Oxgkvq2vTWAdp08kPqaQ1|bE44=sh9+AjxUwW>{rPVt@9RI8V*qf zYr<}I(_a5cD#}C_@fh4HGXM=2t!p(mo#Ktw8X=9?qeX%fRh3!d1}-f`2c=oy+1C{| zA$wOkrm$GPjR#z;w6^9O(c*VYlyd6t_@4?PpRY?(t~c^~Yzw$f?w8FA;~C&nks5R- z>_SodcVJNPupVAVAUtgLMlZQV?Dd1-UC2QOovE-;+6US9i4z`a@H3}2oKq*VNiu(h(4g#CLxzl*X%}x&s93qxhFi4kN1E#SiNa^$|^ve$pC+j`#3hrmLd=UwtVF z9gL2t**twdeb2++{T}T5r1-+6a=&xgk3%uSy(hIK#^&<2XzEb8o@D-bb0a4rzViQz+dyu%utSosLdQ8W)mQhocM7Ppq^i`1kP8BdL|C6Ko% zbWvc*05;`UwJ1jrpvDR|YZj*Z&?pfLpCrCPP?cX&hdf|vNAY3|SjS>m+n)!5d;r_` zD|lGAMjn{2P-Pa?L2kClATq=aMHSsAvI4S(A`eA5u#tM3LKT|Ny67yn9p_$~i`?RC zn8;o~)%73(nbqgCZ2ud+?ss-?ACVPf>07QxNz^_8IMpLP`j?i`j>WWXgM0E-ZFkUp zXGuaAeX+yi%Wy5|;_a*-8Ar&sdP@HXR|tx#w6SB4$(ICVU(RxUUoD~*sRI5I(kS+` z={?7v;yAjMueWRk0fyIb+W}t_Czwq^iD}UAiF#ZZhZ% zXVTLlDjSif7=(a2Mb}pigO>hf-4vfz>P4KB+3s+j&Jqh3;oU>uwX-h3oq|C0m=F1< zjhS!K%Y$9K=tl{fRt(DQ(h2*m^E>N(ANAwRssxz*F*y;=$~T;3AyG=q7uO7xb8?H{ zvMWw}i*{#{VhzUh45<6?h&Y~6+odC3Rw7+7&|kBC`+mwUVK0fJ37;Oc!?5@OI0_I+ zNMW*qV2<^!SR*m=sEVc~j0_vyEqyj~EeP4uYCIfRN0THAED!HTwOmv}t!P}>!h%8T zyW17HOc&QKEUG=avhFO^2#cmUm%nB&Vmvb}?#o@++r9Jt_7mL&q^lk@rOB=pLINm8 zq5fQ!rb@u0LLmHxMDus()9svg@hTf?c@tv~ojHt025(qYfBqy2{mK;{*xXWBp7+E8 zv5$Iu9D8nmrCDAP+`1 zvGK7=QN2RGZi5+a8JCXiA`-p>R8>Jz1@x~gGgMM(U;NFA+sEM|#JviXx0QYKeBR@j z-q&bwuLDx8!W#X+WqC{!|2PO;?zBcS;hZrf)Vo07XP;VecaKj925sXmMd!GbbnUwm z(XMyYt&Qe*m+%E79ptxDI(B%o&Ow8!D6|$A6~fB<&$%I;KEaEn_Vz1pI>766$kYE< z_^jdG_k$ZZ0twfeIfS);H>QE!r8%_UH?gD5S|qyv*1_!esOm=!un}}E1FGkeX`5~P z;a&V|IEaWoT1tCk_Wom(Kpleo@yHSJ>4XEMS#1Sv2tqrXIWS#5-h$2DF3(Sf2wW_7 zzhVf){IPSYe%cL{h=savxKrtI4_3OIMt&#GtjC3q8;Rn8P?AtMWji|SU08yX;3KOe z+p$mOAKYDdl76!hOhde)Jf$dgmr+KH6bdGAE)XTMb^=d0M?DQD?>Al$n5FVEvESlv z?J=exXz$@7!2Wg6ME3}s3D!5Fvn*9gUzrls!A)pwSQkB|0XupJp0z!u#$OMCjcWTk zQvEhmDpp&SVTEITJF}{3SxC9C?|iEbfGb?%Ctk5BWiF%g>bFnMdEO@UnTP?f%s6SL zM3~{aEaFB26$qlKxtZX^FHB{s^5#^i2o*@`1Emi`>8}7dqgxP6iC9vDG1z-tA!>?(#DvV zB?Bk0hTu11JN3iNCSQEBvi1`@DR@+4>I5}w zD}V(zLKyAXS^{pbD<5MZ;LRRzxf!ZOSqzFeovZAcmStTho)jzoNag$!san>KU`a3P zdgxw;WuH%?_>0`y6p8QViJPO3x6*7|v~KgJFH;4IZN+lF|2P1!Q;>$^#QU>YJ$q3h zP?w#ixwSNh(uzXQ3nDdx?uoF=yN{|`cfea+PF;3-e56SQlBVhoj}kWe1UB!W*V6Bf z2vxX~SSgX!**LY#lL>MlU4nvr{?SGhLu;+u!4HnXU_W9(wF7+s{>HFjtfD1p*tzk( z2AGbT_U%_ad4{P2{!ewf67qQ6q1Ulf;GTwKh`UO48DZIxCJ+G6kYY%DpHw1|_-|(Z zOh#HF9{-RpNx$8dYl9;VvyJ5=7Fx&=rZC&YFMph;t-ow_#;>#l@^B}iv~+#k&*mR` z`=In@zIy%94<+!F*o8J6BuaS^9q=iecr?E*5uxQu?<#Ausu@PID<~g(I7q#^=n(ok z$=5`RUB79Ftm>UcA(zESETlp4Cf4q0RMuq&DJ*4~+tI=hf+h9=A!s8>m>m5!u-bz* z8{$Dt-}foxLI!EGih6&K^Ey37V*7A$h4|hjSe44Uz8d1XpYsqs1d^>2dk~~v0=n+V zlExLN&6ALiNcZNx-K63Ia5?jEhxhjVG%vfck^2lo&Fi|1^ybtXd9jK?ns0d>i5xus z*$u~19msFS&|Vfx@bcvcXkhsx?nTc?6|!WG6ut?a;*0==Y_))O{Z_}{)FaUhd4GvY_O?a}Og0gXL4HatL&TvM5`0cVpHDK93)mVckL z-|Pf0BGV0Q95WzW%3Xts`_ zS8mKI&9FllA!Ld-!~@k@ZDB|}RKL=<5#-FlKFKoF;pGh)KnTrvhVaer6_veZJ!60! z;-+qE=o74SJ8w}bs5N)GIZUs#H8`&gT1FVlrsTwzGF42Fh9 z=Lwnf;aL$h=_~;4!%0bvkrxiM4#90RrOE!(p^gU8k8Ww{1x(5cBU~<`KNA;6C?|v2 zD?3Bx<8%<#g@oTommt|XPX44)z)V$&-6BsvkPJYxAm0u~5@mtd3XS{_{^<~gto9mU zMWZe+1AN5rA$IWmIp@D@An}+KO8mkh@2H;#ui4rCgIDw4?v$#BdTd=3HcgDo&PH|sH^SV4T z--4{HLQKhU?(tbki+O7e*tx2_IY_^4Q5d(fvT9G(`=axL2Q#a&;3>>KKLxCJw%^}* z&A*LmX5YRg8coQts3RA_x5xtL7IG36Z~i%QF%*=2lWNeV*-K>y^*zIiKZaGkrrxON zWTV?F@6cWsUU<{nj6o_TGgYHauv&o+Av(r>vlIhJr%PJ?cG=a9hgX+Xz4ac!<*lg! zza2-RmE#z!yD z&SI=0+~>{=$nQ?~|D>I8gB{kjVG|p+%%E<4WGt219)vhg-0v4Rocsbr8*2Y(W9|jp z`z8+NSt+;Nso`+k>hE|~z{XL`%={dLb8ja4VlZz%~y2R*5s`2r`E;&=3$%cPf!pYeja=KMpi*4Zg3(U4duo7v-Srf$mUtq!kM zWb6UX1l$c<ZqufEG+g#RsIV%S%|y8|4cuf zEtUY-i^VWH9TCQ1OKoWk_Xo-FH$O0z9uBE5G0{){!#UX?H_e(J;S+NquNjxIXe_3^ z$-f%(i(9`={4?pydi^LN&@*?^W#Q%L_%QycM|8J_Q=n+M2+-c;wti|wXDJWYzec2! zvnDLne7ccgXly2bs5}X*QV#9JrXvQ`CQ=|=TDH-*BYkT&3TJEf%eTr&{Ih4qbMbUp&lclD!fSo(dbE$G*^D2~i3e_cUS7uD~YEz2|w+HOzu{nS^yGwUe%IQ36pn^r7o##NS(Fs7`53eb*ph(5S3F=G|xo- zeaUwdMT84A8@phcBK0;o4jc(EXH4~(qab83js#*9bIyZM+kW9s zFu2Bvew<01nX+IW1c&Mp`Nv6s)S$(;M*D5B)!}JRm_*~2TaAmAy5#3RVI)ptK#gA7 z)2PvTYeQ9fBneti4_1(A`_RR-rE?|%ZO1Mf6)nqk?-dl{f(MJ0J)dqC{S4X%?R^dO z-q98U3;;_>I}yHLABJpJlX zosOBi`*eyZCNS;TH5M-9r&FBwSQ5LP#wP*ck{wCX^RRS*Nh8isam~4jhu5gz&lS~~ z5c+N++ueKtWj>7+mDX{+)K%OF%UJ8UcG|Xe2VB&5duuM&0C$^406EM>rrRoP6qILpDUm!v5c4tN;pV0T7WW`NeG#-*`gWv;0BT^%DHj#*^ zH@ypnVu13q(1jR@@|5SSWsLE;TK#n#fTQVAUcv(^po-cAFDs&K7;Y9PGEAE4ifBZH zigW7~{gne6)$kJMhBDuU538B}(!Nw!oV5r1nnhnfmPr3_`iXp2ghAUr2*AFsBY^C+w6jdG$KGDIY&^fVy4pHj?}CSW%M?j z2TyqmAuSk)EEVM{*2DQ$$!8ciY^iH4|7PLOrg62h0!VF`nnN5cU zc69f0Ex+1{6w2}4Ik)HuO2IaIepnwj&;22s{5%?-5O9&R$-YOvZr(+WZ(VV#S={}3hmkAd;Lx}FJHyv%Oq6%B`~2NNT3ZKHi5MJp zn|yK(6%WJmmUUS;;`e8x2t!Jb0X_jvl?wLrp&4ced)f<%1}n}gp*L?GQ+ytH{uXum z#}25#7_Fl)xT#51joW5WpJ1Yt%o?=cSMC(#>g0FC^Iu{rql4T!jcBT?g(#JHWN!e| z;WEsWnaXxe_}&i$r_J?#yzPBg{1#-MwRrZ{tT7b1xTN48cz{7Q;1m_4xJr4&u}^ho zrVC{iZ?6!JIQh+86nA<=Z4M-`yH)awH9ovEiY;9X$c1#=M%(cV|)YBx=SGZk*8( z>B7Hae+&lq7JENvvkO5j!{rCF&B%l~#Sj01xz=a(x1QRUeftc24&F#gfS_PSv?ssK zT_!P!a<>eR{M%{(+7ZEFm-Lnvz4$FhhM)x{tSYPXVlh-K@FCl>b@SH`{-Iol*TTvK zY|aG0v6TAjL0_vjJH`S%)vqUH?l9u-!^8Cpvn8Sv$}@pEf?>_G($36MAS%D6t*A4C z&BYEq#m>St9R7q%<1JvaHAR54$elVODuUIcJSsp~{!)yCVk3Rqmp^y^F394*1Wl6+ zCY1R)JN20sGh(52r3q~0G$;!sm$5Ni>YKrOD}MhbCYwCItVW33bxjgAQy1LRWR^8) zsp$xBXCtdCZ9hwuy(x$lp2fp!}f2^J5u^yD%V~R#hcZ+KSt~lS8LMY}>8}vkgi#vV zwYdj9kb6%A!~Eo33jZn3pTCTtdi+k`00afJa{z+ideXdoS_`KoJn)NoaMRO%XNmbJ z<%HLQg>5U|MybU$26=~+ggv$mkfC(Eu3qv8H5oC0jr@T>?bA>cT!9|4ohC=ZY! zI3_t?L&VfnmH}*%mFtKqRoAVLV98~jN*qvDl`0>9%)`UQ(N6+X2y=wWP>0`OKw_SK z`esJtS*9p>R6XNF|754sSjCCPfyV&P<0wcLa3MdErij3grOPvXZ#2}*SO(-)HGyF- zy`RbyHnc$N6#Uo(!j+z?sbuX%k6dBZ2U%xQOb~i2 z34yHlUnUc!pUt%2Fe6|lLWLEp7^PFrxT2<+_GJ=}jiGMa>+q~wA)pY&th|QZbDG4* z$yNRNG^k&mZ%lojlc&A3{(}>Ql8WZtRwtRu=$0Uoo5ZYq$Ct<)I!)}Yn;_MP3%IL? zKj{uT<}Fh~Z|X_*7(9Zc(zZHaE!_cWcvz=;^#Y4M?OiLS#92FX2;Lh`FsPh~8awMD{q672dFeg;{8%M<;>A=!i20q+_pwlrbiI##F0_Ua{ z6yp~mVRO|j*hG#bCf$)7k}|ol_HT0_13GC`TV>O`In@JkU|}4&{;?+6EEYW*a5MZjcg@K%PRwJ zNRyS%eV=G}NBw^>09guNQHj{+wi`#NynMVDQ(aeN3a041&c7x=K-z`|X^o(CBsAF0 zklJvFiF>FBa(>nz#9C!IbZ;j`d`jlZZlFiBx~eneH)C;ipBhkMI6-h-pxN(_z9f zUFZc1oI6bO!}oHSD^LEvi*sJ|oiHg2vr2@+u{gbJok?;Ir_>ylwlg@+W!c`#{V9@m z!r$ZbE7GhbI(zpEZLKcEV}57;7XcniQ(PbrwNOInMH^lZP5dsqmh0G?J2!_DKC6m? zWQMA)h*hyeKXL>vjzDW1;hR zIf<5{bH&K}+R`6hCv5+Zobx@=;(d(0*}}Y~!W&kvL%I=yZ&Sd_y0v#da{lJ&PUs+E z)u#wlr7ias%)k}7Ni%a0A-h(hKnFnvuueT(BDwRB z|0e7tR3A=i@q@8prh-2G%4MTfsd({;xfF6T8P*A|$wy>Gye3m~aXoRE{2gZV?5#Ky?^XF(Cp)Nzsd3pEf!Ykf}botqadSuJKNeeu(Y6w*Fi@#iFlPGs_6~aOHl&RRlo=LsPDNBp*>N-?9 zuP#oFsW7p~4~GdU-;*@~&|5vubI{Lo`@B682G7>hC0LU{Ru5{K?@29+%w63y-CIuK z2^vfYo$za{`GSg1l}gMM^gS`7#HyJ;&%~)LwtTj~1fBuk_R-c2!$-|3ZX!PkM@C^( zAeEtWTX408GIRXc-?38E$u1mF3d&!@L#wWW+Zod9Z7KcFBR-Dr`?l{-S1~MKgvsU4ps z=Q|kK+Xml3RGUDX=Fd*97RP|^w-R7l!FL{)tf`8#JAk9_EEEU$+-6gq9=VpAK3qDc zqv%1mf-obPVlS<^C=yO9S@wGQYzoHuv~WvvG_dEN(MuSUA*sD40N^^LuCdj3!JsC3 zR09zJUAPJIHG3(5n9p1@AdRFb50-dy^QlBPKV*Y|`4;3k9vI`^O`sTN{a}(~eP6n< z6;c21i2K!++Nyp1u5w+lZlFZ{&yG#P4Rd|WJMIw%2ayK%QWZH|MfoQ+ovj6m6C1!C z!Wl*f4~>Y6<5lIy?^Q%bS}i8U=l_2MyUJ;dch_j3N?(9Vi+dHEY)jLzE=X2d@H;Rf zY(?PArg@TBRb$x~4HT9`!oO6BkSlK**KIWG&h_B1JY?GKJGc!Kq?@1_JJxstPs)kI zm$e9-A1yXilzwx?boEjGVgM>-V3MK#j|-Bdc+2j750XogGpEQREtWU89;cQ1ni?`a zFB`*bDSeX1gMfBnX}`v_NbA&>0$ppxnZLyD(*X2gJ~Uf($H~D*e;<5JWgA3&K%bFK zY%eUB>i_p^(E1t4gD$2p%O&b9d~#QEcXE~eynsi~b@v&L7}^}HL%?YZ+v95;2t z%1Ib6QZ716YNTv63J|>TcRSyxJd2oEoi30VjM@J^ems}$=O~-wzH-tCCPQYNHEj*f zlg?1Y)1cfvdRuiTXdey8-*{N$J>;P5KX>*UW@*sfV33iF_3Lp#@hoOE`&?slR5pzZ zhiV|@c92&omQJ!36Rs>yqc~^#UZGt^{{7BY=x+wjJs7Zcru35{nsXXCjB(4IO$!YR!c-)yzQB~tXVFQWG5tWNx*Qs8 z!`?O`wWSmId1+|h^+x;w1c7!GuG=CThZvo_~|5Kdk%BJl5sGAwq}+4#~BHa zXI__(RVE#@{%7L|@8W4YtoG8AYYC*QQw*>?B14oZflr67#7_EQEM6j|oNJnhmV=&_4_^X)|$w`i=}{#y?his*pvUdy@*(mHzIj#9gOIymMIHdje_+uigA_O-;L0 z<3tR8ytMCK@jJ_DSqP_|m$T6mm&gxR@{$p^HVC_;1(t#_G>o4OUBgHIzDc)oe-vEB ztSop@hh8#dtfe-Oj;d0rPSe+zwix%_c-W(+Eayh!#|)tN<0;{Z4Qu-U+CIJ6>o2>l zB^b{y5!q2TvSE=s$@#rPSh#z&Q{`)USAcnHN#dD~8~UJy5HX`<-&~G!@f|SL@2vnz zuDFC0JdksEIB!9cNck`(HnZ1)+>p946m z97gu2UXcNt*~jh&600cL;I;tD5a=Yl|Mwe_yCDI;-F2 zy%_SJ^u;J@m0gCyWsV)%5`^`wbP1SJ{RHp)EZ8Iqx{L#gzwTivOB-Ui)5h`%z7MLj zcyLBOs{?Pab_41nP-9_hEYHixes5 zQ2Y_CP0mMluuA-fOby&v*x}XKQ{ZmCcAINW@kns=S+dNs<&@T-b(F#*o({ zIpY|%T`yIv=Np_w5-^ z5*V)lU8mxuZ6kT+6K0dU`oelUI8sYx>c8R=25 z^YQTn0-UJsAT#~b3T5RyC~gpNo7uogLN%y$IpI-pipkChz=oBVC z*%LAN(*?|%lqu(LQWL*bzLY+e<=Q9i%T2H!94-bdaFpU*qa=Eq4F1<8F-%5+H~}e+ zz(bXwEb&9-f?!L1ZOsEUk&OwwtU6xJNnXC8#&_sywS9T?Vho~dj37&yOD60s`;L19|iN}&GqqV$~!lj!*!P990JHW?A))GVn>WDj8154ux})u=MH8W|FN+ck5Y ze0X4FL>9ieKooxC(Af%29f0)I$k`8;w4M%Pb<{yBBgiFr|0#$R7_3D4|LW)7;9OwE z%g4h)QhG7wmyQ-}Qi8P}2+!dCKq;3>pQA|cN7CmVtGBsKA>ttO5$>WTgWp#7mbTj2 zG$2;51qHwm(N5UTAHabr2yU>E4ZH)onDC8e1UWU!=U`udY^Q8}k&8x_vh_J`FE!Gy z?=E!1{-w87FC@sJ1^P{s!|@7|cE(629t;MI`=)s4BQa)Pf1D$!sepD1c z$oKyrl1#G{24K>s5)MH3SK(=*FYOlL{Q49(Ob!4M4@#pq`@oF{Fv~EKp^SyoYc+$g z4=LLBt&5DN9+lLPtb+cQA?5L+x4Y?(;AEV(IN6Em+MB&HaV;hG?>imS%yZPzGJdvEhAQbGMV9rA$6qW4R_Q>z zZzf}?zcn?;ebiYMOQ3gYf&DN2Sz)%|Bq3LUb$0XY8?GB5frqq})wgxT-e7CeBGfqY^REi-5GrXMdij*8^6wlj%Wc7ySeO1Ch|#O88( z1wS8r%jbM+-<0*shtC%~wox=sm5S_r@{Fk;K#}GAK~RO3lX4Q7SE$v0C)%dfAexND z#Q_)UI1QCG0K;RJnRRDKN4*>*Q)|KSY_esnb%X0*nXY7DomzH|O(;z-48gEsU7yo5 zHys|SiclY{d))@ygs-12fEGGubzt-yfcQzKlA$%A3A!(LA8}8Me$?vkwb22c%sg2YeGLj^;GIKJpK71Fy zcpfgu=8pH|O><#~OwNFX91Ul~d1wDAGt1eYja=lhq{NQMxG)~`!dg-_t;Y&&B1rV2cIE@a_5#Hs%hW zoNf-x9Q`BEj#RLsUjH=0Qgt9R$Z`0eKm++m@q8-@NTphb=$QsM4QbS-?QQ2U;d+E4 z(Eo|uwf(hrSoJHjNgf27udi$#&$y{6lX(yJ#-Nr8_4)qQ3TxxLL%hBPq?EEt)nC-N z{V3>eOc~RiGJtU;8WDgl?>@Ks;i_Xqtl%4y$`R*42H4(#Qz{sW>U+Z)1r3m+G+sXNDeM-<0aa2{0H6-4< zr_{3gsV|&l4Io9~>CYr*#5$I9j$5SHC*RFkul{23{i@;NZ|5X|65$~zxhuHhNG1eh zDp1kjIm`^TqD3KobXW%@`{5Vxel0*vWr-f|XwqiE1Vv8UNqC1AHQf8m|Ic4FUNj8H zTL+g_z67G{?*#dQAY2kq(Jm{AXV%3_tKy)fz~!w}MkRLxlqX})TlE7HpmL>hUv<|M zZJ+ABjNhZkAV+`ixV3$vUpR^6;F>#JMa}FolU3u4*e-1*fDxYsiKpXAs5_Zl2T~iM z;im*QjI-9&w<4W#`5miJn6;L)VCg@mS`iPC*LdB!Wd_%4j6#x6gsJxY=Jeb4AGwKW z_R{D*1R!e<`iPy175#)5yD9!3Edub~>YI6F zksd7*sQ4}`86%#3F@p%sMfPg#lAE}2ddLC~2J0}g826p{DMf_&uKl8j1Fsj@FfX2{ zkmmQoT1M{Ri8dM-ko$uPpYp7wgsn2`VPjRDn{2`M0AwEXc?B45&0JC8EY!U;B7%z< za6Km`F$5GgZs)p-HT9mXkQtXZV-0%59jdr3t?x6e6LL-}F7J`jrI032dU8eInux@( zAA0V_o5o?{rksjOQT@0e7fc5^ZP2P)cF)t}zgi zq)+(8zjw13K7d8)q`xnE16Y4k(%8XE(Ie=!NahVY(2vc6GJ{_t7aP@QCE~Ff3 zj5|GxPmk9Nd278erJ)f5f`%q4zEV9C4@c7=rSF#F2Z`!4+c1<~=t(0ULtzZersNAa z+P;1t7?@7##Rd!+(Jj(}9~$9vtive9^FFHKd}}Yvmb=MbyG%L&omlCxQmDEy1jw=6 zA2&T3gT7m!Rb2nkDv>Hg9y($jwV!NENt~*#!n#kA&EZv4;pP6)%tI5jNo@tTe3cHW z*<+X2vBSJx@eqy9ecuouc%}tHNA|#WQ05)o@afR#3mEm85?pQxCK@ETs}Xxz4t{gtI%C zCX3LwG6u?jPR0J}+wVmxnrkxCE%$0Sz7Y}Yvnjf7#|kKjmS+JV9Y@W6#V4zDKdTc< z1OEcvEkOiBB;2mw-mgRnult&g(&~oXHlvc_jRuQs9Th3^83*Tn`bt(NN0{@ zZ^uo0+GI;DSq~N$)AJ3bcL2bS-<@K(kR*TdeMbmxv&Slk_36<^4#&H&Wt6=BIIqzJ z5w|URfaDJdq6FP8l$>1O7dn#EmgSvd>}Twq(Qc5Z!(7xZ;c&W^SYsI;`=$D(%`2BH0WVbmuMHAE zid`I_M8arUf33UzCf&`k0{v=n`FU!f>=!79KUc|qxtBsyFl1zV?3{pM^}^Z4sfR&&Yj$oGk&82#e+NFX{$?65i85C^m854he36 z%hC|bLd%X!lTC(Fg{JF0Jx#OM`c)M1p=WG6P8z9UT1 zw0DAf&Lq`wiJ2GsNOa&IbOB-Fj9=C`bgsar4Nowoq+?*6h@1vBF7UNEH;nZ16< z)Q!@_eppC8dfB6uX3qq17lX7I(xKbo)jd(;SJpbVexn)EL3M^9NipWuLmo5+oGwyT zl*u0A+31?My}tKq)GMZ#jw>QMYxG#PDmuejPV*{ZujA%JV3pNwXY}Dz%uppwoqov* zllM)S%WW#i)#SFvau4KX!PC&mE4>qqffc0EA01q&vU(4c3K^Vti}_bBz+6WaK!~~O z&mEFaSN+Wdr{mJq{Mjd23KZlk0>-EHtP%Dv6!vmS%Pp0BtL!OX6?_v zzk#A?=m=`U`clw#Z31~B@yz8QSQTvrk6WpsGNW`CEprQqOP}PRNA5(ll)IF>Z$Q9G!zn?MKHsHjQL8zJh=OeyM}*3BEol zk-~-wtzP_)BA=m2m#Hx7ILKpp`vKxVp=hE0N4Cf*OH01gJ;}k}Y+t(B24JI@xFM5_ zhEHMqQ~yC5+f`L1R)28_49$S;@Bo#_D?yLzh~oR<{)GTW>N~c+3@AcPD;io&&K?=K zmsPvC_Wcu-xj`v-K!K7w(y$=+zx=jGH!l)AY(#N^p#+VrDJ9kT7Y`ss==XKJ znwKF2!04I$+PP@VvZ2x5CdwYJX%phCgQSA0!;VxBB(8q)vlauSkVW$DanzL~&%e0y zw1ij-26*Q|@PE+QT?4D)!9EY%V($Zo(h^`swUp5nechNU5Oom`0Gf0b(gnsGwZ z!NyI_hoM}(D!o3!KTe+5JtS`H)Ydx=JJ_TT_nIok?Q9^C>xeJ^UEvlOZNKi}sNT|M zbL2;ig`e*Ydg^s8To#Z--oHfj#D$ZEDp%dg`ldtNPwVt(o_CtFe&HO=$$|!}ovX>R zy8j;#P9^!=1N0Y1_6rkEnG#RScRnpn-_V5N2r6Aq__3-A+eOdr=c?ZYyA@>y`MJC? zEBkg=8M2y^YKV<#g`ma2xELrZq>mXRG36m%MJPNOAiOo8DdB-NS|Y1^;!-E|4X~N~ z1!3c!sL95Nrei*sx>5XUu2~4wiiAnL0)8d8gc$s$XG3EVZJ_h&X&c}9?|f}ygz(N` z>`KCWEakbJ#tfM8>BeQ8m^iw5^bOtTz52VMASa%CbPZ%I@{C5PsD7SW==&s#z!=O3 zmvm?2dQx$Bzo*%U4&LsBNLC{agSmHHP!{&4za3-2O{hKLKuF zjP*=Cd9h9EQpPU`$`TDcnB6F(y^sG;#;qo&Ci<$Cc@7V)K#6${52F?DWuhxp$8s(~ zV@Y}}Ls}79HY{z+XrBw=bPmckmw_i7hs-gCsut>rHyUE8BFuKt;JuPh3;IH;g`#ee z<+NQH)c2X6vdC7)v-KvBRS}(oXE!_7AxJBx$6D%CLM$(&;UlBPoL|}hLyW5WO#>W( zPoVQ9pIlXC^>cKhYNKC^D^WM<&zCzsB@?$Xn#2fOW|{|3q9GFNJY?*Zm3_En|DdZB zY|H!)mE|r%oPnK#HS<+&h$qfhI+G-_k1w+!+aRW+ToQ62b6)CON z+`Wh&sApYYzx}z8i-h@aETvF12}xLzOjk<1S2I0WTi^YwJ=e8mp-W7fEvU(E?sp{W zrOQrCcIwH-0o>tyZopbot2P-RrTtbV3{Fw)%b+h7r|NfaUP@u4qRA1S6h~`%>6K@K|H?pjCRIv_V z{jB{{2n!Lxqi7NaH{cz;c8hY|e}E@|6ps`v%RGP7S5tK38%dljwXX0rouN3o^P zoQ-<4h;49Yzz&*-stMrf)n{;Nc4BxVHJuy#4s1jihT3lQ>Bz_=(*n>eo{FFD!KKI^ zL4MVoeKo+wX+Q)|pv?(lE2BmR^6Tqd{>k{3L`snE_5a$sCOiAdl`w2tpdI!1szZaQ z-Wzx@2=Sp;P`&*g4Trs@0Ksss_4f$~JxQRYu`xflAP$qF*K0ZtDB9NerUdIn91N~+Us)(*{}{_Sy;A^Ufy0|rF$dbOv0u}O0|H3{a z?MlI5tt>}X%CA$n&YS`i?|_voJe5v9{gVEAg=5~*5sXVljk#}G`cf4102-7o z<3F1~-vv;u9L>mkfZq|G{C@#fGq~*NBq_vYIL^w=1wT46XNOMiP?%0rK=rcEdGnct z!BbQ`lqGihlq-gYYwtsWk>BQijH;w4tL!LdRqcQazL}|l1e_Fd1S2=;?aX0N7*Q2J z0=xKRCBzarP?ecvY5V1{*4F7E?kpcB3{3k7uaKNduDeZE45=uw{9C|@6YLJJlY*q8 z{9)bnS{Do}Xy-8AdUgG2e`y|jmgzcI3?bR`>*mwJ*3n^l_^&XcW^>P@f3d*bZCAS^ z@T=|OIvCe^p=up#5`_vC6rgO7k-Ay1eOrO~_@45tav!P=h@XkZpALRt{I-%$^QYb8 zT=epD6biy@I<8R@?C{2L8RgT5Mprlf`C1fYjU@_eTa zbm38v3rx_9`*L%LTKH-$Rub7yY|TY=zAu`ysO{=9u?f64{zzSQg`WVN9*=)Kmd`Un zFJ7=l{kwgTl`qZa5;4SVYMP!So3_nXxa<1Mo~aawORtx3m-9wD%6T{WWwEtnXRrqJ z(sClHu+1$;QNf3aVI|;ec3Lp7l*V087{1_2=G=`*;U$Ql|bYT!GZ%Ky8{1 z{!;CutwZeDex_s01#%+nc;dkWtixv${5^O5UrjSV3{!AAKM-&3iq;&{)vjwUxa;I> z@6*hBBOgy72N3vvxIE1bXFu6jnsZSM9qh;YSMvUrd3ywm}{ zNS#4i6^Ze9S!4iMYn-|AVH;vc9PUQfmvb_0$h0#`>!nI4>YL|iNh)Uzo;5b*JVt3x zEF*Ut2Wi-daO}W>QQV=|OvOh3cOo4v&V8XWa3+HiVBz6O(%oazE|}-maDb8eBv2$K z3D^kxu7FD9v09;c^U~KiCZyA9f`#Si1pm)64~5WQ$J)HU@AMXxUgLIzlo%LovkQVK z_~4mKnB{Z(EH00NI?7;q&|8be**OOCgj3XWa$Z7e@+-9?MgstzHJ{)aUIa${vjziQ z#28&UX#od{{R(jv++riBrt3@^V1~r?6k5@&$~#IAnp9YS>ZaKJEmv&<;*=RsZIUQM2eicIo;vxdOdhn{hVR)R3Vj5K+wToMoMZ)d`S8Q!AHG; z9%l(Q`E~I>?4)$&R~THrW5UF+18k#5OcRX^!*2t4+xD7tLQ7 zsNR*x8ttK5mN>%%x(SOAdR1Md2^d*m+7;=P5@bgdShhY-=g)th>r-QkLf_18-{#-5 zU@dBRlMh}8N%@(-F5W7AkOKoe!Q0?p#NY3f`1JlJ^2_RsE1VN;->zQ-C2~v2JQxD? zNzf0E71kgA;;{nudreDiLsb8FauozY$Sig@uvjWX)?Ug4qgjm?LtOd zT3^ti-8V4@O^Kdt485Q7X&VpLhG01yF#*Ft)Z-|fT4?2Yngb<39yomLlklz!U(`H1 zj!md%COS9idq@&WOa6&n?s&C5IoH9|L30IZ4Vx2}&|cG;;c>_Zea0}9j#!&n>zagb zvBwf#!5U=R+6Zq@Ve;@uaNFIN`@cC1Nt57l%YbqLD+$OfaJh57?z?5;VzkR&wYv^! zw8;A8`vukvY2ue8pjMPrZajeWAHWpPE_0Sz)Zrvm`hcXDPa2HGW`e~Sedlsb0XUw$ zm{En<=Ao>vl}l*)PGkiWx#2Nm0`OC zQLm*6v|~2>kt4USOsm3$V4%E?Jg0i^(dY*f1Z|8ZA?spfQ-N{%?Ei{x0)q#_-4UAr zHtWQ1%-pz3;mNp&NMx8LawFQ;b~R=XU|c#*MEoa-)o-hPdJBwu2}pHP;dP8((q2ae zfQ4q7gXLro_-l5jcY>TKd4S1qzOU%b!7+;Xg2s>c-oUD^Za0Tep3>Y!H%FL4i3L9eF)m}`w)s(6T6Qgs7&Mpu*0c@B*#YybS)49rkB!5!Z^Ra z>bwRC+2ebgZ=fe7kMWTS9p^?aEJ{w^whP$NFI*@ZMideNY!-X?@alSsduyJUe*+VG z)kihsY#+F0n}DI7P@K-o)}(^7Uhyx8ZL!_=If?QsLP#6fr|^on=k;^9!h|t!?B69$ zT=6Y7=Dq3=0&YJkoX8daB7qqrrotKA4s#&1TgO3I^SE|~{qxYQ4wB^lS{p1F-xHc( z_h}}@U2|taNi@&!U3S~lIoHmxsXweYTwVf%Z2L!?oopaDC&~SZbSj3TtiSy*eRxXT z8W?I8WaF#2mKY_zLPQ=xy^7R&6B|NXl45w+`r!@Gm&~gbKCUS@LK$`K=%%m+@}?tQ z!WRDpD5tS$b?3tx(~KY9!uJ3CZ%1M|ND`I(emN^5x8q;4=|`N~2c^68))vOyQHNc6M3Ei;?qt{WWUv8kBn8c8prD2E}!E+M+$&QL-K*I_SQ0A7)rM{gK%{!xPw1yySfTs{S6nN0* zQdqCq|FA9|1gabbQ}Tw*l4y2kt*MBOW2gEdV5&`368Y^cU~=b@5I!%Clv{4NkOW?U zeab6{rR7XZ?s|K6D7I}BYPyN)5+dAKK~+>tU`AWVUTg?8Rl5d=R;~@$n#HrfWq_y2 zYkR4wnBdzenTGY07|JcqNLOptelitRzl$ze=#*Hg+M6FIfYYeqVX3rn$|C8v64sou z1JUnkUaGyCU$bU9d9B2ugMc6xQ}d^(c+D>--{e|K`%7EFIqV=O*~uWdGct7zGH7WZ z+6_hTRzS#i_W1F#|#p zY;b)qrt{WjhM?ytwY4`lERKSWosR1SPS><~Zr<*!=P-;}dt>CWGT_OtUL z_x?3WsmfUvtR^B_C!k-#h;3M{r?reqs-uAT&=nk#Fmu`gI@7xkj!aPaFw|rl zt5)1sC3+B#WzT-MGD?@ZC1BVR8 zV1e;G`vAP2W3XBq5dbPu*`{@N6E|j<`hanNpu8F9d-A^;!@CAk&Ja%QA%+Jmjjr&S zWKcW%U*pvzNU5_q9;bY2?Z8~INM#48l=Tdu0KXlU%Q=AgKWX{(ZIamBOMhXF9thz> zx25g(8xH(~gGXFMR@mxT5RI6EiX}>5TX&b>9o3X$)@|rZ&ETRWOYa3&Jll!~{oWG^ zb2f*p6f?C7aCwr>9XnV!Y3Zd}YbXo=LQ-#0j~3ee{vcw59a%|gwWElsZI>}U*Y)g8dNm3=IY8@=2wqH1Uz!Re%`H7Sj&Ji> z2hd3icruCkUqE#HOnmYSugq<)^0%eig%I!kpDGT2(2ybVKh^?XT~s27#%gbf^h#(+O8392RCUc@=0HY=r1OS!pc5ukgZeDZ}~Xg@lPT4 z`)q8Zg!FqN6In&X`Lz2<mW{V$!`W`i6zVhljG3bQvjiD#?a&gl6Oz=L!bd~1pFggJ8OzUi zW8R3bpGhJv=2bZFAuCoAkIi`bjUY; lnNAet*7M4ooqII3UXE#sR&rmGwL&cxJ& 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

    x# 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 { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Omit the body of server-sent event stream responses. + // Cloning such responses would prevent client-side stream cancelations + // from reaching the original stream (a teed stream only cancels its + // source once both of its branches cancel) and would buffer the + // entire stream into the unconsumed clone indefinitely. + const isEventStreamResponse = response.headers + .get('content-type') + ?.toLowerCase() + .startsWith('text/event-stream') + + // Clone the response so both the client and the library could consume it. + const responseClone = isEventStreamResponse ? null : response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: response.type, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: responseClone ? responseClone.body : null, + }, + }, + }, + responseClone && responseClone.body + ? [serializedRequest.body, responseClone.body] + : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/ui/public/sandbox_proxy.html b/ui/public/sandbox_proxy.html deleted file mode 100644 index d829b1bb6..000000000 --- a/ui/public/sandbox_proxy.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - MCP Apps Sandbox Proxy - - - - - - diff --git a/ui/scripts/init.sh b/ui/scripts/init.sh index f8d88469a..3d788a656 100644 --- a/ui/scripts/init.sh +++ b/ui/scripts/init.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -euo pipefail # Create nginx temp directories # These are required when running with readOnlyRootFilesystem: true @@ -9,7 +9,60 @@ mkdir -p /tmp/nginx/client_temp \ /tmp/nginx/proxy_temp \ /tmp/nginx/fastcgi_temp \ /tmp/nginx/uwsgi_temp \ - /tmp/nginx/scgi_temp + /tmp/nginx/scgi_temp \ + /tmp/kagent -# Start supervisord -exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +# Runtime browser configuration. +# +# Vite inlines import.meta.env at BUILD time, so anything the chart configures +# per-deployment cannot be baked into the bundle — it would freeze the Helm +# values as of image build and silently ignore whatever the operator set. These +# values are rendered into a small script instead, which the document loads +# before the app and nginx serves with no-store. +# +# A script rather than a JSON document the app fetches, because the API base URL +# is needed by a module-level constant: there is no point early enough for an +# awaited fetch to have landed. Keep the keys in sync with `ui/src/env.ts`. +CONFIG_PATH=/tmp/kagent/env-config.js + +API_BASE_URL="${KAGENT_API_BASE_URL:-/api}" +SSO_REDIRECT_PATH="${SSO_REDIRECT_PATH:-/oauth2/start}" +STREAM_TIMEOUT_MS="${KAGENT_STREAM_TIMEOUT_MS:-1800000}" +ENABLE_MOCK_UI="${ENABLE_MOCK_UI:-false}" + +# Anything an installed extension reads, passed through verbatim. Empty unless +# the chart sets them, and the app ignores keys it has no use for. +UI_BACKEND_HOST="${UI_BACKEND_HOST:-}" +LOCAL_CLUSTER_NAME="${LOCAL_CLUSTER_NAME:-}" + +# A non-numeric timeout would abort every chat stream immediately, which looks +# like the backend hanging up rather than like a bad value. Fall back instead. +if ! [[ "$STREAM_TIMEOUT_MS" =~ ^[0-9]+$ ]]; then + echo "init.sh: KAGENT_STREAM_TIMEOUT_MS='${STREAM_TIMEOUT_MS}' is not a number; using 1800000" >&2 + STREAM_TIMEOUT_MS=1800000 +fi + +# Escape backslashes first, then double quotes, so the value is safe inside a +# JSON string regardless of what the chart passed through. `<` is escaped as well +# because this lands inside a `, + ); + }, + + configureServer(server) { + // A changed `.env` has to reach the page the same way it would reach a + // restarted pod: the values are read once at load, so nothing short of a + // full reload picks them up. + server.watcher.add(path.resolve(import.meta.dirname, ".env")); + server.watcher.on("change", (file) => { + if (file.endsWith(".env")) { + server.ws.send({ type: "full-reload", path: "*" }); + } + }); + }, + }; +} + +export default defineConfig(({ mode }) => ({ + // JSX is transformed by oxc, which routes the factory at @emotion/react — + // that alone enables the `css` prop, no Babel step required. + plugins: [react({ jsxImportSource: "@emotion/react" }), devEnvConfig(mode)], + // The component library's bundle assigns to `global` at module top level, + // which exists in Node and not in a browser. Referencing an undeclared + // identifier throws before `global ?? window` can fall back, so importing + // anything from the library takes the whole page down. The library ships one + // barrel with no per-component entry points, so this cannot be avoided by + // importing more narrowly. + define: { global: "globalThis" }, + optimizeDeps: { + // Dev pre-bundles dependencies before serving them, and that pass does not see the + // top-level `define` above — so it needs its own copy. + // + // `rolldownOptions`, not `esbuildOptions`: Vite 8 pre-bundles with Rolldown and + // deprecated the esbuild form, which it warned about on every start. Worse than the + // noise, a deprecated option that stops being read would take the shim with it + // silently — and the failure that produces is the component library taking the whole + // page down, a long way from this line. + // Nested under `transform`, which is where rolldown takes a define map. + rolldownOptions: { transform: { define: { global: "globalThis" } } }, + }, + resolve: { + alias: { "@": path.resolve(import.meta.dirname, "./src") }, + }, + server: { + port: Number(process.env.UI_LOOP_PORT ?? 8001), + host: "0.0.0.0", + // Stands in for what nginx does in a deployed cluster, so a dev server + // talking to a real controller uses the same relative URLs as production. + // Requests are only proxied in live mode; the mock worker intercepts first + // otherwise, so these rules are inert by default. + proxy: { + "/api": { target: CONTROLLER_URL, changeOrigin: true }, + "/a2a": { target: CONTROLLER_URL, changeOrigin: true }, + }, + }, + preview: { + port: Number(process.env.UI_LOOP_PORT ?? 8001), + host: "0.0.0.0", + }, + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/testSetup.ts"], + css: true, + exclude: ["**/node_modules/**", "**/playwright/**"], + // Unit tests drive the client against this repo's own mock handlers, so the + // mode is stated rather than inherited. It used to come free from the dev + // default; that default is gone, because a page that quietly serves fixtures + // when the backend is down is worse than one that says so. + env: { VITE_API_MODE: "mock" }, + }, +})); diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts deleted file mode 100644 index d4d96e791..000000000 --- a/ui/vitest.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { defineConfig } from 'vitest/config'; - -import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; - -import { playwright } from '@vitest/browser-playwright'; - -const dirname = - typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); - -// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon -export default defineConfig({ - test: { - projects: [ - { - extends: true, - plugins: [ - // The plugin will run tests for the stories defined in your Storybook config - // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest - storybookTest({ configDir: path.join(dirname, '.storybook') }), - ], - test: { - name: 'storybook', - browser: { - enabled: true, - headless: true, - provider: playwright({}), - instances: [{ browser: 'chromium' }], - }, - setupFiles: ['.storybook/vitest.setup.ts'], - }, - }, - ], - }, -}); diff --git a/ui/vitest.shims.d.ts b/ui/vitest.shims.d.ts deleted file mode 100644 index 7782f28d3..000000000 --- a/ui/vitest.shims.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/ui/yarn.lock b/ui/yarn.lock new file mode 100644 index 000000000..0b324e71a --- /dev/null +++ b/ui/yarn.lock @@ -0,0 +1,5699 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@adobe/css-tools@npm:^4.4.0": + version: 4.5.0 + resolution: "@adobe/css-tools@npm:4.5.0" + checksum: 10c0/fc969e1117098eb4cccdb73beb2508daa0e52760af1183d6288bafea59204943490ab3ede28593032ffb8929c0cee270b2a53254fe61139ab00604ea8fc33cea + languageName: node + linkType: hard + +"@ant-design/colors@npm:^8.0.1": + version: 8.0.1 + resolution: "@ant-design/colors@npm:8.0.1" + dependencies: + "@ant-design/fast-color": "npm:^3.0.0" + checksum: 10c0/6108c2204ce98dbf68682fcd04c3cba087f5316f01c6a28928eb8d7b291c23b6a5fcc9cd74af6211f7984d3ccc7e26be16ec664a9380737b46ee24fc78222e30 + languageName: node + linkType: hard + +"@ant-design/cssinjs-utils@npm:^2.1.2": + version: 2.1.2 + resolution: "@ant-design/cssinjs-utils@npm:2.1.2" + dependencies: + "@ant-design/cssinjs": "npm:^2.1.2" + "@babel/runtime": "npm:^7.23.2" + "@rc-component/util": "npm:^1.4.0" + peerDependencies: + react: ">=18" + react-dom: ">=18" + checksum: 10c0/46663c8c92e23f2fbc61682099b5ec03aa26e9bffab96be6889b310d2a58adf46758c5f9f977aa13d46ba3606cf4c64ea0bb76985bc1f4b5619b81be5849b172 + languageName: node + linkType: hard + +"@ant-design/cssinjs@npm:^2.1.2": + version: 2.1.2 + resolution: "@ant-design/cssinjs@npm:2.1.2" + dependencies: + "@babel/runtime": "npm:^7.11.1" + "@emotion/hash": "npm:^0.8.0" + "@emotion/unitless": "npm:^0.7.5" + "@rc-component/util": "npm:^1.4.0" + clsx: "npm:^2.1.1" + csstype: "npm:^3.1.3" + stylis: "npm:^4.3.4" + peerDependencies: + react: ">=16.0.0" + react-dom: ">=16.0.0" + checksum: 10c0/70d8169f1e2044bb44d6a51265a3123e8ac2018b1725ccda018b083a610afdf8e9a27e16df27ad0eaa67e85c827d5abcc8f47dd5ad448c0c8560dfcd0353aa68 + languageName: node + linkType: hard + +"@ant-design/fast-color@npm:^3.0.0, @ant-design/fast-color@npm:^3.0.1": + version: 3.0.1 + resolution: "@ant-design/fast-color@npm:3.0.1" + checksum: 10c0/bc6535351f855b1af777b9d18c5b5ca7eeb8769c1dc9cd8579408ed5284e4d6dd2d9cd192ff8110dc7a82e9dc9bc2e5c51ceeff3237c88c5464aca0e4d70eb9a + languageName: node + linkType: hard + +"@ant-design/icons-svg@npm:^4.5.0": + version: 4.5.0 + resolution: "@ant-design/icons-svg@npm:4.5.0" + checksum: 10c0/6d8c15ffc43c7c39560e075682da5b177462d42ca92ac227702c9ce267fea688025449bc6528d7b64f1bc88408384af5d6c52464afa47a01b5394894816d7f9e + languageName: node + linkType: hard + +"@ant-design/icons@npm:^6.3.2": + version: 6.3.2 + resolution: "@ant-design/icons@npm:6.3.2" + dependencies: + "@ant-design/colors": "npm:^8.0.1" + "@ant-design/icons-svg": "npm:^4.5.0" + "@rc-component/util": "npm:^1.11.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.0.0" + react-dom: ">=16.0.0" + checksum: 10c0/3f7dcfda65d22f43f78173de60ac47fd5ffcb54a9db4dcaa2fd8380aa424facb8c51edc9c7a9afeaab8ca012c07827edfe73bb51e7a33891fed90ce1da692620 + languageName: node + linkType: hard + +"@ant-design/react-slick@npm:~2.0.0": + version: 2.0.0 + resolution: "@ant-design/react-slick@npm:2.0.0" + dependencies: + "@babel/runtime": "npm:^7.28.4" + clsx: "npm:^2.1.1" + json2mq: "npm:^0.2.0" + throttle-debounce: "npm:^5.0.0" + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/1aaa3aae2a9e7d06840bf620b0ac1d0de4088f969fec45ae1b1c7a16ab1e0d6d89809ec9c915fad07c8a719b447ebf5031bb53024d1a92f1ab75e3c6788ed42e + languageName: node + linkType: hard + +"@asamuzakjp/css-color@npm:^6.0.5": + version: 6.0.5 + resolution: "@asamuzakjp/css-color@npm:6.0.5" + dependencies: + "@csstools/css-calc": "npm:^3.2.1" + "@csstools/css-color-parser": "npm:^4.1.9" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + lru-cache: "npm:^11.5.2" + checksum: 10c0/c08cc6bcea92c5e04fac208fbdbd125ed89e0eeca258df48819878347c75f90c3eb3a52940049fcb580ac9afe83c1d6bbf4e4caee0f8c2655cf47286f6ce3f09 + languageName: node + linkType: hard + +"@asamuzakjp/dom-selector@npm:^8.3.0": + version: 8.3.0 + resolution: "@asamuzakjp/dom-selector@npm:8.3.0" + dependencies: + bidi-js: "npm:^1.0.3" + css-tree: "npm:^3.2.1" + is-potential-custom-element-name: "npm:^1.0.1" + lru-cache: "npm:^11.5.2" + checksum: 10c0/3f541cb7dd4ab1c6762802c0c908487ddbf5a2434b3ddf91b145c84272f0110fa6b02fc9baf879aa5bc3c2968dc702fdbaadfa3b58ee5f42645cb0e2949cbbf6 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/code-frame@npm:7.29.7" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.29.7" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10c0/169fc2080169a40c1760155eaaaf739bcb882df0bec76a83adbda5493645bc17270a3434b8848c494b1933e96fe1d147370001e3cda09a39f43ae30f08ef2069 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/compat-data@npm:7.29.7" + checksum: 10c0/47913f05e08a45a1c9df38c02b4b49e391005085b489432647a1abe112e5d9c75e3be8ea5972b7f6da4ec5d1339922ceb9ea02b8a25d4ed1cb8636e5261f344e + languageName: node + linkType: hard + +"@babel/core@npm:^7.24.4": + version: 7.29.7 + resolution: "@babel/core@npm:7.29.7" + dependencies: + "@babel/code-frame": "npm:^7.29.7" + "@babel/generator": "npm:^7.29.7" + "@babel/helper-compilation-targets": "npm:^7.29.7" + "@babel/helper-module-transforms": "npm:^7.29.7" + "@babel/helpers": "npm:^7.29.7" + "@babel/parser": "npm:^7.29.7" + "@babel/template": "npm:^7.29.7" + "@babel/traverse": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + "@jridgewell/remapping": "npm:^2.3.5" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10c0/112fb09c24de7a1de64d1de2c31fe65c4e6af4cb2fb6e6d99ea5373e6fc51e75b88581c0efae4c4c68f119a02a988c7106e95011a41530a2fb8ed793c7eaa07b + languageName: node + linkType: hard + +"@babel/generator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/generator@npm:7.29.7" + dependencies: + "@babel/parser": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/9bf72b01b5bd0ea5b1288a0e37dbd360bff2f2b1ce73342c0d40fb3db2ec3dc004ada5ffa925c5e12939a416eed59e600d562b8ecd938ce0d27dfd0eb6c6c2b7 + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-compilation-targets@npm:7.29.7" + dependencies: + "@babel/compat-data": "npm:^7.29.7" + "@babel/helper-validator-option": "npm:^7.29.7" + browserslist: "npm:^4.24.0" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10c0/4c15fd4c69a0a7047799a28a88460c19cede0a0ee8af994ea169114986f4af48b92c7393a4a3fee0456c11a656eece3448a6ed06354453d6c27cccf17195453b + languageName: node + linkType: hard + +"@babel/helper-globals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-globals@npm:7.29.7" + checksum: 10c0/f38417c40b1129a1b2b519ca961b9040c8827d1444fd74068702286b91b77089431dc76b6b9d5c1496e5da2a4f3ad329c6946e688ba3fa0d1d0b3d2b4f34f36a + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-imports@npm:7.29.7" + dependencies: + "@babel/traverse": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + checksum: 10c0/6adf60d97356027413342a092f818d9678c4f5caff716a33e3284b5ae14e47a9e88059d421dde4ee4894691260039a12602c0e7becadc175602194b40dfa345d + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-transforms@npm:7.29.7" + dependencies: + "@babel/helper-module-imports": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + "@babel/traverse": "npm:^7.29.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10c0/ee5a2172c24a42be696836f4b0d947489c9729d8adf5821885cf77d1ad5333e3c447368e9a71f67df1099570490553dccf9f888ef0a92a48aa63cb086bd8c7e1 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-string-parser@npm:7.29.7" + checksum: 10c0/194bc0f1716e396d5ffde56ad6119745fb9557662c98611590e5e454906783a4ccb21ce93056b8eb69a4909044834e45d96e50ac695bbe9e3221648fe033c06c + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7" + checksum: 10c0/4795354e7ae0dcafa72de1cd04ec51252dc1498517170beaf019e03effc5b7bf13c6b21a3949a77e07b8125be7f106ed1131350d8ebd4566ae874094a726d62b + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-option@npm:7.29.7" + checksum: 10c0/d2a06c6d0ac40ba4a2f219fc2cab249c7a94bacdb2686273b7f9598571c908809b48468ff588915a346e6cc7296f60b581023d1d498b747fed06f779d335c2cc + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helpers@npm:7.29.7" + dependencies: + "@babel/template": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + checksum: 10c0/218e8d10953647c9f44775f5a022b227a182674853b5ea8631889deb7e1a3e4bc870388aaecf59bb8bd92a87f9a96220ed3f70a35bffec6bcf9169ecb67891ac + languageName: node + linkType: hard + +"@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/parser@npm:7.29.7" + dependencies: + "@babel/types": "npm:^7.29.7" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/65133038f80b54a714d6027cb77cee3f9a6b5c4c6842ce674301e13947cbcbfa8055e63acaf1b84c085d34226a14425b2c2b97b829e0e226d2e8f1299942a51d + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.29.2": + version: 7.29.7 + resolution: "@babel/runtime@npm:7.29.7" + checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e + languageName: node + linkType: hard + +"@babel/runtime@npm:^8.0.0": + version: 8.0.0 + resolution: "@babel/runtime@npm:8.0.0" + checksum: 10c0/450857cf52b3a3935d4aad505277201361c86185dad3bdd2ae083d78b76c6265c7fc7610e170c80de1b825d5bfdcfb50697f0853fff922b2661c91cff1fbbd81 + languageName: node + linkType: hard + +"@babel/template@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/template@npm:7.29.7" + dependencies: + "@babel/code-frame": "npm:^7.29.7" + "@babel/parser": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + checksum: 10c0/8bb7f900dcab0e9e1c5ffbc33ca10e0d26b7b2e2ca804becb73ee771b9c4ed6e2908a4ae4a14c08560febb45d2b6b9a173955e42ad404d05f8b04840a14d9c58 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/traverse@npm:7.29.7" + dependencies: + "@babel/code-frame": "npm:^7.29.7" + "@babel/generator": "npm:^7.29.7" + "@babel/helper-globals": "npm:^7.29.7" + "@babel/parser": "npm:^7.29.7" + "@babel/template": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + debug: "npm:^4.3.1" + checksum: 10c0/e256a1fbdb956555b76f3c285b1e453f6bedec8b3afb61751d99d933efd11c7d79caf5ddf2493570058a9f7deaa1b48324380d7c1aa1443fd9508becbf56331a + languageName: node + linkType: hard + +"@babel/types@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/types@npm:7.29.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + checksum: 10c0/b6623994c69717fa27294f5fa46d59140338e2d86c6c1c13085c84ef7d53086ee357fbf4fe9abe3dd3da75734dc77c4c0df2f90fb29e667558bb3b3fb705e88f + languageName: node + linkType: hard + +"@bramus/specificity@npm:^2.4.2": + version: 2.4.2 + resolution: "@bramus/specificity@npm:2.4.2" + dependencies: + css-tree: "npm:^3.0.0" + bin: + specificity: bin/cli.js + checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72 + languageName: node + linkType: hard + +"@bufbuild/protobuf@npm:2.13.0": + version: 2.13.0 + resolution: "@bufbuild/protobuf@npm:2.13.0" + checksum: 10c0/bb4d39512302887399355ac2ba1228e562c1cae818ff88b4b85b3060758aba5fec94786fe80f69de8878f98e730911c0bd145741adb582d0abf6b42de921e50c + languageName: node + linkType: hard + +"@connectrpc/connect-web@npm:2.1.2": + version: 2.1.2 + resolution: "@connectrpc/connect-web@npm:2.1.2" + peerDependencies: + "@bufbuild/protobuf": ^2.7.0 + "@connectrpc/connect": 2.1.2 + checksum: 10c0/304667186990d79e61ace8af03a39c5474e6ecb21282eeaa9e216a0e214524003891005ff3cfbda33230c21b779cfece530bf8cfdd48550b7861621513d18e0e + languageName: node + linkType: hard + +"@connectrpc/connect@npm:2.1.2": + version: 2.1.2 + resolution: "@connectrpc/connect@npm:2.1.2" + peerDependencies: + "@bufbuild/protobuf": ^2.7.0 + checksum: 10c0/4e1500089023b752efd44fa940a5ae1e1fa00e1bebc4ba0ced272bb935920d8c3b85b77450e644ad442bb8ca9a275b0c17136f896ba8eff036178131db01d615 + languageName: node + linkType: hard + +"@csstools/color-helpers@npm:^6.1.0": + version: 6.1.0 + resolution: "@csstools/color-helpers@npm:6.1.0" + checksum: 10c0/ebb71eebcd6dde16a89d687c30d4c7f315f9079ec803497ebac0528d0a9ef09847eaf167b4565c4919f156e32e7ca2167199ac2d2020f184f7888551a3b4712b + languageName: node + linkType: hard + +"@csstools/css-calc@npm:^3.2.1, @csstools/css-calc@npm:^3.3.0": + version: 3.3.0 + resolution: "@csstools/css-calc@npm:3.3.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/83157b9fbf3599dfff2338ac40e7eec0884d1d729b9ca4a949af3c2da55d14368b2ac70ff4f659fc1ec6801a48c9225f2211a8be987678aadf6795dbebe5a2da + languageName: node + linkType: hard + +"@csstools/css-color-parser@npm:^4.1.9": + version: 4.1.10 + resolution: "@csstools/css-color-parser@npm:4.1.10" + dependencies: + "@csstools/color-helpers": "npm:^6.1.0" + "@csstools/css-calc": "npm:^3.3.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/d8fe23036f8d9cdbb9fc99f09e2803a74c280c511ccf25c4990f4913c0e23c7687138a85c8c27eeec509c2e3aa72957b8c328ce295d3faaa16cb8fd4b4ede122 + languageName: node + linkType: hard + +"@csstools/css-parser-algorithms@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-parser-algorithms@npm:4.0.0" + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d + languageName: node + linkType: hard + +"@csstools/css-syntax-patches-for-csstree@npm:^1.1.7": + version: 1.1.7 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.7" + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + checksum: 10c0/cdebc36196f951f4436132f5346927c8aeff2917a1c2af42ad36eb87a2b1883c8cd21cb6b3105e951ae0fa964d2ebda6309bae476d232d27f0ec657199df6bb6 + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-tokenizer@npm:4.0.0" + checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f + languageName: node + linkType: hard + +"@emnapi/core@npm:2.0.0-alpha.3": + version: 2.0.0-alpha.3 + resolution: "@emnapi/core@npm:2.0.0-alpha.3" + dependencies: + "@emnapi/wasi-threads": "npm:2.0.1" + tslib: "npm:^2.4.0" + checksum: 10c0/dfc26f53eb40dab0e316206cabe01e6e98d21ef8a0b820f81ce7e0fa7b4307c94f2a9599c11e6c601ad7bd77e8efb9b7cb9f86dbc3b6237f799405e0cb60c587 + languageName: node + linkType: hard + +"@emnapi/runtime@npm:2.0.0-alpha.3": + version: 2.0.0-alpha.3 + resolution: "@emnapi/runtime@npm:2.0.0-alpha.3" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/c8f6e0ad2f9c0ced8bc156a7ec3ab5fbbd2d3b7147b6a37537f6892131844b969d3aa450749807fc238160b5db61d6160b8066abfce4b2c40acfb1941bb31dd1 + languageName: node + linkType: hard + +"@emnapi/wasi-threads@npm:2.0.1": + version: 2.0.1 + resolution: "@emnapi/wasi-threads@npm:2.0.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/5f7bf4cc4f6a1c31ec2084c9321d054f3b3b6c7e89430bb23fc3487d55e7be40f1ff31b2cb9a51721b444d4e8ac1f065b8749606771f6e4e7ccb32bbb709a303 + languageName: node + linkType: hard + +"@emotion/babel-plugin@npm:^11.13.5": + version: 11.13.5 + resolution: "@emotion/babel-plugin@npm:11.13.5" + dependencies: + "@babel/helper-module-imports": "npm:^7.16.7" + "@babel/runtime": "npm:^7.18.3" + "@emotion/hash": "npm:^0.9.2" + "@emotion/memoize": "npm:^0.9.0" + "@emotion/serialize": "npm:^1.3.3" + babel-plugin-macros: "npm:^3.1.0" + convert-source-map: "npm:^1.5.0" + escape-string-regexp: "npm:^4.0.0" + find-root: "npm:^1.1.0" + source-map: "npm:^0.5.7" + stylis: "npm:4.2.0" + checksum: 10c0/8ccbfec7defd0e513cb8a1568fa179eac1e20c35fda18aed767f6c59ea7314363ebf2de3e9d2df66c8ad78928dc3dceeded84e6fa8059087cae5c280090aeeeb + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/cache@npm:11.14.0" + dependencies: + "@emotion/memoize": "npm:^0.9.0" + "@emotion/sheet": "npm:^1.4.0" + "@emotion/utils": "npm:^1.4.2" + "@emotion/weak-memoize": "npm:^0.4.0" + stylis: "npm:4.2.0" + checksum: 10c0/3fa3e7a431ab6f8a47c67132a00ac8358f428c1b6c8421d4b20de9df7c18e95eec04a5a6ff5a68908f98d3280044f247b4965ac63df8302d2c94dba718769724 + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.8.0": + version: 0.8.0 + resolution: "@emotion/hash@npm:0.8.0" + checksum: 10c0/706303d35d416217cd7eb0d36dbda4627bb8bdf4a32ea387e8dd99be11b8e0a998e10af21216e8a5fade518ad955ff06aa8890f20e694ce3a038ae7fc1000556 + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.9.2": + version: 0.9.2 + resolution: "@emotion/hash@npm:0.9.2" + checksum: 10c0/0dc254561a3cc0a06a10bbce7f6a997883fd240c8c1928b93713f803a2e9153a257a488537012efe89dbe1246f2abfe2add62cdb3471a13d67137fcb808e81c2 + languageName: node + linkType: hard + +"@emotion/memoize@npm:^0.9.0": + version: 0.9.0 + resolution: "@emotion/memoize@npm:0.9.0" + checksum: 10c0/13f474a9201c7f88b543e6ea42f55c04fb2fdc05e6c5a3108aced2f7e7aa7eda7794c56bba02985a46d8aaa914fcdde238727a98341a96e2aec750d372dadd15 + languageName: node + linkType: hard + +"@emotion/react@npm:^11.14.0": + version: 11.14.0 + resolution: "@emotion/react@npm:11.14.0" + dependencies: + "@babel/runtime": "npm:^7.18.3" + "@emotion/babel-plugin": "npm:^11.13.5" + "@emotion/cache": "npm:^11.14.0" + "@emotion/serialize": "npm:^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.2.0" + "@emotion/utils": "npm:^1.4.2" + "@emotion/weak-memoize": "npm:^0.4.0" + hoist-non-react-statics: "npm:^3.3.1" + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d0864f571a9f99ec643420ef31fde09e2006d3943a6aba079980e4d5f6e9f9fecbcc54b8f617fe003c00092ff9d5241179149ffff2810cb05cf72b4620cfc031 + languageName: node + linkType: hard + +"@emotion/serialize@npm:^1.3.3": + version: 1.3.3 + resolution: "@emotion/serialize@npm:1.3.3" + dependencies: + "@emotion/hash": "npm:^0.9.2" + "@emotion/memoize": "npm:^0.9.0" + "@emotion/unitless": "npm:^0.10.0" + "@emotion/utils": "npm:^1.4.2" + csstype: "npm:^3.0.2" + checksum: 10c0/b28cb7de59de382021de2b26c0c94ebbfb16967a1b969a56fdb6408465a8993df243bfbd66430badaa6800e1834724e84895f5a6a9d97d0d224de3d77852acb4 + languageName: node + linkType: hard + +"@emotion/sheet@npm:^1.4.0": + version: 1.4.0 + resolution: "@emotion/sheet@npm:1.4.0" + checksum: 10c0/3ca72d1650a07d2fbb7e382761b130b4a887dcd04e6574b2d51ce578791240150d7072a9bcb4161933abbcd1e38b243a6fb4464a7fe991d700c17aa66bb5acc7 + languageName: node + linkType: hard + +"@emotion/unitless@npm:^0.10.0": + version: 0.10.0 + resolution: "@emotion/unitless@npm:0.10.0" + checksum: 10c0/150943192727b7650eb9a6851a98034ddb58a8b6958b37546080f794696141c3760966ac695ab9af97efe10178690987aee4791f9f0ad1ff76783cdca83c1d49 + languageName: node + linkType: hard + +"@emotion/unitless@npm:^0.7.5": + version: 0.7.5 + resolution: "@emotion/unitless@npm:0.7.5" + checksum: 10c0/4d0d94f53cb97b4481bbfa394953e1899a0b877644642ba9dd7247c27eb8c48e14e22aeb11411d7d9874685ad85dd5fb5b50eb78c6d8840eb56a84b92dcef2f4 + languageName: node + linkType: hard + +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": + version: 1.2.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/074dbc92b96bdc09209871070076e3b0351b6b47efefa849a7d9c37ab142130767609ca1831da0055988974e3b895c1de7606e4c421fecaa27c3e56a2afd3b08 + languageName: node + linkType: hard + +"@emotion/utils@npm:^1.4.2": + version: 1.4.2 + resolution: "@emotion/utils@npm:1.4.2" + checksum: 10c0/7d0010bf60a2a8c1a033b6431469de4c80e47aeb8fd856a17c1d1f76bbc3a03161a34aeaa78803566e29681ca551e7bf9994b68e9c5f5c796159923e44f78d9a + languageName: node + linkType: hard + +"@emotion/weak-memoize@npm:^0.4.0": + version: 0.4.0 + resolution: "@emotion/weak-memoize@npm:0.4.0" + checksum: 10c0/64376af11f1266042d03b3305c30b7502e6084868e33327e944b539091a472f089db307af69240f7188f8bc6b319276fd7b141a36613f1160d73d12a60f6ca1a + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.10.1 + resolution: "@eslint-community/eslint-utils@npm:4.10.1" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/b514586655698bc6b74db72a496c77e813c78b63e36a83429845ac7057dd77113b0b6c31e6e590d5baaeee2abae6ea22363a41209bcafa078d895ab83ce83011 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.23.5": + version: 0.23.5 + resolution: "@eslint/config-array@npm:0.23.5" + dependencies: + "@eslint/object-schema": "npm:^3.0.5" + debug: "npm:^4.3.1" + minimatch: "npm:^10.2.4" + checksum: 10c0/b24833c4c76e78ee075d306cd3f095db46b2db0f90cc13a6ee6e4275f9889731c05bf5403ab5fefb79c756e07ac9184ed0e04570341382f9eccbccc80e6d1a0c + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.7.0": + version: 0.7.0 + resolution: "@eslint/config-helpers@npm:0.7.0" + dependencies: + "@eslint/core": "npm:^1.2.1" + checksum: 10c0/fd40d57d6f1db49f7b647048b88a433dc7f6522ef3edf855a43cb526ef4fc40622ceed0dc8de2e03d254f30f8e035370570de1d4bd8e7c2b1200131451e0d331 + languageName: node + linkType: hard + +"@eslint/core@npm:^1.2.1": + version: 1.2.1 + resolution: "@eslint/core@npm:1.2.1" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10c0/10979b40588ecfef771fcb5013a542a35fb30692cc95a65f3481b0b36fbd89f5679efeb30d57f4eed35203d859aabace2a620177d6c536f71b299a1af2f3398f + languageName: node + linkType: hard + +"@eslint/js@npm:^10.0.1": + version: 10.0.1 + resolution: "@eslint/js@npm:10.0.1" + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + checksum: 10c0/9f3fcaf71ba7fdf65d82e8faad6ecfe97e11801cc3c362b306a88ea1ed1344ae0d35330dddb0e8ad18f010f6687a70b75491b9e01c8af57acd7987cee6b3ec6c + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^3.0.5": + version: 3.0.5 + resolution: "@eslint/object-schema@npm:3.0.5" + checksum: 10c0/1db337431f520b99e9edda64ef5fafd7ec6a029843eeb608753025125b6649d861d843cffafafd3c4e37926d7d5f9ec0c6a8e3665c13c3da2144e8132892e92e + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.7.2": + version: 0.7.2 + resolution: "@eslint/plugin-kit@npm:0.7.2" + dependencies: + "@eslint/core": "npm:^1.2.1" + levn: "npm:^0.4.1" + checksum: 10c0/aafba08077bcd6d7dde6c2e21db18086046a88f914f29971a84cac9ad2d48952ded1b293e665e523805297eff756522dafa16f0062195e2c7143dcd1d47d11ed + languageName: node + linkType: hard + +"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.1, @exodus/bytes@npm:^1.6.0": + version: 1.15.1 + resolution: "@exodus/bytes@npm:1.15.1" + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + checksum: 10c0/333056a6953bbf875d9f3b86c32314de29458d842e5f56f6ef8034b18c2d9660184550093d1bae5de0064043d5e23f54cc03148798d9d29cf5167ac03f2e9f8c + languageName: node + linkType: hard + +"@humanfs/core@npm:^0.19.2": + version: 0.19.2 + resolution: "@humanfs/core@npm:0.19.2" + dependencies: + "@humanfs/types": "npm:^0.15.0" + checksum: 10c0/d0a1d52d7b30c27d49475a53072d1510b81c5803e44b342fb8faf3887f1aa27593a1e6dc76a45268e7892d3f4e198146659281f6b6d55eacf3fd5a38bac30c5c + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.8 + resolution: "@humanfs/node@npm:0.16.8" + dependencies: + "@humanfs/core": "npm:^0.19.2" + "@humanfs/types": "npm:^0.15.0" + "@humanwhocodes/retry": "npm:^0.4.0" + checksum: 10c0/56140579db811af4e160b195d45d0f29acf644d192c93fe24c9e594ebf06f19dfc157494a07c84540b8a071c0e4b37209c2362765d31734f4d0be869c2422e25 + languageName: node + linkType: hard + +"@humanfs/types@npm:^0.15.0": + version: 0.15.0 + resolution: "@humanfs/types@npm:0.15.0" + checksum: 10c0/fc26b9a024b0e55f7eaf64036df94345bf5d36d6a41ef80ef38e78f1f7430ce26cf435af736adae58913baae18eac3f38c18739054a3d379102015978eae862e + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 10c0/909b69c3b86d482c26b3359db16e46a32e0fb30bd306a3c176b8313b9e7313dba0f37f519de6aa8b0a1921349e505f259d19475e123182416a506d7f87e7f529 + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: 10c0/3775bb30087d4440b3f7406d5a057777d90e4b9f435af488a4923ef249e93615fb78565a85f173a186a076c7706a81d0d57d563a2624e4de2c5c9c66c486ce42 + languageName: node + linkType: hard + +"@inquirer/ansi@npm:^2.0.7": + version: 2.0.7 + resolution: "@inquirer/ansi@npm:2.0.7" + checksum: 10c0/a574f97a899f0d9346fa26b528b3f4a9ba6dcb9172288efb6b4314d8486470ed53d2f538200f66a25b843c6e0cbf83688c6d5174a8dc6eca853b291b09609c5a + languageName: node + linkType: hard + +"@inquirer/confirm@npm:^6.0.11": + version: 6.1.1 + resolution: "@inquirer/confirm@npm:6.1.1" + dependencies: + "@inquirer/core": "npm:^11.2.1" + "@inquirer/type": "npm:^4.0.7" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/4684406161c09327df830b4026f3165b31e13831276d215051586408ed434423263b15686393ce95a4b55058c1b7f9b08aa4b66f5ac930b47523fff75051d36f + languageName: node + linkType: hard + +"@inquirer/core@npm:^11.2.1": + version: 11.2.1 + resolution: "@inquirer/core@npm:11.2.1" + dependencies: + "@inquirer/ansi": "npm:^2.0.7" + "@inquirer/figures": "npm:^2.0.7" + "@inquirer/type": "npm:^4.0.7" + cli-width: "npm:^4.1.0" + fast-wrap-ansi: "npm:^0.2.0" + mute-stream: "npm:^3.0.0" + signal-exit: "npm:^4.1.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/b5be386cecd9e441ac2f9d3417a6ae1c4658b3ee6cdf5dae791211400f4de158851f81fca2245e2062833716f95366b9e1717770828cb7365e756c16e822f0d2 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^2.0.7": + version: 2.0.7 + resolution: "@inquirer/figures@npm:2.0.7" + checksum: 10c0/e0573dc9ad25fa3628d5164745e52852d8cd832a9918605b7716df2e37a0005a0aaf40b6d81cef2ca09cb708b200e61b82d1dcd17003f572577e233c19a9ec7b + languageName: node + linkType: hard + +"@inquirer/type@npm:^4.0.7": + version: 4.0.7 + resolution: "@inquirer/type@npm:4.0.7" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/80678ac1c6e19ce309909e4a54a69adc95697ea3abc2cb92f17b1bc52f4caadbcb4003ae7339fb5a70c0d36d3bde975e1bb4450069662f41c953a0d28695bb70 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/9a7d65fb13bd9aec1fbab74cda08496839b7e2ceb31f5ab922b323e94d7c481ce0fc4fd7e12e2610915ed8af51178bdc61e168e92a8c8b8303b030b03489b13b + languageName: node + linkType: hard + +"@jridgewell/remapping@npm:^2.3.5": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/3de494219ffeb2c5c38711d0d7bb128097edf91893090a2dbc8ee0b55d092bb7347b1fd0f478486c5eab010e855c73927b1666f2107516d472d24a73017d1194 + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 + languageName: node + linkType: hard + +"@kurkle/color@npm:^0.3.0": + version: 0.3.4 + resolution: "@kurkle/color@npm:0.3.4" + checksum: 10c0/0e9fd55c614b005c5f0c4c755bca19ec0293bc7513b4ea3ec1725234f9c2fa81afbc78156baf555c8b9cb0d305619253c3f5bca016067daeebb3d00ebb4ea683 + languageName: node + linkType: hard + +"@mswjs/interceptors@npm:^0.41.3": + version: 0.41.9 + resolution: "@mswjs/interceptors@npm:0.41.9" + dependencies: + "@open-draft/deferred-promise": "npm:^2.2.0" + "@open-draft/logger": "npm:^0.3.0" + "@open-draft/until": "npm:^2.0.0" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.3" + strict-event-emitter: "npm:^0.5.1" + checksum: 10c0/2efff40877e07ce29846be76c2683177308a72a3ccfe4c096b496412a279acd92b5b9cdad40ad71c36b149a4a7d9e9b4e7d295893a8ba9661b43334f5784ecd8 + languageName: node + linkType: hard + +"@napi-rs/wasm-runtime@npm:^1.2.0": + version: 1.2.1 + resolution: "@napi-rs/wasm-runtime@npm:1.2.1" + dependencies: + "@tybys/wasm-util": "npm:^0.10.3" + peerDependencies: + "@emnapi/core": ^1.7.1 || ^2.0.0-alpha.3 + "@emnapi/runtime": ^1.7.1 || ^2.0.0-alpha.3 + checksum: 10c0/33532973e573e566536b9c342caea85a504e1e6332d1dc6aeb55de4b476dea215db271ce0c1e044d0ddc121ef9ef9caec8654c8937c9f5330db4f0051e9ff8d1 + languageName: node + linkType: hard + +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: 10c0/eafc1b1d0fc8edb5e1c753c5e0f3293410b40dde2f92688211a54806d4136887051f39b98c1950370be258483deac9dfd17cf8b96557553765198ef2547e4549 + languageName: node + linkType: hard + +"@open-draft/deferred-promise@npm:^3.0.0": + version: 3.0.0 + resolution: "@open-draft/deferred-promise@npm:3.0.0" + checksum: 10c0/4dd697e55495e436be9536413cc9975e792e9ca7472e81e3d3d69e9b65cb678465aac90b463ac02f2b490c0581c4e9aa8a33d2a5857decbe2c6d9ffb310f8e1f + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.0" + checksum: 10c0/90010647b22e9693c16258f4f9adb034824d1771d3baa313057b9a37797f571181005bc50415a934eaf7c891d90ff71dcd7a9d5048b0b6bb438f31bef2c7c5c1 + languageName: node + linkType: hard + +"@open-draft/until@npm:^2.0.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 10c0/61d3f99718dd86bb393fee2d7a785f961dcaf12f2055f0c693b27f4d0cd5f7a03d498a6d9289773b117590d794a43cd129366fd8e99222e4832f67b1653d54cf + languageName: node + linkType: hard + +"@oxc-project/types@npm:=0.142.0": + version: 0.142.0 + resolution: "@oxc-project/types@npm:0.142.0" + checksum: 10c0/e4fa60b8fe1a77b0db6b9a2dcbc78d9a5a18ef64dffde01d909108f3ddbc562b876c568cb01e297ba71a256fc8aaafc928d4562475a9b2a25b276fee5bf635c3 + languageName: node + linkType: hard + +"@playwright/test@npm:^1.62.1": + version: 1.62.1 + resolution: "@playwright/test@npm:1.62.1" + dependencies: + playwright: "npm:1.62.1" + bin: + playwright: cli.js + checksum: 10c0/4b76f2f717723f84a73601f74fde97f5d92e4b5c869cf87c0f5628bf247fa1d18c9dc6c8e136463303233239e8ce58f902513d4281a9fabc3c426e911ded2c83 + languageName: node + linkType: hard + +"@rc-component/async-validator@npm:^6.0.0": + version: 6.0.0 + resolution: "@rc-component/async-validator@npm:6.0.0" + dependencies: + "@babel/runtime": "npm:^7.24.4" + checksum: 10c0/9918b080cfd2c59b48da548f58050514627b71f83823bfa0fcfdd6d7bb43f63c8c99abe0200791880139c9877bd335d5d26d91f74b21c9fadfa653673687752f + languageName: node + linkType: hard + +"@rc-component/cascader@npm:~1.17.0": + version: 1.17.0 + resolution: "@rc-component/cascader@npm:1.17.0" + dependencies: + "@rc-component/select": "npm:~1.8.0" + "@rc-component/tree": "npm:~1.3.2" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/566822f688d9c38571bf716c20f9ead268ea9598af559c49e24a2b374319a9b20ef2b3384478aae087f814c0591efdc218684b0b98c6fd1ef5c13e5ccea8a324 + languageName: node + linkType: hard + +"@rc-component/checkbox@npm:~2.0.0": + version: 2.0.0 + resolution: "@rc-component/checkbox@npm:2.0.0" + dependencies: + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/b31d072843d1961401fd4652108cca7d97597d26c521038c5cc1a9aed311b5a1e6c723cbfb7084cd0f1fb62f7fc2c6006d6638feb80f4291c9fc2e26ba1a26f9 + languageName: node + linkType: hard + +"@rc-component/collapse@npm:~1.2.0": + version: 1.2.0 + resolution: "@rc-component/collapse@npm:1.2.0" + dependencies: + "@babel/runtime": "npm:^7.10.1" + "@rc-component/motion": "npm:^1.1.4" + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/b8f954b21d0dada200a3dcec3ea7455b6c827ce358827dd50aa721953f4bac6224fcbb0f07546e6b9a5286c781819ebbe57ecaeb8fd898054ac276b8beb1945c + languageName: node + linkType: hard + +"@rc-component/color-picker@npm:~3.1.1": + version: 3.1.1 + resolution: "@rc-component/color-picker@npm:3.1.1" + dependencies: + "@ant-design/fast-color": "npm:^3.0.1" + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/7eb5308e20ecc4b164294642d5115b923aab7d282afbf555ed858eed2711ee65c3b204e6776e6accda1da352bd4e51e6302952dc7206f9f2d9796b068e550c32 + languageName: node + linkType: hard + +"@rc-component/context@npm:^2.0.1": + version: 2.0.2 + resolution: "@rc-component/context@npm:2.0.2" + dependencies: + "@rc-component/util": "npm:^1.11.0" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/64e8f332098502cef5dbb23a47ab566587b5a6d8d91d528868744449ce07dc4cf4c20fefa403bd153e9605921dcb48c057dcd5267a99c28e17ee467b69067ac8 + languageName: node + linkType: hard + +"@rc-component/dialog@npm:~1.10.0": + version: 1.10.0 + resolution: "@rc-component/dialog@npm:1.10.0" + dependencies: + "@rc-component/motion": "npm:^1.3.3" + "@rc-component/portal": "npm:^2.1.0" + "@rc-component/util": "npm:^1.9.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/0defdb60e7d147c22111442f7edbe61d5eaa732cd31bafb05d8f798119e418234270f0e48b515aaffd00d9158988f14f80d6fb95ec07cfc0dbbe29f103d367c9 + languageName: node + linkType: hard + +"@rc-component/drawer@npm:~1.4.2": + version: 1.4.2 + resolution: "@rc-component/drawer@npm:1.4.2" + dependencies: + "@rc-component/motion": "npm:^1.1.4" + "@rc-component/portal": "npm:^2.1.3" + "@rc-component/util": "npm:^1.9.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/4d69581372ec7ce00f36a0a61010e0129f20b78ebece324df75b8107cd76d2590e94540cff95b48102c62d8e1042ffeb215e2a35a2175cdd3a40eb350470cefd + languageName: node + linkType: hard + +"@rc-component/dropdown@npm:~1.0.0, @rc-component/dropdown@npm:~1.0.3": + version: 1.0.3 + resolution: "@rc-component/dropdown@npm:1.0.3" + dependencies: + "@rc-component/trigger": "npm:^3.0.0" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.11.0" + react-dom: ">=16.11.0" + checksum: 10c0/b94641655106e4e356fec9a3850c411f3a7f88b163a1298ceec351f2c7788d5883ef149d9e0749adb0e1545da3debe7c1dde3c22fcc8c6960066d12ccb075b86 + languageName: node + linkType: hard + +"@rc-component/form@npm:~1.8.5": + version: 1.8.6 + resolution: "@rc-component/form@npm:1.8.6" + dependencies: + "@rc-component/async-validator": "npm:^6.0.0" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/dbedb4527642ae31cb478c69958af0ac20eefc3185e5809d36d5e681928876d5270f67da1d2ca1064e64954e08f676039f990311829c15d9d37da2e35f58d6c2 + languageName: node + linkType: hard + +"@rc-component/image@npm:~1.9.0": + version: 1.9.0 + resolution: "@rc-component/image@npm:1.9.0" + dependencies: + "@rc-component/motion": "npm:^1.0.0" + "@rc-component/portal": "npm:^2.1.2" + "@rc-component/util": "npm:^1.10.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/92ccc2050ba94f8121b686a63af59ede944fce9e0c2d3e7ab51fcc6b9027f68e981067e6039efc6c673b93b6af4be0f5df87c99e068939ee37efda4ecd77134a + languageName: node + linkType: hard + +"@rc-component/input-number@npm:~1.6.2": + version: 1.6.2 + resolution: "@rc-component/input-number@npm:1.6.2" + dependencies: + "@rc-component/mini-decimal": "npm:^1.0.1" + "@rc-component/util": "npm:^1.4.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/322a585d9e1a86b4e775de27cd9cbad94f38c4d21d1848148eb12e6114e74e4de148414d3ac89e24d312a471d482d4d6259a528b22b3e2ca3c44ab230284ba8f + languageName: node + linkType: hard + +"@rc-component/input@npm:~1.3.0, @rc-component/input@npm:~1.3.1": + version: 1.3.1 + resolution: "@rc-component/input@npm:1.3.1" + dependencies: + "@rc-component/resize-observer": "npm:^1.1.1" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.0.0" + react-dom: ">=16.0.0" + checksum: 10c0/8a61ab9dfef2dc5044f7cb51e5da81700a76f1dd586e63256563ad9f820ab8f226beb295b9ae12b676c079b016fe3551ab4d09768b44e2eee317885cfeda8499 + languageName: node + linkType: hard + +"@rc-component/mentions@npm:~1.10.0": + version: 1.10.0 + resolution: "@rc-component/mentions@npm:1.10.0" + dependencies: + "@rc-component/input": "npm:~1.3.0" + "@rc-component/menu": "npm:~1.4.0" + "@rc-component/trigger": "npm:^3.0.0" + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/e8ae2b51bbd6ef7023d51a6c884d3a7f7e57cb73508d51f84364223246f2fa05a6876ff580090a8598113be192c9b84b9aac60b5d42ed737616e301a0b4e7503 + languageName: node + linkType: hard + +"@rc-component/menu@npm:~1.4.0, @rc-component/menu@npm:~1.4.1": + version: 1.4.1 + resolution: "@rc-component/menu@npm:1.4.1" + dependencies: + "@rc-component/motion": "npm:^1.1.4" + "@rc-component/overflow": "npm:^1.0.0" + "@rc-component/trigger": "npm:^3.0.0" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/6fd214cf3ee2bc48ddd40f958c8ad20a95804f19850193934b4429b0d305773c46da5df3fb57deade2febb12c60c3b8c63337f546928d5ab83f68123c8d65ea4 + languageName: node + linkType: hard + +"@rc-component/mini-decimal@npm:^1.0.1": + version: 1.1.4 + resolution: "@rc-component/mini-decimal@npm:1.1.4" + dependencies: + "@babel/runtime": "npm:^7.18.0" + checksum: 10c0/32215c6a71ea0dba03fd9a7769f0fd23008d40d3c0c494180acc3d50746f3f8cc122ab2c0e07664271f21123455d5f1552f951357b288f19f2e0a06d95a4174e + languageName: node + linkType: hard + +"@rc-component/motion@npm:^1.0.0, @rc-component/motion@npm:^1.1.3, @rc-component/motion@npm:^1.1.4, @rc-component/motion@npm:^1.3.3": + version: 1.3.3 + resolution: "@rc-component/motion@npm:1.3.3" + dependencies: + "@rc-component/util": "npm:^1.11.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/1731f51d3856f514ac1d36fe7d1747c722e13e1404a1df0b8000d9cded5321771d7079c1d1016705531c148c38cd9054c7de2e4fe08beb05e8e4d96956ed6c0c + languageName: node + linkType: hard + +"@rc-component/mutate-observer@npm:^2.0.1": + version: 2.0.1 + resolution: "@rc-component/mutate-observer@npm:2.0.1" + dependencies: + "@rc-component/util": "npm:^1.2.0" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/45b63795a818eba95ca67dd28aacd9f535ef652919a13bed692d077a818e0365dbae6acf1bac83952905064767e6b1b176da998e52bc00d0a35f89fe866b6884 + languageName: node + linkType: hard + +"@rc-component/notification@npm:~2.0.7": + version: 2.0.7 + resolution: "@rc-component/notification@npm:2.0.7" + dependencies: + "@rc-component/motion": "npm:^1.1.4" + "@rc-component/util": "npm:^1.11.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/3a21cf070c43769fc98abe0c2818012204ec4f96a99be7463322b8f1d3644e7572a394c0db43851ccf7bafd3676660308cc082970360bb9a55a0670f11f385b0 + languageName: node + linkType: hard + +"@rc-component/overflow@npm:^1.0.0": + version: 1.0.1 + resolution: "@rc-component/overflow@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.11.1" + "@rc-component/resize-observer": "npm:^1.0.1" + "@rc-component/util": "npm:^1.4.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/710df9afcf06ea80a79a89bd7f92bf92d524ac6fa3c82cbd5e48e18e74bf60a4b56705dfcb2d9738c64091db87bd0f8336959a952e175d67eb63aad36395ba9a + languageName: node + linkType: hard + +"@rc-component/pagination@npm:~1.4.0": + version: 1.4.0 + resolution: "@rc-component/pagination@npm:1.4.0" + dependencies: + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/36a425c3fe7266c71969c27278bb01171fed92394f21f1a6da62213f925069415cfb2281f7c2a0d8346623b9e49750e92fa85191c63aa9f7241ead024c696f7c + languageName: node + linkType: hard + +"@rc-component/picker@npm:~1.11.0": + version: 1.11.0 + resolution: "@rc-component/picker@npm:1.11.0" + dependencies: + "@rc-component/overflow": "npm:^1.0.0" + "@rc-component/resize-observer": "npm:^1.0.0" + "@rc-component/trigger": "npm:^3.6.15" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + date-fns: ">= 2.x" + dayjs: ">= 1.x" + luxon: ">= 3.x" + moment: ">= 2.x" + react: ">=16.9.0" + react-dom: ">=16.9.0" + peerDependenciesMeta: + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + checksum: 10c0/da5385e06e0daaf99dfcebcc46f3b16d6316810c8ad39800d5b38edc0fc5ba59ec504e99611b0ad4c411a6efdb2ef861f60246e0ba6e1ff5db28da9d3fb6efbd + languageName: node + linkType: hard + +"@rc-component/portal@npm:^2.1.0, @rc-component/portal@npm:^2.1.2, @rc-component/portal@npm:^2.1.3, @rc-component/portal@npm:^2.2.0, @rc-component/portal@npm:^2.2.1": + version: 2.2.1 + resolution: "@rc-component/portal@npm:2.2.1" + dependencies: + "@rc-component/util": "npm:^1.11.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/6576386bae88d139bc8b7e1699689bed38a14b7a555e6c185aa6e408a2c6eac421eb363fc9a7428b29ede0c1a526f227aeec239f4bbe8c8e05d569dd27faf8eb + languageName: node + linkType: hard + +"@rc-component/progress@npm:~1.0.2": + version: 1.0.2 + resolution: "@rc-component/progress@npm:1.0.2" + dependencies: + "@rc-component/util": "npm:^1.2.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/2148f75bd0f4ad8431669a91acd7e4d69566387e315860e4b83a6ff1b85271703540a4050c03815c857825c25a4fcd6ab0f8ed78071c96513cb784efbb385483 + languageName: node + linkType: hard + +"@rc-component/qrcode@npm:~2.0.0": + version: 2.0.0 + resolution: "@rc-component/qrcode@npm:2.0.0" + dependencies: + "@babel/runtime": "npm:^7.24.7" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/b0b21e574719214bd30195df0dda2b29fa87394b40d6108f3d6f42b029d4d82de230a74338d72e1e04c0a61b90ae25e532cf366289c2b6deb6d92e4ef314562e + languageName: node + linkType: hard + +"@rc-component/rate@npm:~1.0.1": + version: 1.0.1 + resolution: "@rc-component/rate@npm:1.0.1" + dependencies: + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/34eba661e6d54286f8ef1934ccbab2d7a6269b26249f84393b77206151706c6cc56001a497a841abcc1ed1221f86134e4b7fd34f216287ef46e50869fdd9d99d + languageName: node + linkType: hard + +"@rc-component/resize-observer@npm:^1.0.0, @rc-component/resize-observer@npm:^1.0.1, @rc-component/resize-observer@npm:^1.1.1, @rc-component/resize-observer@npm:^1.1.2": + version: 1.1.2 + resolution: "@rc-component/resize-observer@npm:1.1.2" + dependencies: + "@rc-component/util": "npm:^1.2.0" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/059b6015adac38c1001940360308d44e47a0bd62992be4c0b6e566df6d5429a8b60428cd6d21289da48a393c8cd6ead4c61032b36315ca394e660d3261956c33 + languageName: node + linkType: hard + +"@rc-component/segmented@npm:~1.3.0": + version: 1.3.0 + resolution: "@rc-component/segmented@npm:1.3.0" + dependencies: + "@babel/runtime": "npm:^7.11.1" + "@rc-component/motion": "npm:^1.1.4" + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.0.0" + react-dom: ">=16.0.0" + checksum: 10c0/ef76db98618700439673670c4b6ea00a822e02c4e90ea6781b9f626ba5acbae861b2a7c8acc4985d8b5171fbe8bf4d81b31387bc26f3453f999e2d8348d7e792 + languageName: node + linkType: hard + +"@rc-component/select@npm:~1.8.0, @rc-component/select@npm:~1.8.2": + version: 1.8.2 + resolution: "@rc-component/select@npm:1.8.2" + dependencies: + "@rc-component/overflow": "npm:^1.0.0" + "@rc-component/trigger": "npm:^3.0.0" + "@rc-component/util": "npm:^1.11.1" + "@rc-component/virtual-list": "npm:^1.2.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10c0/2bd6e2a361501e257f2ec57fcaadf3f95aa8e93f3f2f441c15dfbb12b2d4e51c21fb6514d0728daeddb96722ddacf66ddb4f623a73d8cfa722b0874c81729132 + languageName: node + linkType: hard + +"@rc-component/slider@npm:~1.1.1": + version: 1.1.1 + resolution: "@rc-component/slider@npm:1.1.1" + dependencies: + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/e68a1c8d36881291c3ba70482a4d28d3e8a532af0f094d358c63a16632aa79e8a16a652f302606b7ab0cbf128a132a904068740a660f9cc1ca62868bb0b68b16 + languageName: node + linkType: hard + +"@rc-component/steps@npm:~1.2.2": + version: 1.2.2 + resolution: "@rc-component/steps@npm:1.2.2" + dependencies: + "@rc-component/util": "npm:^1.2.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/6910ab1a7bccd91a12d5a839a012599389b6e4be71637280200913e160a2f12be1bf8b7c211f3ce9f02d7791677225178b5012e309c00f9f45471c33b052d302 + languageName: node + linkType: hard + +"@rc-component/switch@npm:~1.0.3": + version: 1.0.3 + resolution: "@rc-component/switch@npm:1.0.3" + dependencies: + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/6ef1746f056d63bae06a00fa09a4c437539f7e60821c479722dc9800a327564b5e749d7f5d3874236d9424e4eb56ddee4561b56f0ce8d1417d431e710b9789f9 + languageName: node + linkType: hard + +"@rc-component/table@npm:~1.10.4": + version: 1.10.4 + resolution: "@rc-component/table@npm:1.10.4" + dependencies: + "@rc-component/context": "npm:^2.0.1" + "@rc-component/resize-observer": "npm:^1.0.0" + "@rc-component/util": "npm:^1.11.1" + "@rc-component/virtual-list": "npm:^1.0.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/331397f5e36f7c4432ced7476ec616e15bbe680d76bf5ab7a630a3617e43569f3eb21c4c1062e5b8ebae63dc1ea0973f45611c386dbf7f2feb2cf7a7c0ee7c08 + languageName: node + linkType: hard + +"@rc-component/tabs@npm:~1.11.0": + version: 1.11.0 + resolution: "@rc-component/tabs@npm:1.11.0" + dependencies: + "@rc-component/dropdown": "npm:~1.0.0" + "@rc-component/menu": "npm:~1.4.0" + "@rc-component/motion": "npm:^1.1.3" + "@rc-component/resize-observer": "npm:^1.0.0" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/32eda862e17d3375b6216cd64579f6bf65d2526ce30ba5b2abffa620a89f0e559079659fd295e1404aca09517c13ea83c3524c980cd2a861c91f5c195de7dcff + languageName: node + linkType: hard + +"@rc-component/tooltip@npm:~1.4.0": + version: 1.4.0 + resolution: "@rc-component/tooltip@npm:1.4.0" + dependencies: + "@rc-component/trigger": "npm:^3.7.1" + "@rc-component/util": "npm:^1.3.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/219019822fbbec7c1920285fa784df936b0abd54cc66fe1927b90023c750f2fd3693e0ebc75201b469b68c14e4a95a94ed53f3cfb1606b9a165dedbe35e11f88 + languageName: node + linkType: hard + +"@rc-component/tour@npm:~2.4.0": + version: 2.4.0 + resolution: "@rc-component/tour@npm:2.4.0" + dependencies: + "@rc-component/portal": "npm:^2.2.0" + "@rc-component/trigger": "npm:^3.0.0" + "@rc-component/util": "npm:^1.7.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/a65a88d81d04ea588f778ef19427db25f95f7827c2fb2b038dfeeccbbc025a885b96c63fbabf57f46d6a3c31d8be1fe029980bd444a7c02157cb9c40130d3c9d + languageName: node + linkType: hard + +"@rc-component/tree-select@npm:~1.11.0": + version: 1.11.0 + resolution: "@rc-component/tree-select@npm:1.11.0" + dependencies: + "@rc-component/select": "npm:~1.8.0" + "@rc-component/tree": "npm:~1.3.2" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10c0/8f06101aabcbed01634c92b185475b50712ae651994e977dd747436222e283fd9da39c48ce516ee55d321f578b058642251c36ba2bfa8141184df79eea7c1e8f + languageName: node + linkType: hard + +"@rc-component/tree@npm:~1.3.2": + version: 1.3.2 + resolution: "@rc-component/tree@npm:1.3.2" + dependencies: + "@rc-component/motion": "npm:^1.0.0" + "@rc-component/util": "npm:^1.11.1" + "@rc-component/virtual-list": "npm:^1.2.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: "*" + react-dom: "*" + checksum: 10c0/167a1cf662b116415e31af440868a1e80b399cda6f2bdfa72cefe080c93602f2ea75357e52a937e1f02158a2b17c72e9b8271e7ee4a4798892bd7c7f0f9592d6 + languageName: node + linkType: hard + +"@rc-component/trigger@npm:^3.0.0, @rc-component/trigger@npm:^3.10.1, @rc-component/trigger@npm:^3.6.15, @rc-component/trigger@npm:^3.7.1": + version: 3.10.1 + resolution: "@rc-component/trigger@npm:3.10.1" + dependencies: + "@rc-component/motion": "npm:^1.3.3" + "@rc-component/portal": "npm:^2.2.1" + "@rc-component/resize-observer": "npm:^1.1.2" + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/20f70f4de12d4077ba6dac9352e3cf7386e6c7e9092752a6951938fbe591acc7a511f7ecacfa534d1e7ee1a858077b91795ceec381ef34bff866699d3a57d549 + languageName: node + linkType: hard + +"@rc-component/upload@npm:~1.1.1": + version: 1.1.1 + resolution: "@rc-component/upload@npm:1.1.1" + dependencies: + "@rc-component/util": "npm:^1.11.1" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 10c0/0bb37a2050df385aee1767231afcb6432201765c679141c6d0198eea83496e4d47975d0e7a403d40531c5916dd78003805a15feb4f6c8a128cf9c8d0a886ca84 + languageName: node + linkType: hard + +"@rc-component/util@npm:^1.10.1, @rc-component/util@npm:^1.11.0, @rc-component/util@npm:^1.11.1, @rc-component/util@npm:^1.12.0, @rc-component/util@npm:^1.2.0, @rc-component/util@npm:^1.2.1, @rc-component/util@npm:^1.3.0, @rc-component/util@npm:^1.4.0, @rc-component/util@npm:^1.7.0, @rc-component/util@npm:^1.9.0": + version: 1.12.0 + resolution: "@rc-component/util@npm:1.12.0" + dependencies: + is-mobile: "npm:^5.0.0" + react-is: "npm:^19.2.7" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/357aa5d88dc2662d81aa9d84c94ba0349589b53c89ad46989bdaf08840902186d4970f35b1f62cdf1fb0be2632323f3c56f2eb5e5ac961e0107333e34e7a34ab + languageName: node + linkType: hard + +"@rc-component/virtual-list@npm:^1.0.1, @rc-component/virtual-list@npm:^1.2.0": + version: 1.4.0 + resolution: "@rc-component/virtual-list@npm:1.4.0" + dependencies: + "@babel/runtime": "npm:^8.0.0" + "@rc-component/resize-observer": "npm:^1.0.1" + "@rc-component/util": "npm:^1.4.0" + clsx: "npm:^2.1.1" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/2c347a0079a2ff54304355104cea661de482c536ab45b9e0a7b329161a94200bcddb239fe74e1e2eddfc0571fff26e68d38b1dd5d4a202decddd7b62b67b50d4 + languageName: node + linkType: hard + +"@rolldown/binding-android-arm64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-android-arm64@npm:1.2.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-darwin-arm64@npm:1.2.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-darwin-x64@npm:1.2.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-freebsd-x64@npm:1.2.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.2.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.2.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.2.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.2.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.2.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.2.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.2.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.2.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.2.1" + dependencies: + "@emnapi/core": "npm:2.0.0-alpha.3" + "@emnapi/runtime": "npm:2.0.0-alpha.3" + "@napi-rs/wasm-runtime": "npm:^1.2.0" + checksum: 10c0/e5bbfad1862187c60cd9cab802af22c71b2496f89fe73a161931f04594f9db1e09ad7a503a2f980f55a2f6d5a4d2013ad1d17260970ac170a1aa6b9286a41738 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.2.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.2.1": + version: 1.2.1 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.2.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/pluginutils@npm:^1.0.0, @rolldown/pluginutils@npm:^1.0.1": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd + languageName: node + linkType: hard + +"@standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 + languageName: node + linkType: hard + +"@testing-library/dom@npm:^10.4.1": + version: 10.4.1 + resolution: "@testing-library/dom@npm:10.4.1" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.3.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" + pretty-format: "npm:^27.0.2" + checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1 + languageName: node + linkType: hard + +"@testing-library/jest-dom@npm:^7.0.0": + version: 7.0.0 + resolution: "@testing-library/jest-dom@npm:7.0.0" + dependencies: + "@adobe/css-tools": "npm:^4.4.0" + aria-query: "npm:^5.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + picocolors: "npm:^1.1.1" + redent: "npm:^3.0.0" + peerDependencies: + "@testing-library/dom": ">=10 <11" + checksum: 10c0/f5ecf821ed098438a5e9ce993d431505f0a1af43edb05e423c6ab7becf9d7dbe46385fc93e0e460a4c7996f4513cad72b8967dd8e98c75bc7ea6733ff7efcf4e + languageName: node + linkType: hard + +"@testing-library/react@npm:^16.3.2": + version: 16.3.2 + resolution: "@testing-library/react@npm:16.3.2" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/f9c7f0915e1b5f7b750e6c7d8b51f091b8ae7ea99bacb761d7b8505ba25de9cfcb749a0f779f1650fb268b499dd79165dc7e1ee0b8b4cb63430d3ddc81ffe044 + languageName: node + linkType: hard + +"@testing-library/user-event@npm:^14.6.1": + version: 14.6.1 + resolution: "@testing-library/user-event@npm:14.6.1" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: 10c0/75fea130a52bf320d35d46ed54f3eec77e71a56911b8b69a3fe29497b0b9947b2dc80d30f04054ad4ce7f577856ae3e5397ea7dff0ef14944d3909784c7a93fe + languageName: node + linkType: hard + +"@tybys/wasm-util@npm:^0.10.3": + version: 0.10.3 + resolution: "@tybys/wasm-util@npm:0.10.3" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/fd2bd2a79c6cd8c79ed1cf7a0fa375c64589264c88a27acaf9756d556b453ea222b62a4f68dd2fbb8b3a78b6bab3b1f4fb2431b6afc6aeda8344b53a521a1cd3 + languageName: node + linkType: hard + +"@types/aria-query@npm:^5.0.1": + version: 5.0.4 + resolution: "@types/aria-query@npm:5.0.4" + checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08 + languageName: node + linkType: hard + +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f + languageName: node + linkType: hard + +"@types/debug@npm:^4.0.0": + version: 4.1.13 + resolution: "@types/debug@npm:4.1.13" + dependencies: + "@types/ms": "npm:*" + checksum: 10c0/e5e124021bbdb23a82727eee0a726ae0fc8a3ae1f57253cbcc47497f259afb357de7f6941375e773e1abbfa1604c1555b901a409d762ec2bb4c1612131d4afb7 + languageName: node + linkType: hard + +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + +"@types/esrecurse@npm:^4.3.1": + version: 4.3.1 + resolution: "@types/esrecurse@npm:4.3.1" + checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03 + languageName: node + linkType: hard + +"@types/estree-jsx@npm:^1.0.0": + version: 1.0.5 + resolution: "@types/estree-jsx@npm:1.0.5" + dependencies: + "@types/estree": "npm:*" + checksum: 10c0/07b354331516428b27a3ab99ee397547d47eb223c34053b48f84872fafb841770834b90cc1a0068398e7c7ccb15ec51ab00ec64b31dc5e3dbefd624638a35c6d + languageName: node + linkType: hard + +"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 10c0/3ad3286ca2988cd550dafb8f2ad599c8474868e954fa601a36655bdfefd8039f7c714b8c1c7f2ae219ffbd58bd4660e66fa7479a0120fc02d4777057d4865387 + languageName: node + linkType: hard + +"@types/hast@npm:^3.0.0": + version: 3.0.5 + resolution: "@types/hast@npm:3.0.5" + dependencies: + "@types/unist": "npm:*" + checksum: 10c0/f3af8594a6903a507ed191eda944af18099198d6708c29102ae17118c3e20779f6929e91ed37e033541fd28d58055d8a5910a4366dbdf976cf81b13a464741fb + languageName: node + linkType: hard + +"@types/json-schema@npm:^7.0.15": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db + languageName: node + linkType: hard + +"@types/mdast@npm:^4.0.0": + version: 4.0.4 + resolution: "@types/mdast@npm:4.0.4" + dependencies: + "@types/unist": "npm:*" + checksum: 10c0/84f403dbe582ee508fd9c7643ac781ad8597fcbfc9ccb8d4715a2c92e4545e5772cbd0dbdf18eda65789386d81b009967fdef01b24faf6640f817287f54d9c82 + languageName: node + linkType: hard + +"@types/ms@npm:*": + version: 2.1.0 + resolution: "@types/ms@npm:2.1.0" + checksum: 10c0/5ce692ffe1549e1b827d99ef8ff71187457e0eb44adbae38fdf7b9a74bae8d20642ee963c14516db1d35fa2652e65f47680fdf679dcbde52bbfadd021f497225 + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:^26.1.2": + version: 26.1.2 + resolution: "@types/node@npm:26.1.2" + dependencies: + undici-types: "npm:~8.3.0" + checksum: 10c0/a45503222c7db8f374afd5c9381db63dd95b6b1f703abea0890dd3d4a09eeb41da489e08a1a45baf18fe89fb77fbf310ff2789f680c490b04dafc41a60800a86 + languageName: node + linkType: hard + +"@types/parse-json@npm:^4.0.0": + version: 4.0.2 + resolution: "@types/parse-json@npm:4.0.2" + checksum: 10c0/b1b863ac34a2c2172fbe0807a1ec4d5cb684e48d422d15ec95980b81475fac4fdb3768a8b13eef39130203a7c04340fc167bae057c7ebcafd7dec9fe6c36aeb1 + languageName: node + linkType: hard + +"@types/react-dom@npm:^19.2.3": + version: 19.2.3 + resolution: "@types/react-dom@npm:19.2.3" + peerDependencies: + "@types/react": ^19.2.0 + checksum: 10c0/b486ebe0f4e2fb35e2e108df1d8fc0927ca5d6002d5771e8a739de11239fe62d0e207c50886185253c99eb9dedfeeb956ea7429e5ba17f6693c7acb4c02f8cd1 + languageName: node + linkType: hard + +"@types/react@npm:^19.2.17": + version: 19.2.17 + resolution: "@types/react@npm:19.2.17" + dependencies: + csstype: "npm:^3.2.2" + checksum: 10c0/bc2c4af96b3e480604424de70d5ebda90c5f4b485df471858c0bc2d7d70364b606ec3c4d8579f94f01aa0c6c0591f56bcf14cba5689f5eea4b74250ccdc3a232 + languageName: node + linkType: hard + +"@types/set-cookie-parser@npm:^2.4.10": + version: 2.4.10 + resolution: "@types/set-cookie-parser@npm:2.4.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/010b0c582ea70a2088618b4725808e80c30cce296c19ec58e51d94e0fd1038201b7b99238bf3ea74e1894163c8037d10a4f1729de62b2801ce240ff070f43e76 + languageName: node + linkType: hard + +"@types/statuses@npm:^2.0.6": + version: 2.0.6 + resolution: "@types/statuses@npm:2.0.6" + checksum: 10c0/dd88c220b0e2c6315686289525fd61472d2204d2e4bef4941acfb76bda01d3066f749ac74782aab5b537a45314fcd7d6261eefa40b6ec872691f5803adaa608d + languageName: node + linkType: hard + +"@types/unist@npm:*, @types/unist@npm:^3.0.0": + version: 3.0.3 + resolution: "@types/unist@npm:3.0.3" + checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60 + languageName: node + linkType: hard + +"@types/unist@npm:^2.0.0": + version: 2.0.11 + resolution: "@types/unist@npm:2.0.11" + checksum: 10c0/24dcdf25a168f453bb70298145eb043cfdbb82472db0bc0b56d6d51cd2e484b9ed8271d4ac93000a80da568f2402e9339723db262d0869e2bf13bc58e081768d + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.65.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.65.0" + "@typescript-eslint/type-utils": "npm:8.65.0" + "@typescript-eslint/utils": "npm:8.65.0" + "@typescript-eslint/visitor-keys": "npm:8.65.0" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + "@typescript-eslint/parser": ^8.65.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/1d9ddad417b43037c35c91a7c30bfc0015ccc40da57fe9faadae97fbaee6031a539a35265a9933d3bb946f307c397b7a00953806339576a721d619695a972079 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/parser@npm:8.65.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.65.0" + "@typescript-eslint/types": "npm:8.65.0" + "@typescript-eslint/typescript-estree": "npm:8.65.0" + "@typescript-eslint/visitor-keys": "npm:8.65.0" + debug: "npm:^4.4.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/b3f5333abe5dfb08b53b89c824d045d96d5eb5798fab45eeedfaab72e4ce133a82fb4ebbc1acf79098aac9b08f7acc119fe0edd63ac2f91d80c98f5ca87c41e0 + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/project-service@npm:8.65.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.65.0" + "@typescript-eslint/types": "npm:^8.65.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/56d8f598f1bb6ca892ff79320bd1aa134eefa05cf0bad4932c44518475a8bccf0c9027ae2177b3c1761496c8107236b81dd93f67d09093c2242f07f3f2063c1f + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/scope-manager@npm:8.65.0" + dependencies: + "@typescript-eslint/types": "npm:8.65.0" + "@typescript-eslint/visitor-keys": "npm:8.65.0" + checksum: 10c0/303bae83a2243e742fcf86bef0b67615dc8915d2dd40268b796e2f49a40057b05de41608846aa362ad77e2d6976ec3cf30f1cdf245c1e39d18fd8ce91ae38c5c + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.65.0, @typescript-eslint/tsconfig-utils@npm:^8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.65.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/5b897bbba4584ee67c7a0bc0b4b6cbf8db2cb5997d5ab8f07fae692315dd51cabcc7116c609a94a162747dd267118b7a0f1c8fb29d71210ad38549e4b8b6adb1 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/type-utils@npm:8.65.0" + dependencies: + "@typescript-eslint/types": "npm:8.65.0" + "@typescript-eslint/typescript-estree": "npm:8.65.0" + "@typescript-eslint/utils": "npm:8.65.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/05a1a1283020f63eac3cb6202afd08459531a4522a85ceb0f5789f2902df7c180565f65f217cc780127888b7113b1c8c34aa6bfd7020d568f01a17f290216e9b + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.65.0, @typescript-eslint/types@npm:^8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/types@npm:8.65.0" + checksum: 10c0/919b6d96111ea26f09c6cb1121fbba915147761be3fbf9c4de5b200faa2891087ebdcfd2f4a1f27a4c6153daeefc7448147c651a074b3ec70440f3f8c1092a81 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.65.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.65.0" + "@typescript-eslint/tsconfig-utils": "npm:8.65.0" + "@typescript-eslint/types": "npm:8.65.0" + "@typescript-eslint/visitor-keys": "npm:8.65.0" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/578ff5247f17865277badfe13362a82ba82c8bb11aaa78323933895e0ba0fc02788ae6aa9bd84bc8287660004a76231341977a3188b399c776b7e713c5054239 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/utils@npm:8.65.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.65.0" + "@typescript-eslint/types": "npm:8.65.0" + "@typescript-eslint/typescript-estree": "npm:8.65.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/258775532bb23bfeccb7ef25369a62b5fcf48f633af178830113d5aea6d0e968ad67a63b3371206151eb6a8dca0a3381f7efa07958fd8063505e1a53e470a439 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.65.0": + version: 8.65.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.65.0" + dependencies: + "@typescript-eslint/types": "npm:8.65.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/f50cf2da4077f8eebfd56658ede17dfbf2e39875dc53562f5c1bc6e9835a0b4b00e0e0255e2dc3488234aca1f783d89beb084c417b22027520bf015a7b59ffb8 + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.0.0": + version: 1.3.3 + resolution: "@ungap/structured-clone@npm:1.3.3" + checksum: 10c0/b199e280ee06e9c447e0ccd38df60a65c2b2c13d3c77af50b1e6d77230aee1ea7da309d7b80c5a4850c8e22e4eee9e4b2813c6a33f8c360560d7f2e99a9f6e8f + languageName: node + linkType: hard + +"@vitejs/plugin-react@npm:^6.0.5": + version: 6.0.5 + resolution: "@vitejs/plugin-react@npm:6.0.5" + dependencies: + "@rolldown/pluginutils": "npm:^1.0.1" + peerDependencies: + "@rolldown/plugin-babel": ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + "@rolldown/plugin-babel": + optional: true + babel-plugin-react-compiler: + optional: true + checksum: 10c0/fb02246fe3652d7fb746190bdd098b9e29918dfcf8c67b9c0c37300ce789e82331b2ba761530ac6ac9a61390b774d44f401787bd1c87a6c866d1e74a745cc1a0 + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/expect@npm:4.1.10" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.10" + "@vitest/utils": "npm:4.1.10" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/a817ad0d9bd6a039776a7228d54fb8319c17e4af15917407f5566ac61781a8511f591d302519d6999217399915bc3c0290028189fc73f5c38f80cb01b6f19c8d + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/mocker@npm:4.1.10" + dependencies: + "@vitest/spy": "npm:4.1.10" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/4aa70b0df58681652e2e28093437fb2e8f4d02a6d03f5619abc266ac1c5ae5f43326148061d13ae6e071e0f6cfcf7634659af63644de8ce098a7c98949a3d1ad + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/pretty-format@npm:4.1.10" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/1a5daba730ffe23f2000bff484b4b2842f3b178d93663cb487b215516b8d3b62caa3e2bb2a3c63307b61a9fe58fb9bfff38559bc0c5e49d8aa403d6803a1d918 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/runner@npm:4.1.10" + dependencies: + "@vitest/utils": "npm:4.1.10" + pathe: "npm:^2.0.3" + checksum: 10c0/554b72639de9694271b99be8ae273fe12ec793093ec91cce143816cd1187d40b7138a4d9d4de4f456cfca9567de986825bff97e107c05b9eb4abc130e854286d + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/snapshot@npm:4.1.10" + dependencies: + "@vitest/pretty-format": "npm:4.1.10" + "@vitest/utils": "npm:4.1.10" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/e71398725f51af5fd0c07bb4b957d0f987daf9b4c564ac24cb2a4d1afde1a6939f535ac17761a32dcc41b0a1e6d4088af66dc44df89fdebebb92aabed1a92b5f + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/spy@npm:4.1.10" + checksum: 10c0/e5c08012560af6727fd66741c5cda25560d7c5442103d0c83e4276a9b0dd90b9da6cdf823a461195229a16c6ff87768ce788a68d0fa29dea73ee285618668178 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/utils@npm:4.1.10" + dependencies: + "@vitest/pretty-format": "npm:4.1.10" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/05b0ecec6997ec22fc08377e57dbd8fa37992e05961f3a7a916d98b1ab56d15c2a87dbd83d392b628242bdc156b1705e7fa60a3bf0c54bdb51158c153e05fc5d + languageName: node + linkType: hard + +"abbrev@npm:^5.0.0": + version: 5.0.0 + resolution: "abbrev@npm:5.0.0" + checksum: 10c0/8e88f5c798ea4562d28c5a3e9ad69e3879890bc5d695d8f2dffb8609be4c890aacc8f80ef4553fdd2c6a62d70c2ce8bc57b38074e383beb7487bdafa9ed42ea5 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/4c54868fbef3b8d58927d5e33f0a4de35f59012fe7b12cf9dfbb345fb8f46607709e1c4431be869a23fb63c151033d84c4198fa9f79385cec34fcb1dd53974c1 + languageName: node + linkType: hard + +"acorn@npm:^8.16.0": + version: 8.18.0 + resolution: "acorn@npm:8.18.0" + bin: + acorn: bin/acorn + checksum: 10c0/be771be2135cc07910cf76f444ad514d7dcfd6d4a8026e597e93155275abc8ef61eee12211d52146e9d962874269b634f397464942087be013c89d0c54c5f8e5 + languageName: node + linkType: hard + +"ajv@npm:^6.14.0": + version: 6.15.0 + resolution: "ajv@npm:6.15.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10c0/67966499dd272ecde1c2e467084411132891523d057487587879d39ac04207f4351b7b2324c83198013967fbfa632c1612adc960114a30770fbe07a0773b32c2 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df + languageName: node + linkType: hard + +"antd@npm:^6.5.2": + version: 6.5.2 + resolution: "antd@npm:6.5.2" + dependencies: + "@ant-design/colors": "npm:^8.0.1" + "@ant-design/cssinjs": "npm:^2.1.2" + "@ant-design/cssinjs-utils": "npm:^2.1.2" + "@ant-design/fast-color": "npm:^3.0.1" + "@ant-design/icons": "npm:^6.3.2" + "@ant-design/react-slick": "npm:~2.0.0" + "@babel/runtime": "npm:^7.29.2" + "@rc-component/cascader": "npm:~1.17.0" + "@rc-component/checkbox": "npm:~2.0.0" + "@rc-component/collapse": "npm:~1.2.0" + "@rc-component/color-picker": "npm:~3.1.1" + "@rc-component/dialog": "npm:~1.10.0" + "@rc-component/drawer": "npm:~1.4.2" + "@rc-component/dropdown": "npm:~1.0.3" + "@rc-component/form": "npm:~1.8.5" + "@rc-component/image": "npm:~1.9.0" + "@rc-component/input": "npm:~1.3.1" + "@rc-component/input-number": "npm:~1.6.2" + "@rc-component/mentions": "npm:~1.10.0" + "@rc-component/menu": "npm:~1.4.1" + "@rc-component/motion": "npm:^1.3.3" + "@rc-component/mutate-observer": "npm:^2.0.1" + "@rc-component/notification": "npm:~2.0.7" + "@rc-component/pagination": "npm:~1.4.0" + "@rc-component/picker": "npm:~1.11.0" + "@rc-component/progress": "npm:~1.0.2" + "@rc-component/qrcode": "npm:~2.0.0" + "@rc-component/rate": "npm:~1.0.1" + "@rc-component/resize-observer": "npm:^1.1.2" + "@rc-component/segmented": "npm:~1.3.0" + "@rc-component/select": "npm:~1.8.2" + "@rc-component/slider": "npm:~1.1.1" + "@rc-component/steps": "npm:~1.2.2" + "@rc-component/switch": "npm:~1.0.3" + "@rc-component/table": "npm:~1.10.4" + "@rc-component/tabs": "npm:~1.11.0" + "@rc-component/tooltip": "npm:~1.4.0" + "@rc-component/tour": "npm:~2.4.0" + "@rc-component/tree": "npm:~1.3.2" + "@rc-component/tree-select": "npm:~1.11.0" + "@rc-component/trigger": "npm:^3.10.1" + "@rc-component/upload": "npm:~1.1.1" + "@rc-component/util": "npm:^1.12.0" + clsx: "npm:^2.1.1" + dayjs: "npm:^1.11.11" + scroll-into-view-if-needed: "npm:^3.1.0" + throttle-debounce: "npm:^5.0.2" + peerDependencies: + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 10c0/f234c456f02ab55e0614f5ab41d523d958254294ebe082eff0b00266c9e7871b94983e84f342d53555086f5d0f8532071de17890c195cea8f824cdcfaa35d046 + languageName: node + linkType: hard + +"aria-query@npm:5.3.0": + version: 5.3.0 + resolution: "aria-query@npm:5.3.0" + dependencies: + dequal: "npm:^2.0.3" + checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469 + languageName: node + linkType: hard + +"aria-query@npm:^5.0.0": + version: 5.3.2 + resolution: "aria-query@npm:5.3.2" + checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e + languageName: node + linkType: hard + +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + +"babel-plugin-macros@npm:^3.1.0": + version: 3.1.0 + resolution: "babel-plugin-macros@npm:3.1.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + cosmiconfig: "npm:^7.0.0" + resolve: "npm:^1.19.0" + checksum: 10c0/c6dfb15de96f67871d95bd2e8c58b0c81edc08b9b087dc16755e7157f357dc1090a8dc60ebab955e92587a9101f02eba07e730adc253a1e4cf593ca3ebd3839c + languageName: node + linkType: hard + +"bail@npm:^2.0.0": + version: 2.0.2 + resolution: "bail@npm:2.0.2" + checksum: 10c0/25cbea309ef6a1f56214187004e8f34014eb015713ea01fa5b9b7e9e776ca88d0fdffd64143ac42dc91966c915a4b7b683411b56e14929fad16153fc026ffb8b + languageName: node + linkType: hard + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b + languageName: node + linkType: hard + +"baseline-browser-mapping@npm:^2.10.44": + version: 2.11.8 + resolution: "baseline-browser-mapping@npm:2.11.8" + bin: + baseline-browser-mapping: dist/cli.cjs + checksum: 10c0/4fd0ba04e96d9139827eb0aed4b086c789ca9062877a17946b7b51a5be5959412c4919d6c017ad8d233445cbef48716e409e0256326b789a435c699e4c5c7a49 + languageName: node + linkType: hard + +"bidi-js@npm:^1.0.3": + version: 1.0.3 + resolution: "bidi-js@npm:1.0.3" + dependencies: + require-from-string: "npm:^2.0.2" + checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.8": + version: 5.0.9 + resolution: "brace-expansion@npm:5.0.9" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10c0/3dea38884a1c3c8b1c9c44a7402a0c76fca460f70cffb3127242b0b4cbf4472019e022ade021eec44838ff19f1dac2625dfd11dd459d7e1e055b0698a8d52fec + languageName: node + linkType: hard + +"browserslist@npm:^4.24.0": + version: 4.28.7 + resolution: "browserslist@npm:4.28.7" + dependencies: + baseline-browser-mapping: "npm:^2.10.44" + caniuse-lite: "npm:^1.0.30001806" + electron-to-chromium: "npm:^1.5.393" + node-releases: "npm:^2.0.51" + update-browserslist-db: "npm:^1.2.3" + bin: + browserslist: cli.js + checksum: 10c0/dc41922a9ff0d81e5492498bdab2d932e3a3222f4277814ba38ee64f4e51f26e5244a7cce0777b4cac3ceff6eb196f5cadbb2a832620973a7f9c39611f6bc78e + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001806": + version: 1.0.30001806 + resolution: "caniuse-lite@npm:1.0.30001806" + checksum: 10c0/9442c8afff0968e9b2ce0104a7340c1ce4a5c09f469ccb9eef10d25712ea0afe542486e5318c697c70dd502f988cfc04eaa1c9438c92ac3bcf4d708a2a17bee1 + languageName: node + linkType: hard + +"ccount@npm:^2.0.0": + version: 2.0.1 + resolution: "ccount@npm:2.0.1" + checksum: 10c0/3939b1664390174484322bc3f45b798462e6c07ee6384cb3d645e0aa2f318502d174845198c1561930e1d431087f74cf1fe291ae9a4722821a9f4ba67e574350 + languageName: node + linkType: hard + +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + +"character-entities-html4@npm:^2.0.0": + version: 2.1.0 + resolution: "character-entities-html4@npm:2.1.0" + checksum: 10c0/fe61b553f083400c20c0b0fd65095df30a0b445d960f3bbf271536ae6c3ba676f39cb7af0b4bf2755812f08ab9b88f2feed68f9aebb73bb153f7a115fe5c6e40 + languageName: node + linkType: hard + +"character-entities-legacy@npm:^3.0.0": + version: 3.0.0 + resolution: "character-entities-legacy@npm:3.0.0" + checksum: 10c0/ec4b430af873661aa754a896a2b55af089b4e938d3d010fad5219299a6b6d32ab175142699ee250640678cd64bdecd6db3c9af0b8759ab7b155d970d84c4c7d1 + languageName: node + linkType: hard + +"character-entities@npm:^2.0.0": + version: 2.0.2 + resolution: "character-entities@npm:2.0.2" + checksum: 10c0/b0c645a45bcc90ff24f0e0140f4875a8436b8ef13b6bcd31ec02cfb2ca502b680362aa95386f7815bdc04b6464d48cf191210b3840d7c04241a149ede591a308 + languageName: node + linkType: hard + +"character-reference-invalid@npm:^2.0.0": + version: 2.0.1 + resolution: "character-reference-invalid@npm:2.0.1" + checksum: 10c0/2ae0dec770cd8659d7e8b0ce24392d83b4c2f0eb4a3395c955dce5528edd4cc030a794cfa06600fcdd700b3f2de2f9b8e40e309c0011c4180e3be64a0b42e6a1 + languageName: node + linkType: hard + +"chart.js@npm:^4.5.1": + version: 4.5.1 + resolution: "chart.js@npm:4.5.1" + dependencies: + "@kurkle/color": "npm:^0.3.0" + checksum: 10c0/3f2a11dcaae9079e8e6b8ad077e2ae311f04996f9da14815730891e66215ee8b5f2c0eb70b5a156e5bde0f89a41bae13506dc6153e50fd22dcb282b21eec706f + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 + languageName: node + linkType: hard + +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"comma-separated-tokens@npm:^2.0.0": + version: 2.0.3 + resolution: "comma-separated-tokens@npm:2.0.3" + checksum: 10c0/91f90f1aae320f1755d6957ef0b864fe4f54737f3313bd95e0802686ee2ca38bff1dd381964d00ae5db42912dd1f4ae5c2709644e82706ffc6f6842a813cdd67 + languageName: node + linkType: hard + +"compute-scroll-into-view@npm:^3.0.2": + version: 3.1.1 + resolution: "compute-scroll-into-view@npm:3.1.1" + checksum: 10c0/59761ed62304a9599b52ad75d0d6fbf0669ee2ab7dd472fdb0ad9da36628414c014dea7b5810046560180ad30ffec52a953d19297f66a1d4f3aa0999b9d2521d + languageName: node + linkType: hard + +"convert-source-map@npm:^1.5.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: 10c0/281da55454bf8126cbc6625385928c43479f2060984180c42f3a86c8b8c12720a24eac260624a7d1e090004028d2dee78602330578ceec1a08e27cb8bb0a8a5b + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b + languageName: node + linkType: hard + +"cookie@npm:^1.0.1, cookie@npm:^1.1.1": + version: 1.1.1 + resolution: "cookie@npm:1.1.1" + checksum: 10c0/79c4ddc0fcad9c4f045f826f42edf54bcc921a29586a4558b0898277fa89fb47be95bc384c2253f493af7b29500c830da28341274527328f18eba9f58afa112c + languageName: node + linkType: hard + +"cosmiconfig@npm:^7.0.0": + version: 7.1.0 + resolution: "cosmiconfig@npm:7.1.0" + dependencies: + "@types/parse-json": "npm:^4.0.0" + import-fresh: "npm:^3.2.1" + parse-json: "npm:^5.0.0" + path-type: "npm:^4.0.0" + yaml: "npm:^1.10.0" + checksum: 10c0/b923ff6af581638128e5f074a5450ba12c0300b71302398ea38dbeabd33bbcaa0245ca9adbedfcf284a07da50f99ede5658c80bb3e39e2ce770a99d28a21ef03 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + +"css-tree@npm:^3.0.0, css-tree@npm:^3.2.1": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525 + languageName: node + linkType: hard + +"csstype@npm:^3.0.2, csstype@npm:^3.1.3, csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce + languageName: node + linkType: hard + +"data-urls@npm:^7.0.0": + version: 7.0.0 + resolution: "data-urls@npm:7.0.0" + dependencies: + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f + languageName: node + linkType: hard + +"date-fns@npm:^4.4.0": + version: 4.4.0 + resolution: "date-fns@npm:4.4.0" + checksum: 10c0/988f0a13db183f5dfc85c36bbb6847a9c135a9225888bbea4005876ec15539a8613c21a07370a4e7ea543918d5a1cafb423c528b42cdbbde5fdfddb178126b21 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.11": + version: 1.11.21 + resolution: "dayjs@npm:1.11.21" + checksum: 10c0/bd97dfdc4bfea3c66268635690313828b386faa040fbc1f829ff42a2bd748b72c9d9b3c8f9616ce9e61fcb78923f1461a462c969c54b1084458ae1b715898fb0 + languageName: node + linkType: hard + +"debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + +"decimal.js@npm:^10.6.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + +"decode-named-character-reference@npm:^1.0.0": + version: 1.3.0 + resolution: "decode-named-character-reference@npm:1.3.0" + dependencies: + character-entities: "npm:^2.0.0" + checksum: 10c0/787f4c87f3b82ea342aa7c2d7b1882b6fb9511bb77f72ae44dcaabea0470bacd1e9c6a0080ab886545019fa0cb3a7109573fad6b61a362844c3a0ac52b36e4bb + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: 10c0/7f0ee496e0dff14a573dc6127f14c95061b448b87b995fc96c017ce0a1e66af1675e73f1d6064407975bc4ea6ab679497a29fff7b5b9c4e99cb10797c1ad0b4c + languageName: node + linkType: hard + +"dequal@npm:^2.0.0, dequal@npm:^2.0.3": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888 + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.3": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + +"devlop@npm:^1.0.0, devlop@npm:^1.1.0": + version: 1.1.0 + resolution: "devlop@npm:1.1.0" + dependencies: + dequal: "npm:^2.0.0" + checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e + languageName: node + linkType: hard + +"dom-accessibility-api@npm:^0.5.9": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16" + checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053 + languageName: node + linkType: hard + +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: 10c0/10bee5aa514b2a9a37c87cd81268db607a2e933a050074abc2f6fa3da9080ebed206a320cbc123567f2c3087d22292853bdfdceaffdd4334ffe2af9510b29360 + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.5.393": + version: 1.5.398 + resolution: "electron-to-chromium@npm:1.5.398" + checksum: 10c0/5c489a3f8255a2146c2e30112365b091998439c620e8d654d8788954e16c4cd7ffa9977dfb268d7b043ef14818fcf6bb470a7666fe44ee011d2b18c42ab89939 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"entities@npm:^8.0.0": + version: 8.0.0 + resolution: "entities@npm:8.0.0" + checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"error-ex@npm:^1.3.1": + version: 1.3.4 + resolution: "error-ex@npm:1.3.4" + dependencies: + is-arrayish: "npm:^0.2.1" + checksum: 10c0/b9e34ff4778b8f3b31a8377e1c654456f4c41aeaa3d10a1138c3b7635d8b7b2e03eb2475d46d8ae055c1f180a1063e100bffabf64ea7e7388b37735df5328664 + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + +"es-module-lexer@npm:^2.0.0": + version: 2.3.1 + resolution: "es-module-lexer@npm:2.3.1" + checksum: 10c0/ada8b222772b5b8ea92eb6054c383233207418621855a07b480fdd36979b658a41414be09e793fcdd8a67a182741475f47830a01ff2ebd4353d7f6965c7c45f9 + languageName: node + linkType: hard + +"escalade@npm:^3.1.1, escalade@npm:^3.2.0": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10c0/9497d4dd307d845bd7f75180d8188bb17ea8c151c1edbf6b6717c100e104d629dc2dfb687686181b0f4b7d732c7dfdc4d5e7a8ff72de1b0ca283a75bbb3a9cd9 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95 + languageName: node + linkType: hard + +"eslint-plugin-react-hooks@npm:^7.1.1": + version: 7.1.1 + resolution: "eslint-plugin-react-hooks@npm:7.1.1" + dependencies: + "@babel/core": "npm:^7.24.4" + "@babel/parser": "npm:^7.24.4" + hermes-parser: "npm:^0.25.1" + zod: "npm:^3.25.0 || ^4.0.0" + zod-validation-error: "npm:^3.5.0 || ^4.0.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/cee8454915d71ac5d70a0d8f4f260e76eaf45fcd4162747dd4282b792ee5616d187351dabe6cdcff9040c79d0cec625635c4fd0777276be119efa88ebe058525 + languageName: node + linkType: hard + +"eslint-plugin-react-refresh@npm:^0.5.3": + version: 0.5.3 + resolution: "eslint-plugin-react-refresh@npm:0.5.3" + peerDependencies: + eslint: ^9 || ^10 + checksum: 10c0/b2cda4fe8708f1401a0aaf3ab31fa6c93c283c43fdfa039ec8d27061330848ca3c5148a97b2a249ed228d8072fd971cf83694f555a9f6ec88b3802f22470ab22 + languageName: node + linkType: hard + +"eslint-scope@npm:^9.1.2": + version: 9.1.2 + resolution: "eslint-scope@npm:9.1.2" + dependencies: + "@types/esrecurse": "npm:^4.3.1" + "@types/estree": "npm:^1.0.8" + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10c0/9fb8bca5a73e5741efb6cec84467027b6cb6f4203ff9b43a938e272c5cd30800bde46a5c20dfd1609f840225f0b62b7673be391b20acadf8658ca9fa4729b3dd + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 + languageName: node + linkType: hard + +"eslint@npm:^10.8.0": + version: 10.8.0 + resolution: "eslint@npm:10.8.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.8.0" + "@eslint-community/regexpp": "npm:^4.12.2" + "@eslint/config-array": "npm:^0.23.5" + "@eslint/config-helpers": "npm:^0.7.0" + "@eslint/core": "npm:^1.2.1" + "@eslint/plugin-kit": "npm:^0.7.2" + "@humanfs/node": "npm:^0.16.6" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.4.2" + "@types/estree": "npm:^1.0.6" + ajv: "npm:^6.14.0" + cross-spawn: "npm:^7.0.6" + debug: "npm:^4.3.2" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^9.1.2" + eslint-visitor-keys: "npm:^5.0.1" + espree: "npm:^11.2.0" + esquery: "npm:^1.7.0" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^8.0.0" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + minimatch: "npm:^10.2.5" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: 10c0/2dbc523578834088615f388e08418d2620b25655f7a398f6d415765fa2a3a25d6b8b43bd3293d40f977e12752323a841d52654a2648697a00c0102c9794bb3dc + languageName: node + linkType: hard + +"espree@npm:^11.2.0": + version: 11.2.0 + resolution: "espree@npm:11.2.0" + dependencies: + acorn: "npm:^8.16.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^5.0.1" + checksum: 10c0/cf87e18ffd9dc113eb8d16588e7757701bc10c9934a71cce8b89c2611d51672681a918307bd6b19ac3ccd0e7ba1cbccc2f815b36b52fa7e73097b251014c3d81 + languageName: node + linkType: hard + +"esquery@npm:^1.7.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: "npm:^5.2.0" + checksum: 10c0/81a37116d1408ded88ada45b9fb16dbd26fba3aadc369ce50fcaf82a0bac12772ebd7b24cd7b91fc66786bf2c1ac7b5f196bc990a473efff972f5cb338877cf5 + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 + languageName: node + linkType: hard + +"estree-util-is-identifier-name@npm:^3.0.0": + version: 3.0.0 + resolution: "estree-util-is-identifier-name@npm:3.0.0" + checksum: 10c0/d1881c6ed14bd588ebd508fc90bf2a541811dbb9ca04dec2f39d27dcaa635f85b5ed9bbbe7fc6fb1ddfca68744a5f7c70456b4b7108b6c4c52780631cc787c5b + languageName: node + linkType: hard + +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 10c0/9a2fe69a41bfdade834ba7c42de4723c97ec776e40656919c62cbd13607c45e127a003f05f724a1ea55e5029a4cf2de444b13009f2af71271e42d93a637137c7 + languageName: node + linkType: hard + +"expect-type@npm:^1.3.0": + version: 1.4.0 + resolution: "expect-type@npm:1.4.0" + checksum: 10c0/d40d76b8570695d36587beb3cc28494da2ca3ec8f04e67f5622ed2d372d850e401a9adef19c6835e1a8173903f157c79540b34c7b3fbd7cd8ce726cc903c57b7 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 + languageName: node + linkType: hard + +"extend@npm:^3.0.0": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: 10c0/73bf6e27406e80aa3e85b0d1c4fd987261e628064e170ca781125c0b635a3dabad5e05adbf07595ea0cf1e6c5396cacb214af933da7cbaf24fe75ff14818e8f9 + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 10c0/111972b37338bcb88f7d9e2c5907862c280ebf4234433b95bc611e518d192ccb2d38119c4ac86e26b668d75f7f3894f4ff5c4982899afced7ca78633b08287c4 + languageName: node + linkType: hard + +"fast-string-truncated-width@npm:^3.0.2": + version: 3.0.3 + resolution: "fast-string-truncated-width@npm:3.0.3" + checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981 + languageName: node + linkType: hard + +"fast-string-width@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-string-width@npm:3.0.2" + dependencies: + fast-string-truncated-width: "npm:^3.0.2" + checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7 + languageName: node + linkType: hard + +"fast-wrap-ansi@npm:^0.2.0": + version: 0.2.2 + resolution: "fast-wrap-ansi@npm:0.2.2" + dependencies: + fast-string-width: "npm:^3.0.2" + checksum: 10c0/1aa7be4f7cb86f4bdb14691cb6bcc0b8df8b3b89df142ade3ae1602332dcf6f990cd750a923cd581ca0847808cb4ec1aa5afaafa7a72f849e87a2a62c98fa370 + languageName: node + linkType: hard + +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: "npm:^4.0.0" + checksum: 10c0/9e2b5938b1cd9b6d7e3612bdc533afd4ac17b2fc646569e9a8abbf2eb48e5eb8e316bc38815a3ef6a1b456f4107f0d0f055a614ca613e75db6bf9ff4d72c1638 + languageName: node + linkType: hard + +"find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "find-root@npm:1.1.0" + checksum: 10c0/1abc7f3bf2f8d78ff26d9e00ce9d0f7b32e5ff6d1da2857bcdf4746134c422282b091c672cde0572cac3840713487e0a7a636af9aa1b74cb11894b447a521efa + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a + languageName: node + linkType: hard + +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + checksum: 10c0/2c59d93e9faa2523e4fda6b4ada749bed432cfa28c8e251f33b25795e426a1c6dbada777afb1f74fcfff33934fdbdea921ee738fcc33e71adc9d6eca984a1cfc + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.4.4 + resolution: "flatted@npm:3.4.4" + checksum: 10c0/a3a52a88ea5a4c333e5a1f097dcd87a037fa31c236a77cf46222c2aa4036ef895ab9bc76138a66d9dc22bdb3652128f9598cd26e7439561bf19f67276e2bc37e + languageName: node + linkType: hard + +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + languageName: node + linkType: hard + +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: 10c0/782aba6cba65b1bb5af3b095d96249d20edbe8df32dbf4696fd49be2583faf676173bf4809386588828e4dd76a3354fcbeb577bab1c833ccd9fc4577f26103f8 + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: "npm:^4.0.3" + checksum: 10c0/317034d88654730230b3f43bb7ad4f7c90257a426e872ea0bf157473ac61c99bf5d205fad8f0185f989be8d2fa6d3c7dce1645d99d545b6ea9089c39f838e7f8 + languageName: node + linkType: hard + +"globals@npm:^17.8.0": + version: 17.8.0 + resolution: "globals@npm:17.8.0" + checksum: 10c0/e31b36cef3befd446487674dc6ca7b8f8e62a3b76aadd975dbf80ffa732366b3ac58df8941999eb6b1406d97480103711db85d2a49c955da47ff5ec688bee9b5 + languageName: node + linkType: hard + +"goober@npm:^2.1.16": + version: 2.1.19 + resolution: "goober@npm:2.1.19" + peerDependencies: + csstype: ^3.0.10 + checksum: 10c0/f6a01f2e8abfef571ac539f96e650dcde964e7c0bef6c9055a8c8e110624ccd7b51ab90706a806ca570a56141ea689d355fb1cec3ee9209499c249638883990d + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"graphql@npm:^16.13.2": + version: 16.14.2 + resolution: "graphql@npm:16.14.2" + checksum: 10c0/a95a96961eaff55cc9fe9d31fae6f33499ac988b972d07ea5085024cb1333f515b902f376e7393a5489aa82200a8aff3eb96580e4d1b69d702ed19b6eb1ce97a + languageName: node + linkType: hard + +"hasown@npm:^2.0.3": + version: 2.0.4 + resolution: "hasown@npm:2.0.4" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506 + languageName: node + linkType: hard + +"hast-util-to-jsx-runtime@npm:^2.0.0": + version: 2.3.6 + resolution: "hast-util-to-jsx-runtime@npm:2.3.6" + dependencies: + "@types/estree": "npm:^1.0.0" + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + comma-separated-tokens: "npm:^2.0.0" + devlop: "npm:^1.0.0" + estree-util-is-identifier-name: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + mdast-util-mdx-expression: "npm:^2.0.0" + mdast-util-mdx-jsx: "npm:^3.0.0" + mdast-util-mdxjs-esm: "npm:^2.0.0" + property-information: "npm:^7.0.0" + space-separated-tokens: "npm:^2.0.0" + style-to-js: "npm:^1.0.0" + unist-util-position: "npm:^5.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10c0/27297e02848fe37ef219be04a26ce708d17278a175a807689e94a821dcffc88aa506d62c3a85beed1f9a8544f7211bdcbcde0528b7b456a57c2e342c3fd11056 + languageName: node + linkType: hard + +"hast-util-whitespace@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-whitespace@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/b898bc9fe27884b272580d15260b6bbdabe239973a147e97fa98c45fa0ffec967a481aaa42291ec34fb56530dc2d484d473d7e2bae79f39c83f3762307edfea8 + languageName: node + linkType: hard + +"headers-polyfill@npm:^5.0.1": + version: 5.0.1 + resolution: "headers-polyfill@npm:5.0.1" + dependencies: + "@types/set-cookie-parser": "npm:^2.4.10" + set-cookie-parser: "npm:^3.0.1" + checksum: 10c0/c269730a88a12c88718037aa71f178601f2b193ba8a37e276b6ced6b8f7e06fc1ac051f2a7acb0a8b4cc878407066555fdcdbb270e90374baaa472cb26af0c30 + languageName: node + linkType: hard + +"hermes-estree@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-estree@npm:0.25.1" + checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac + languageName: node + linkType: hard + +"hermes-parser@npm:^0.25.1": + version: 0.25.1 + resolution: "hermes-parser@npm:0.25.1" + dependencies: + hermes-estree: "npm:0.25.1" + checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c + languageName: node + linkType: hard + +"hoist-non-react-statics@npm:^3.3.1": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: "npm:^16.7.0" + checksum: 10c0/fe0889169e845d738b59b64badf5e55fa3cf20454f9203d1eb088df322d49d4318df774828e789898dcb280e8a5521bb59b3203385662ca5e9218a6ca5820e74 + languageName: node + linkType: hard + +"html-encoding-sniffer@npm:^6.0.0": + version: 6.0.0 + resolution: "html-encoding-sniffer@npm:6.0.0" + dependencies: + "@exodus/bytes": "npm:^1.6.0" + checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025 + languageName: node + linkType: hard + +"html-url-attributes@npm:^3.0.0": + version: 3.0.1 + resolution: "html-url-attributes@npm:3.0.1" + checksum: 10c0/496e4908aa8b77665f348b4b03521901794f648b8ac34a581022cd6f2c97934d5c910cd91bc6593bbf2994687549037bc2520fcdc769b31484f29ffdd402acd0 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 + languageName: node + linkType: hard + +"ignore@npm:^7.0.5": + version: 7.0.6 + resolution: "ignore@npm:7.0.6" + checksum: 10c0/fc01ef1d14efbe003439b60538726351e81483d1b6f55bdbb3a4465c6346d9481afad5b350dfbd604ddd7049618ef9093ff26dc147984aabc303c56ba53ea3b5 + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.1 + resolution: "import-fresh@npm:3.3.1" + dependencies: + parent-module: "npm:^1.0.0" + resolve-from: "npm:^4.0.0" + checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"inline-style-parser@npm:0.2.7": + version: 0.2.7 + resolution: "inline-style-parser@npm:0.2.7" + checksum: 10c0/d884d76f84959517430ae6c22f0bda59bb3f58f539f99aac75a8d786199ec594ed648c6ab4640531f9fc244b0ed5cd8c458078e592d016ef06de793beb1debff + languageName: node + linkType: hard + +"is-alphabetical@npm:^2.0.0": + version: 2.0.1 + resolution: "is-alphabetical@npm:2.0.1" + checksum: 10c0/932367456f17237533fd1fc9fe179df77957271020b83ea31da50e5cc472d35ef6b5fb8147453274ffd251134472ce24eb6f8d8398d96dee98237cdb81a6c9a7 + languageName: node + linkType: hard + +"is-alphanumerical@npm:^2.0.0": + version: 2.0.1 + resolution: "is-alphanumerical@npm:2.0.1" + dependencies: + is-alphabetical: "npm:^2.0.0" + is-decimal: "npm:^2.0.0" + checksum: 10c0/4b35c42b18e40d41378293f82a3ecd9de77049b476f748db5697c297f686e1e05b072a6aaae2d16f54d2a57f85b00cbbe755c75f6d583d1c77d6657bd0feb5a2 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 + languageName: node + linkType: hard + +"is-core-module@npm:^2.16.1": + version: 2.16.2 + resolution: "is-core-module@npm:2.16.2" + dependencies: + hasown: "npm:^2.0.3" + checksum: 10c0/14b4258390283709c15476d023ec173e27458d5d014ccdb8ed39d576e551c3fa45498b7c9fe178f1529c4cb2648ddd58852a6a62107a019f6e349529f277518a + languageName: node + linkType: hard + +"is-decimal@npm:^2.0.0": + version: 2.0.1 + resolution: "is-decimal@npm:2.0.1" + checksum: 10c0/8085dd66f7d82f9de818fba48b9e9c0429cb4291824e6c5f2622e96b9680b54a07a624cfc663b24148b8e853c62a1c987cfe8b0b5a13f5156991afaf6736e334 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.3": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-hexadecimal@npm:^2.0.0": + version: 2.0.1 + resolution: "is-hexadecimal@npm:2.0.1" + checksum: 10c0/3eb60fe2f1e2bbc760b927dcad4d51eaa0c60138cf7fc671803f66353ad90c301605b502c7ea4c6bb0548e1c7e79dfd37b73b632652e3b76030bba603a7e9626 + languageName: node + linkType: hard + +"is-mobile@npm:^5.0.0": + version: 5.0.0 + resolution: "is-mobile@npm:5.0.0" + checksum: 10c0/70b31c3e4489109e02deb9b590e74858aeec7ef775a882d89ec030cb7dbaeb2be1173d7faee495049629e49822d0aba9b20e6653b1e25dd7c9121247cb8829d7 + languageName: node + linkType: hard + +"is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 10c0/5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed + languageName: node + linkType: hard + +"is-plain-obj@npm:^4.0.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 10c0/32130d651d71d9564dc88ba7e6fda0e91a1010a3694648e9f4f47bb6080438140696d3e3e15c741411d712e47ac9edc1a8a9de1fe76f3487b0d90be06ac9975e + languageName: node + linkType: hard + +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce + languageName: node + linkType: hard + +"js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed + languageName: node + linkType: hard + +"jsdom@npm:^30.0.1": + version: 30.0.1 + resolution: "jsdom@npm:30.0.1" + dependencies: + "@asamuzakjp/css-color": "npm:^6.0.5" + "@asamuzakjp/dom-selector": "npm:^8.3.0" + "@bramus/specificity": "npm:^2.4.2" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.7" + "@exodus/bytes": "npm:^1.15.1" + css-tree: "npm:^3.2.1" + data-urls: "npm:^7.0.0" + decimal.js: "npm:^10.6.0" + html-encoding-sniffer: "npm:^6.0.0" + is-potential-custom-element-name: "npm:^1.0.1" + lru-cache: "npm:^11.5.2" + parse5: "npm:^8.0.1" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^6.0.2" + undici: "npm:^8.9.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^8.0.1" + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^17.1.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/2e3c04c30da46f373259b33ffb1a7786d351370aada3bfa3d7766dc97956659727dc78f49e01813b313e873d32851c842dc319b89d099f9e9decb55339dca155 + languageName: node + linkType: hard + +"jsesc@npm:^3.0.2": + version: 3.1.0 + resolution: "jsesc@npm:3.1.0" + bin: + jsesc: bin/jsesc + checksum: 10c0/531779df5ec94f47e462da26b4cbf05eb88a83d9f08aac2ba04206508fc598527a153d08bd462bae82fc78b3eaa1a908e1a4a79f886e9238641c4cdefaf118b1 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 + languageName: node + linkType: hard + +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 10c0/108fa90d4cc6f08243aedc6da16c408daf81793bf903e9fd5ab21983cda433d5d2da49e40711da016289465ec2e62e0324dcdfbc06275a607fe3233fde4942ce + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: 10c0/cb168b61fd4de83e58d09aaa6425ef71001bae30d260e2c57e7d09a5fd82223e2f22a042dedaab8db23b7d9ae46854b08bb1f91675a8be11c5cffebef5fb66a5 + languageName: node + linkType: hard + +"json2mq@npm:^0.2.0": + version: 0.2.0 + resolution: "json2mq@npm:0.2.0" + dependencies: + string-convert: "npm:^0.2.0" + checksum: 10c0/fc9e2f2306572522d3e61d246afdf70b56ca9ea32f4ad5924c30949867851ab59c926bd0ffc821ebb54d32f3e82e95225f3906eacdb3e54c1ad49acdadf7e0c7 + languageName: node + linkType: hard + +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c + languageName: node + linkType: hard + +"kagent-ui@workspace:.": + version: 0.0.0-use.local + resolution: "kagent-ui@workspace:." + dependencies: + "@bufbuild/protobuf": "npm:2.13.0" + "@connectrpc/connect": "npm:2.1.2" + "@connectrpc/connect-web": "npm:2.1.2" + "@emotion/react": "npm:^11.14.0" + "@eslint/js": "npm:^10.0.1" + "@playwright/test": "npm:^1.62.1" + "@testing-library/dom": "npm:^10.4.1" + "@testing-library/jest-dom": "npm:^7.0.0" + "@testing-library/react": "npm:^16.3.2" + "@testing-library/user-event": "npm:^14.6.1" + "@types/node": "npm:^26.1.2" + "@types/react": "npm:^19.2.17" + "@types/react-dom": "npm:^19.2.3" + "@vitejs/plugin-react": "npm:^6.0.5" + antd: "npm:^6.5.2" + chart.js: "npm:^4.5.1" + date-fns: "npm:^4.4.0" + eslint: "npm:^10.8.0" + eslint-plugin-react-hooks: "npm:^7.1.1" + eslint-plugin-react-refresh: "npm:^0.5.3" + globals: "npm:^17.8.0" + jsdom: "npm:^30.0.1" + lucide-react: "npm:^1.28.0" + msw: "npm:^2.15.0" + react: "npm:^19.2.8" + react-chartjs-2: "npm:^5.3.1" + react-dom: "npm:^19.2.8" + react-hot-toast: "npm:^2.6.0" + react-markdown: "npm:^10.1.0" + react-router-dom: "npm:^7.18.2" + remark-breaks: "npm:^4.0.0" + remark-gfm: "npm:^4.0.1" + swr: "npm:^2.4.2" + typescript: "npm:^6.0.3" + typescript-eslint: "npm:^8.65.0" + vite: "npm:^8.2.0" + vitest: "npm:^4.1.10" + languageName: unknown + linkType: soft + +"keyv@npm:^4.5.4": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: "npm:^1.2.1" + type-check: "npm:~0.4.0" + checksum: 10c0/effb03cad7c89dfa5bd4f6989364bfc79994c2042ec5966cb9b95990e2edee5cd8969ddf42616a0373ac49fac1403437deaf6e9050fbbaa3546093a59b9ac94e + languageName: node + linkType: hard + +"lightningcss-android-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-android-arm64@npm:1.33.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-arm64@npm:1.33.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-x64@npm:1.33.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-freebsd-x64@npm:1.33.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.33.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.33.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.33.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.33.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-musl@npm:1.33.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-arm64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.33.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-win32-x64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.33.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:^1.33.0": + version: 1.33.0 + resolution: "lightningcss@npm:1.33.0" + dependencies: + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.33.0" + lightningcss-darwin-arm64: "npm:1.33.0" + lightningcss-darwin-x64: "npm:1.33.0" + lightningcss-freebsd-x64: "npm:1.33.0" + lightningcss-linux-arm-gnueabihf: "npm:1.33.0" + lightningcss-linux-arm64-gnu: "npm:1.33.0" + lightningcss-linux-arm64-musl: "npm:1.33.0" + lightningcss-linux-x64-gnu: "npm:1.33.0" + lightningcss-linux-x64-musl: "npm:1.33.0" + lightningcss-win32-arm64-msvc: "npm:1.33.0" + lightningcss-win32-x64-msvc: "npm:1.33.0" + dependenciesMeta: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10c0/ce1f8279fbae636dbf37fa6e7385d5f98ed881d72af3362f24afbd4685e19c1fcdfecf17e5dd77f2ebee3d0c23ade276230d85842d07292229a2cffba8ff20a3 + languageName: node + linkType: hard + +"lines-and-columns@npm:^1.1.6": + version: 1.2.4 + resolution: "lines-and-columns@npm:1.2.4" + checksum: 10c0/3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 + languageName: node + linkType: hard + +"longest-streak@npm:^3.0.0": + version: 3.1.0 + resolution: "longest-streak@npm:3.1.0" + checksum: 10c0/7c2f02d0454b52834d1bcedef79c557bd295ee71fdabb02d041ff3aa9da48a90b5df7c0409156dedbc4df9b65da18742652aaea4759d6ece01f08971af6a7eaa + languageName: node + linkType: hard + +"lru-cache@npm:^11.5.2": + version: 11.5.2 + resolution: "lru-cache@npm:11.5.2" + checksum: 10c0/ece1ad731f5b655e85d67047d04bfc13823dc77aa61c5454924a9869ba600a0104e39cc33d726021feef812bc347ca34a5608a5eb1972a5dd0870b7ecd42c3f2 + languageName: node + linkType: hard + +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: "npm:^3.0.2" + checksum: 10c0/89b2ef2ef45f543011e38737b8a8622a2f8998cddf0e5437174ef8f1f70a8b9d14a918ab3e232cb3ba343b7abddffa667f0b59075b2b80e6b4d63c3de6127482 + languageName: node + linkType: hard + +"lucide-react@npm:^1.28.0": + version: 1.28.0 + resolution: "lucide-react@npm:1.28.0" + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/7a18911a68d6227865ca03120e3423665478131648ffbd47d62b242ef25a5982c6312ca7041b5a4d60cf0d8ea66d0f97f50dbe848ae60ab0143471da41ef057e + languageName: node + linkType: hard + +"lz-string@npm:^1.5.0": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b + languageName: node + linkType: hard + +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + +"markdown-table@npm:^3.0.0": + version: 3.0.4 + resolution: "markdown-table@npm:3.0.4" + checksum: 10c0/1257b31827629a54c24a5030a3dac952256c559174c95ce3ef89bebd6bff0cb1444b1fd667b1a1bb53307f83278111505b3e26f0c4e7b731e0060d435d2d930b + languageName: node + linkType: hard + +"mdast-util-find-and-replace@npm:^3.0.0": + version: 3.0.2 + resolution: "mdast-util-find-and-replace@npm:3.0.2" + dependencies: + "@types/mdast": "npm:^4.0.0" + escape-string-regexp: "npm:^5.0.0" + unist-util-is: "npm:^6.0.0" + unist-util-visit-parents: "npm:^6.0.0" + checksum: 10c0/c8417a35605d567772ff5c1aa08363ff3010b0d60c8ea68c53cba09bf25492e3dd261560425c1756535f3b7107f62e7ff3857cdd8fb1e62d1b2cc2ea6e074ca2 + languageName: node + linkType: hard + +"mdast-util-from-markdown@npm:^2.0.0": + version: 2.0.3 + resolution: "mdast-util-from-markdown@npm:2.0.3" + dependencies: + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + decode-named-character-reference: "npm:^1.0.0" + devlop: "npm:^1.0.0" + mdast-util-to-string: "npm:^4.0.0" + micromark: "npm:^4.0.0" + micromark-util-decode-numeric-character-reference: "npm:^2.0.0" + micromark-util-decode-string: "npm:^2.0.0" + micromark-util-normalize-identifier: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + unist-util-stringify-position: "npm:^4.0.0" + checksum: 10c0/d3eac9ac2b88e3b41fb85aa81c7bfd1f4f8a2fde497ad805e66fea7b2abfe486ffd94d2a20f9fd2951dcdebe4916f3bdcf851319891dd62d343e26c2f02583ba + languageName: node + linkType: hard + +"mdast-util-gfm-autolink-literal@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-gfm-autolink-literal@npm:2.0.1" + dependencies: + "@types/mdast": "npm:^4.0.0" + ccount: "npm:^2.0.0" + devlop: "npm:^1.0.0" + mdast-util-find-and-replace: "npm:^3.0.0" + micromark-util-character: "npm:^2.0.0" + checksum: 10c0/963cd22bd42aebdec7bdd0a527c9494d024d1ad0739c43dc040fee35bdfb5e29c22564330a7418a72b5eab51d47a6eff32bc0255ef3ccb5cebfe8970e91b81b6 + languageName: node + linkType: hard + +"mdast-util-gfm-footnote@npm:^2.0.0": + version: 2.1.0 + resolution: "mdast-util-gfm-footnote@npm:2.1.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.1.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + micromark-util-normalize-identifier: "npm:^2.0.0" + checksum: 10c0/8ab965ee6be3670d76ec0e95b2ba3101fc7444eec47564943ab483d96ac17d29da2a4e6146a2a288be30c21b48c4f3938a1e54b9a46fbdd321d49a5bc0077ed0 + languageName: node + linkType: hard + +"mdast-util-gfm-strikethrough@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-strikethrough@npm:2.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + checksum: 10c0/b053e93d62c7545019bd914271ea9e5667ad3b3b57d16dbf68e56fea39a7e19b4a345e781312714eb3d43fdd069ff7ee22a3ca7f6149dfa774554f19ce3ac056 + languageName: node + linkType: hard + +"mdast-util-gfm-table@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-table@npm:2.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + markdown-table: "npm:^3.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + checksum: 10c0/128af47c503a53bd1c79f20642561e54a510ad5e2db1e418d28fefaf1294ab839e6c838e341aef5d7e404f9170b9ca3d1d89605f234efafde93ee51174a6e31e + languageName: node + linkType: hard + +"mdast-util-gfm-task-list-item@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-gfm-task-list-item@npm:2.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + checksum: 10c0/258d725288482b636c0a376c296431390c14b4f29588675297cb6580a8598ed311fc73ebc312acfca12cc8546f07a3a285a53a3b082712e2cbf5c190d677d834 + languageName: node + linkType: hard + +"mdast-util-gfm@npm:^3.0.0": + version: 3.1.0 + resolution: "mdast-util-gfm@npm:3.1.0" + dependencies: + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-gfm-autolink-literal: "npm:^2.0.0" + mdast-util-gfm-footnote: "npm:^2.0.0" + mdast-util-gfm-strikethrough: "npm:^2.0.0" + mdast-util-gfm-table: "npm:^2.0.0" + mdast-util-gfm-task-list-item: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + checksum: 10c0/4bedcfb6a20e39901c8772f0d2bb2d7a64ae87a54c13cbd92eec062cf470fbb68c2ad754e149af5b30794e2de61c978ab1de1ace03c0c40f443ca9b9b8044f81 + languageName: node + linkType: hard + +"mdast-util-mdx-expression@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-mdx-expression@npm:2.0.1" + dependencies: + "@types/estree-jsx": "npm:^1.0.0" + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + checksum: 10c0/9a1e57940f66431f10312fa239096efa7627f375e7933b5d3162c0b5c1712a72ac87447aff2b6838d2bbd5c1311b188718cc90b33b67dc67a88550e0a6ef6183 + languageName: node + linkType: hard + +"mdast-util-mdx-jsx@npm:^3.0.0": + version: 3.2.0 + resolution: "mdast-util-mdx-jsx@npm:3.2.0" + dependencies: + "@types/estree-jsx": "npm:^1.0.0" + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + ccount: "npm:^2.0.0" + devlop: "npm:^1.1.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + parse-entities: "npm:^4.0.0" + stringify-entities: "npm:^4.0.0" + unist-util-stringify-position: "npm:^4.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10c0/3acadaf3b962254f7ad2990fed4729961dc0217ca31fde9917986e880843f3ecf3392b1f22d569235cacd180d50894ad266db7af598aedca69d330d33c7ac613 + languageName: node + linkType: hard + +"mdast-util-mdxjs-esm@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-mdxjs-esm@npm:2.0.1" + dependencies: + "@types/estree-jsx": "npm:^1.0.0" + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + checksum: 10c0/5bda92fc154141705af2b804a534d891f28dac6273186edf1a4c5e3f045d5b01dbcac7400d27aaf91b7e76e8dce007c7b2fdf136c11ea78206ad00bdf9db46bc + languageName: node + linkType: hard + +"mdast-util-newline-to-break@npm:^2.0.0": + version: 2.0.0 + resolution: "mdast-util-newline-to-break@npm:2.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-find-and-replace: "npm:^3.0.0" + checksum: 10c0/756a5660b0a821e0d6d6a0b2d9b13ac32e41cc028c485a91bccf6300977e2557236c6cc93dbd55c68b785f1ed6eae69209a4ffe182533cd1cdfda369021bebd2 + languageName: node + linkType: hard + +"mdast-util-phrasing@npm:^4.0.0": + version: 4.1.0 + resolution: "mdast-util-phrasing@npm:4.1.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/bf6c31d51349aa3d74603d5e5a312f59f3f65662ed16c58017169a5fb0f84ca98578f626c5ee9e4aa3e0a81c996db8717096705521bddb4a0185f98c12c9b42f + languageName: node + linkType: hard + +"mdast-util-to-hast@npm:^13.0.0": + version: 13.2.1 + resolution: "mdast-util-to-hast@npm:13.2.1" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + "@ungap/structured-clone": "npm:^1.0.0" + devlop: "npm:^1.0.0" + micromark-util-sanitize-uri: "npm:^2.0.0" + trim-lines: "npm:^3.0.0" + unist-util-position: "npm:^5.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/3eeaf28a5e84e1e08e6d54a1a8a06c0fca88cb5d36f4cf8086f0177248d1ce6e4e751f4ad0da19a3dea1c6ea61bd80784acc3ae021e44ceeb21aa5413a375e43 + languageName: node + linkType: hard + +"mdast-util-to-markdown@npm:^2.0.0": + version: 2.1.2 + resolution: "mdast-util-to-markdown@npm:2.1.2" + dependencies: + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + longest-streak: "npm:^3.0.0" + mdast-util-phrasing: "npm:^4.0.0" + mdast-util-to-string: "npm:^4.0.0" + micromark-util-classify-character: "npm:^2.0.0" + micromark-util-decode-string: "npm:^2.0.0" + unist-util-visit: "npm:^5.0.0" + zwitch: "npm:^2.0.0" + checksum: 10c0/4649722a6099f12e797bd8d6469b2b43b44e526b5182862d9c7766a3431caad2c0112929c538a972f214e63c015395e5d3f54bd81d9ac1b16e6d8baaf582f749 + languageName: node + linkType: hard + +"mdast-util-to-string@npm:^4.0.0": + version: 4.0.0 + resolution: "mdast-util-to-string@npm:4.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + checksum: 10c0/2d3c1af29bf3fe9c20f552ee9685af308002488f3b04b12fa66652c9718f66f41a32f8362aa2d770c3ff464c034860b41715902ada2306bb0a055146cef064d7 + languageName: node + linkType: hard + +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + +"micromark-core-commonmark@npm:^2.0.0": + version: 2.0.3 + resolution: "micromark-core-commonmark@npm:2.0.3" + dependencies: + decode-named-character-reference: "npm:^1.0.0" + devlop: "npm:^1.0.0" + micromark-factory-destination: "npm:^2.0.0" + micromark-factory-label: "npm:^2.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-factory-title: "npm:^2.0.0" + micromark-factory-whitespace: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-chunked: "npm:^2.0.0" + micromark-util-classify-character: "npm:^2.0.0" + micromark-util-html-tag-name: "npm:^2.0.0" + micromark-util-normalize-identifier: "npm:^2.0.0" + micromark-util-resolve-all: "npm:^2.0.0" + micromark-util-subtokenize: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/bd4a794fdc9e88dbdf59eaf1c507ddf26e5f7ddf4e52566c72239c0f1b66adbcd219ba2cd42350debbe24471434d5f5e50099d2b3f4e5762ca222ba8e5b549ee + languageName: node + linkType: hard + +"micromark-extension-gfm-autolink-literal@npm:^2.0.0": + version: 2.1.0 + resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0" + dependencies: + micromark-util-character: "npm:^2.0.0" + micromark-util-sanitize-uri: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/84e6fbb84ea7c161dfa179665dc90d51116de4c28f3e958260c0423e5a745372b7dcbc87d3cde98213b532e6812f847eef5ae561c9397d7f7da1e59872ef3efe + languageName: node + linkType: hard + +"micromark-extension-gfm-footnote@npm:^2.0.0": + version: 2.1.0 + resolution: "micromark-extension-gfm-footnote@npm:2.1.0" + dependencies: + devlop: "npm:^1.0.0" + micromark-core-commonmark: "npm:^2.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-normalize-identifier: "npm:^2.0.0" + micromark-util-sanitize-uri: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/d172e4218968b7371b9321af5cde8c77423f73b233b2b0fcf3ff6fd6f61d2e0d52c49123a9b7910612478bf1f0d5e88c75a3990dd68f70f3933fe812b9f77edc + languageName: node + linkType: hard + +"micromark-extension-gfm-strikethrough@npm:^2.0.0": + version: 2.1.0 + resolution: "micromark-extension-gfm-strikethrough@npm:2.1.0" + dependencies: + devlop: "npm:^1.0.0" + micromark-util-chunked: "npm:^2.0.0" + micromark-util-classify-character: "npm:^2.0.0" + micromark-util-resolve-all: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/ef4f248b865bdda71303b494671b7487808a340b25552b11ca6814dff3fcfaab9be8d294643060bbdb50f79313e4a686ab18b99cbe4d3ee8a4170fcd134234fb + languageName: node + linkType: hard + +"micromark-extension-gfm-table@npm:^2.0.0": + version: 2.1.1 + resolution: "micromark-extension-gfm-table@npm:2.1.1" + dependencies: + devlop: "npm:^1.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/04bc00e19b435fa0add62cd029d8b7eb6137522f77832186b1d5ef34544a9bd030c9cf85e92ddfcc5c31f6f0a58a43d4b96dba4fc21316037c734630ee12c912 + languageName: node + linkType: hard + +"micromark-extension-gfm-tagfilter@npm:^2.0.0": + version: 2.0.0 + resolution: "micromark-extension-gfm-tagfilter@npm:2.0.0" + dependencies: + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/995558843fff137ae4e46aecb878d8a4691cdf23527dcf1e2f0157d66786be9f7bea0109c52a8ef70e68e3f930af811828ba912239438e31a9cfb9981f44d34d + languageName: node + linkType: hard + +"micromark-extension-gfm-task-list-item@npm:^2.0.0": + version: 2.1.0 + resolution: "micromark-extension-gfm-task-list-item@npm:2.1.0" + dependencies: + devlop: "npm:^1.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/78aa537d929e9309f076ba41e5edc99f78d6decd754b6734519ccbbfca8abd52e1c62df68d41a6ae64d2a3fc1646cea955893c79680b0b4385ced4c52296181f + languageName: node + linkType: hard + +"micromark-extension-gfm@npm:^3.0.0": + version: 3.0.0 + resolution: "micromark-extension-gfm@npm:3.0.0" + dependencies: + micromark-extension-gfm-autolink-literal: "npm:^2.0.0" + micromark-extension-gfm-footnote: "npm:^2.0.0" + micromark-extension-gfm-strikethrough: "npm:^2.0.0" + micromark-extension-gfm-table: "npm:^2.0.0" + micromark-extension-gfm-tagfilter: "npm:^2.0.0" + micromark-extension-gfm-task-list-item: "npm:^2.0.0" + micromark-util-combine-extensions: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/970e28df6ebdd7c7249f52a0dda56e0566fbfa9ae56c8eeeb2445d77b6b89d44096880cd57a1c01e7821b1f4e31009109fbaca4e89731bff7b83b8519690e5d9 + languageName: node + linkType: hard + +"micromark-factory-destination@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-factory-destination@npm:2.0.1" + dependencies: + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/bbafcf869cee5bf511161354cb87d61c142592fbecea051000ff116068dc85216e6d48519d147890b9ea5d7e2864a6341c0c09d9948c203bff624a80a476023c + languageName: node + linkType: hard + +"micromark-factory-label@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-factory-label@npm:2.0.1" + dependencies: + devlop: "npm:^1.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/0137716b4ecb428114165505e94a2f18855c8bbea21b07a8b5ce514b32a595ed789d2b967125718fc44c4197ceaa48f6609d58807a68e778138d2e6b91b824e8 + languageName: node + linkType: hard + +"micromark-factory-space@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-factory-space@npm:2.0.1" + dependencies: + micromark-util-character: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/f9ed43f1c0652d8d898de0ac2be3f77f776fffe7dd96bdbba1e02d7ce33d3853c6ff5daa52568fc4fa32cdf3a62d86b85ead9b9189f7211e1d69ff2163c450fb + languageName: node + linkType: hard + +"micromark-factory-title@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-factory-title@npm:2.0.1" + dependencies: + micromark-factory-space: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/e72fad8d6e88823514916890099a5af20b6a9178ccf78e7e5e05f4de99bb8797acb756257d7a3a57a53854cb0086bf8aab15b1a9e9db8982500dd2c9ff5948b6 + languageName: node + linkType: hard + +"micromark-factory-whitespace@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-factory-whitespace@npm:2.0.1" + dependencies: + micromark-factory-space: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/20a1ec58698f24b766510a309b23a10175034fcf1551eaa9da3adcbed3e00cd53d1ebe5f030cf873f76a1cec3c34eb8c50cc227be3344caa9ed25d56cf611224 + languageName: node + linkType: hard + +"micromark-util-character@npm:^2.0.0": + version: 2.1.1 + resolution: "micromark-util-character@npm:2.1.1" + dependencies: + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/d3fe7a5e2c4060fc2a076f9ce699c82a2e87190a3946e1e5eea77f563869b504961f5668d9c9c014724db28ac32fa909070ea8b30c3a39bd0483cc6c04cc76a1 + languageName: node + linkType: hard + +"micromark-util-chunked@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-chunked@npm:2.0.1" + dependencies: + micromark-util-symbol: "npm:^2.0.0" + checksum: 10c0/b68c0c16fe8106949537bdcfe1be9cf36c0ccd3bc54c4007003cb0984c3750b6cdd0fd77d03f269a3382b85b0de58bde4f6eedbe7ecdf7244759112289b1ab56 + languageName: node + linkType: hard + +"micromark-util-classify-character@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-classify-character@npm:2.0.1" + dependencies: + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/8a02e59304005c475c332f581697e92e8c585bcd45d5d225a66c1c1b14ab5a8062705188c2ccec33cc998d33502514121478b2091feddbc751887fc9c290ed08 + languageName: node + linkType: hard + +"micromark-util-combine-extensions@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-combine-extensions@npm:2.0.1" + dependencies: + micromark-util-chunked: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/f15e282af24c8372cbb10b9b0b3e2c0aa681fea0ca323a44d6bc537dc1d9382c819c3689f14eaa000118f5a163245358ce6276b2cda9a84439cdb221f5d86ae7 + languageName: node + linkType: hard + +"micromark-util-decode-numeric-character-reference@npm:^2.0.0": + version: 2.0.2 + resolution: "micromark-util-decode-numeric-character-reference@npm:2.0.2" + dependencies: + micromark-util-symbol: "npm:^2.0.0" + checksum: 10c0/9c8a9f2c790e5593ffe513901c3a110e9ec8882a08f466da014112a25e5059b51551ca0aeb7ff494657d86eceb2f02ee556c6558b8d66aadc61eae4a240da0df + languageName: node + linkType: hard + +"micromark-util-decode-string@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-decode-string@npm:2.0.1" + dependencies: + decode-named-character-reference: "npm:^1.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-decode-numeric-character-reference: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + checksum: 10c0/f24d75b2e5310be6e7b6dee532e0d17d3bf46996841d6295f2a9c87a2046fff4ab603c52ab9d7a7a6430a8b787b1574ae895849c603d262d1b22eef71736b5cb + languageName: node + linkType: hard + +"micromark-util-encode@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-encode@npm:2.0.1" + checksum: 10c0/b2b29f901093845da8a1bf997ea8b7f5e061ffdba85070dfe14b0197c48fda64ffcf82bfe53c90cf9dc185e69eef8c5d41cae3ba918b96bc279326921b59008a + languageName: node + linkType: hard + +"micromark-util-html-tag-name@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-html-tag-name@npm:2.0.1" + checksum: 10c0/ae80444db786fde908e9295f19a27a4aa304171852c77414516418650097b8afb401961c9edb09d677b06e97e8370cfa65638dde8438ebd41d60c0a8678b85b9 + languageName: node + linkType: hard + +"micromark-util-normalize-identifier@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-normalize-identifier@npm:2.0.1" + dependencies: + micromark-util-symbol: "npm:^2.0.0" + checksum: 10c0/5299265fa360769fc499a89f40142f10a9d4a5c3dd8e6eac8a8ef3c2e4a6570e4c009cf75ea46dce5ee31c01f25587bde2f4a5cc0a935584ae86dd857f2babbd + languageName: node + linkType: hard + +"micromark-util-resolve-all@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-resolve-all@npm:2.0.1" + dependencies: + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/bb6ca28764696bb479dc44a2d5b5fe003e7177aeae1d6b0d43f24cc223bab90234092d9c3ce4a4d2b8df095ccfd820537b10eb96bb7044d635f385d65a4c984a + languageName: node + linkType: hard + +"micromark-util-sanitize-uri@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-sanitize-uri@npm:2.0.1" + dependencies: + micromark-util-character: "npm:^2.0.0" + micromark-util-encode: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + checksum: 10c0/60e92166e1870fd4f1961468c2651013ff760617342918e0e0c3c4e872433aa2e60c1e5a672bfe5d89dc98f742d6b33897585cf86ae002cda23e905a3c02527c + languageName: node + linkType: hard + +"micromark-util-subtokenize@npm:^2.0.0": + version: 2.1.0 + resolution: "micromark-util-subtokenize@npm:2.1.0" + dependencies: + devlop: "npm:^1.0.0" + micromark-util-chunked: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/bee69eece4393308e657c293ba80d92ebcb637e5f55e21dcf9c3fa732b91a8eda8ac248d76ff375e675175bfadeae4712e5158ef97eef1111789da1ce7ab5067 + languageName: node + linkType: hard + +"micromark-util-symbol@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-util-symbol@npm:2.0.1" + checksum: 10c0/f2d1b207771e573232436618e78c5e46cd4b5c560dd4a6d63863d58018abbf49cb96ec69f7007471e51434c60de3c9268ef2bf46852f26ff4aacd10f9da16fe9 + languageName: node + linkType: hard + +"micromark-util-types@npm:^2.0.0": + version: 2.0.2 + resolution: "micromark-util-types@npm:2.0.2" + checksum: 10c0/c8c15b96c858db781c4393f55feec10004bf7df95487636c9a9f7209e51002a5cca6a047c5d2a5dc669ff92da20e57aaa881e81a268d9ccadb647f9dce305298 + languageName: node + linkType: hard + +"micromark@npm:^4.0.0": + version: 4.0.2 + resolution: "micromark@npm:4.0.2" + dependencies: + "@types/debug": "npm:^4.0.0" + debug: "npm:^4.0.0" + decode-named-character-reference: "npm:^1.0.0" + devlop: "npm:^1.0.0" + micromark-core-commonmark: "npm:^2.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-chunked: "npm:^2.0.0" + micromark-util-combine-extensions: "npm:^2.0.0" + micromark-util-decode-numeric-character-reference: "npm:^2.0.0" + micromark-util-encode: "npm:^2.0.0" + micromark-util-normalize-identifier: "npm:^2.0.0" + micromark-util-resolve-all: "npm:^2.0.0" + micromark-util-sanitize-uri: "npm:^2.0.0" + micromark-util-subtokenize: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + checksum: 10c0/07462287254219d6eda6eac8a3cebaff2994e0575499e7088027b825105e096e4f51e466b14b2a81b71933a3b6c48ee069049d87bc2c2127eee50d9cc69e8af6 + languageName: node + linkType: hard + +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c + languageName: node + linkType: hard + +"minimatch@npm:^10.2.2, minimatch@npm:^10.2.4, minimatch@npm:^10.2.5": + version: 10.2.6 + resolution: "minimatch@npm:10.2.6" + dependencies: + brace-expansion: "npm:^5.0.8" + checksum: 10c0/4559a836243b98bd4d17ea9f7edae698717c76399eea7be374f3737f33164e4907f19e9726891ddeb122f750a5a7fa80d2ac43e851d6e5984dc4ff42ec127d3a + languageName: node + linkType: hard + +"minipass@npm:^7.0.4, minipass@npm:^7.1.2": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb + languageName: node + linkType: hard + +"minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"msw@npm:^2.15.0": + version: 2.15.0 + resolution: "msw@npm:2.15.0" + dependencies: + "@inquirer/confirm": "npm:^6.0.11" + "@mswjs/interceptors": "npm:^0.41.3" + "@open-draft/deferred-promise": "npm:^3.0.0" + "@types/statuses": "npm:^2.0.6" + cookie: "npm:^1.1.1" + graphql: "npm:^16.13.2" + headers-polyfill: "npm:^5.0.1" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.3" + path-to-regexp: "npm:^6.3.0" + picocolors: "npm:^1.1.1" + rettime: "npm:^0.11.11" + statuses: "npm:^2.0.2" + strict-event-emitter: "npm:^0.5.1" + tough-cookie: "npm:^6.0.1" + type-fest: "npm:^5.5.0" + until-async: "npm:^3.0.2" + yargs: "npm:^17.7.2" + peerDependencies: + typescript: ">= 4.8.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 10c0/0d3dbf1b062a82b3711ff195e10ffd9e6f2a5e337b4a0f08cabdb8d0b96c5c0c057f1cdcb67da14c356612554ea463fb5ad8f9c618b5ac009fc4eee43a83c94e + languageName: node + linkType: hard + +"mute-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "mute-stream@npm:3.0.0" + checksum: 10c0/12cdb36a101694c7a6b296632e6d93a30b74401873cf7507c88861441a090c71c77a58f213acadad03bc0c8fa186639dec99d68a14497773a8744320c136e701 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.16": + version: 3.3.16 + resolution: "nanoid@npm:3.3.16" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/bbf2dcffe22d2b62d16de2711752070b539c0f644c7916f823ad6521986b2078cbe524f2d6240f58c46e9141ea0c7b87a029e100c0f7f175228cacaf30e41bba + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 10c0/f5f9a7974bfb28a91afafa254b197f0f22c684d4a1731763dda960d2c8e375b36c7d690e0d9dc8fba774c537af14a7e979129bca23d88d052fbeb9466955e447 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 13.0.1 + resolution: "node-gyp@npm:13.0.1" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + nopt: "npm:^10.0.0" + proc-log: "npm:^7.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + undici: "npm:^8.4.1" + which: "npm:^7.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/424077bc9e9bbe953a8e86db473ba818cbc6a121714008c977fd589e21e5f0c811fbf22faac730dc7182450b5e52df301811d01ae3373898658d999b7710f4e6 + languageName: node + linkType: hard + +"node-releases@npm:^2.0.51": + version: 2.0.51 + resolution: "node-releases@npm:2.0.51" + checksum: 10c0/6cf3fe1f9eabe02ce6de67e9db77b73edc9f5625003791e1f8f239f8e2a851450a5aeb1eff2cc43cfb26312e19b26bfe07626bf428479e6a76f56553fc6148da + languageName: node + linkType: hard + +"nopt@npm:^10.0.0": + version: 10.0.1 + resolution: "nopt@npm:10.0.1" + dependencies: + abbrev: "npm:^5.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/980d89257f9587f3e1f77877ddbf905d6aa3b738ec33e49a4fa1a059a0dd82eb28063982b150654a7ae9de386f2ead60e56172db7d37cf56de545f7392a2a26a + languageName: node + linkType: hard + +"obug@npm:^2.1.1": + version: 2.1.4 + resolution: "obug@npm:2.1.4" + checksum: 10c0/34a0ee97cd88573cfd97d384c2a79f07118ae5680d7e45d1de6e99c74eddefe145e8ca27a2db02195a1ee5fded5aa22b924869c842728c201b9f109a27d0ef19 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.4 + resolution: "optionator@npm:0.9.4" + dependencies: + deep-is: "npm:^0.1.3" + fast-levenshtein: "npm:^2.0.6" + levn: "npm:^0.4.1" + prelude-ls: "npm:^1.2.1" + type-check: "npm:^0.4.0" + word-wrap: "npm:^1.2.5" + checksum: 10c0/4afb687a059ee65b61df74dfe87d8d6815cd6883cb8b3d5883a910df72d0f5d029821f37025e4bccf4048873dbdb09acc6d303d27b8f76b1a80dd5a7d5334675 + languageName: node + linkType: hard + +"outvariant@npm:^1.4.0, outvariant@npm:^1.4.3": + version: 1.4.3 + resolution: "outvariant@npm:1.4.3" + checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: "npm:^3.0.0" + checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 + languageName: node + linkType: hard + +"parse-entities@npm:^4.0.0": + version: 4.0.2 + resolution: "parse-entities@npm:4.0.2" + dependencies: + "@types/unist": "npm:^2.0.0" + character-entities-legacy: "npm:^3.0.0" + character-reference-invalid: "npm:^2.0.0" + decode-named-character-reference: "npm:^1.0.0" + is-alphanumerical: "npm:^2.0.0" + is-decimal: "npm:^2.0.0" + is-hexadecimal: "npm:^2.0.0" + checksum: 10c0/a13906b1151750b78ed83d386294066daf5fb559e08c5af9591b2d98cc209123103016a01df776f65f8219ad26652d6d6b210d0974d452049cddfc53a8916c34 + languageName: node + linkType: hard + +"parse-json@npm:^5.0.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + error-ex: "npm:^1.3.1" + json-parse-even-better-errors: "npm:^2.3.0" + lines-and-columns: "npm:^1.1.6" + checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 + languageName: node + linkType: hard + +"parse5@npm:^8.0.1": + version: 8.0.1 + resolution: "parse5@npm:8.0.1" + dependencies: + entities: "npm:^8.0.0" + checksum: 10c0/c3c1c5aab55f6e4be5245599790e56e64be7764a4a0edd7f98db4fe3bb380f63add752fa047dff0496446c25f4104f0c7c1967723de640bde92306a7bb67ed2f + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 + languageName: node + linkType: hard + +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 + languageName: node + linkType: hard + +"picocolors@npm:1.1.1, picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + +"picomatch@npm:^4.0.3, picomatch@npm:^4.0.4, picomatch@npm:^4.0.5": + version: 4.0.5 + resolution: "picomatch@npm:4.0.5" + checksum: 10c0/947bc6b6e1ff1e6c5aaf95b107a0839d12802f4f7b867663f67d47accba939ca1cb582cf99dfc30438efa1c4648ac5990967e783e8929c36b03e8440704ef1bd + languageName: node + linkType: hard + +"playwright-core@npm:1.62.1": + version: 1.62.1 + resolution: "playwright-core@npm:1.62.1" + bin: + playwright-core: cli.js + checksum: 10c0/a37d0f03bb73364cfd0eba704c4b3359b609c4779ae2649a88d8e2b3a29c7357dfcc58bdcf0cb68ddffdef7687abf78902c8356e89f189682c7e19be38e108f0 + languageName: node + linkType: hard + +"playwright@npm:1.62.1": + version: 1.62.1 + resolution: "playwright@npm:1.62.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.62.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/4d3522cd46325f50c46f8874d31f70d036f6983228a9f8b9d68fc249e4c17464edcd97cc4adfbb24e712d1829c1386d3bc24e17f58a1ced94e7c423300c21845 + languageName: node + linkType: hard + +"postcss@npm:^8.5.23": + version: 8.5.25 + resolution: "postcss@npm:8.5.25" + dependencies: + nanoid: "npm:^3.3.16" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/0a12c1e74b456c57122e81f684e02fd98ff4d57526f794d10c996df1147158808f5ae373ac82b988c8de6cbbaa82dbd7b13803b14f5dd3cf7cc6a42ccad5c9f2 + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: 10c0/b00d617431e7886c520a6f498a2e14c75ec58f6d93ba48c3b639cf241b54232d90daa05d83a9e9b9fef6baa63cb7e1e4602c2372fea5bc169668401eb127d0cd + languageName: node + linkType: hard + +"pretty-format@npm:^27.0.2": + version: 27.5.1 + resolution: "pretty-format@npm:27.5.1" + dependencies: + ansi-regex: "npm:^5.0.1" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^17.0.1" + checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed + languageName: node + linkType: hard + +"proc-log@npm:^7.0.0": + version: 7.0.0 + resolution: "proc-log@npm:7.0.0" + checksum: 10c0/b89c2d862604f35fec795477b0c7e376feab3ba0d4f4d291c4e959567442697cf451ac557d0623c1cc38af45a78128b983410f397a10c5d3a67f76c33de4754b + languageName: node + linkType: hard + +"property-information@npm:^7.0.0": + version: 7.2.0 + resolution: "property-information@npm:7.2.0" + checksum: 10c0/03662c8f9e1544510914c5e594ae72963f67d261027a5fdc06c4134742584fe40dd49b959470d30e757e9fb70b297d8546ab1362a040364144029926ad4e07c4 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0, punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + +"react-chartjs-2@npm:^5.3.1": + version: 5.3.1 + resolution: "react-chartjs-2@npm:5.3.1" + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/40fac3fe23a163232d952bf5cd2a14d7baceadea326b57909d5e556ed2481b7cfb177582ac8f39b8dc51ac11f3d19a6127784746f35f5771181ab700110e7326 + languageName: node + linkType: hard + +"react-dom@npm:^19.2.8": + version: 19.2.8 + resolution: "react-dom@npm:19.2.8" + dependencies: + scheduler: "npm:^0.27.0" + peerDependencies: + react: ^19.2.8 + checksum: 10c0/41ba2247b76f687fcfe5bbc99f514d6b851d8c8041c2f5ded36ed05bd7fdc5208cacbac9de51e3e6633e77f96f44cec9f0d4a5a55184dba4e00738f224439134 + languageName: node + linkType: hard + +"react-hot-toast@npm:^2.6.0": + version: 2.6.0 + resolution: "react-hot-toast@npm:2.6.0" + dependencies: + csstype: "npm:^3.1.3" + goober: "npm:^2.1.16" + peerDependencies: + react: ">=16" + react-dom: ">=16" + checksum: 10c0/c25652e0a477ab501b365a6a2d20b314275a1d9963dd56a9803972d602845f0d44ade2c3345cd66ccd251d62e2e5a10001a55fd80fa183c0e41343f3780ffa9f + languageName: node + linkType: hard + +"react-is@npm:^16.7.0": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 + languageName: node + linkType: hard + +"react-is@npm:^17.0.1": + version: 17.0.2 + resolution: "react-is@npm:17.0.2" + checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053 + languageName: node + linkType: hard + +"react-is@npm:^19.2.7": + version: 19.2.8 + resolution: "react-is@npm:19.2.8" + checksum: 10c0/ed5322c84efe035c8fc814b1614ff5ca7fb8c2872a7c045197daf99f9b1bd68f32a2c79ffcbcfcff2fc5d93924ff9f9447400898f5b1534e6302bb0736a257f0 + languageName: node + linkType: hard + +"react-markdown@npm:^10.1.0": + version: 10.1.0 + resolution: "react-markdown@npm:10.1.0" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + devlop: "npm:^1.0.0" + hast-util-to-jsx-runtime: "npm:^2.0.0" + html-url-attributes: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.0.0" + unified: "npm:^11.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + peerDependencies: + "@types/react": ">=18" + react: ">=18" + checksum: 10c0/4a5dc7d15ca6d05e9ee95318c1904f83b111a76f7588c44f50f1d54d4c97193b84e4f64c4b592057c989228238a2590306cedd0c4d398e75da49262b2b5ae1bf + languageName: node + linkType: hard + +"react-router-dom@npm:^7.18.2": + version: 7.18.2 + resolution: "react-router-dom@npm:7.18.2" + dependencies: + react-router: "npm:7.18.2" + peerDependencies: + react: ">=18" + react-dom: ">=18" + checksum: 10c0/234926d664a5b5c355aeb8642f505784facbb63321231428f24e5ea7fb859876443082d63eef4b8167a01ea8d8dab231a666736343c3e4a86a05b2fcb88927b5 + languageName: node + linkType: hard + +"react-router@npm:7.18.2": + version: 7.18.2 + resolution: "react-router@npm:7.18.2" + dependencies: + cookie: "npm:^1.0.1" + set-cookie-parser: "npm:^2.6.0" + peerDependencies: + react: ">=18" + react-dom: ">=18" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10c0/513b04adf020fcf95124557418e003d16b175765045332858d5817b045e49dfe33b529c4871c57b0cddf24fa5432589ffc45d1a9a5b398b069f02adb755fab15 + languageName: node + linkType: hard + +"react@npm:^19.2.8": + version: 19.2.8 + resolution: "react@npm:19.2.8" + checksum: 10c0/5f86bdb56426652fd6d989d30a6f2e603c057272c47c9ca3a3fbe190a3a39ee9ccce937d63cfc039717abed1b8891d6a499134bc35311acc07eafdacd86537cd + languageName: node + linkType: hard + +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: "npm:^4.0.0" + strip-indent: "npm:^3.0.0" + checksum: 10c0/d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae + languageName: node + linkType: hard + +"remark-breaks@npm:^4.0.0": + version: 4.0.0 + resolution: "remark-breaks@npm:4.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-newline-to-break: "npm:^2.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/d7b319a7993b54c5d574e9255080c5de68cfa24f993873b0ee296af13f478521c41d4b7ae0fc14b4607ea70c8f6967e998ab7a467de13139141e66a1a34cb6be + languageName: node + linkType: hard + +"remark-gfm@npm:^4.0.1": + version: 4.0.1 + resolution: "remark-gfm@npm:4.0.1" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-gfm: "npm:^3.0.0" + micromark-extension-gfm: "npm:^3.0.0" + remark-parse: "npm:^11.0.0" + remark-stringify: "npm:^11.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/427ecc6af3e76222662061a5f670a3e4e33ec5fffe2cabf04034da6a3f9a1bda1fc023e838a636385ba314e66e2bebbf017ca61ebea357eb0f5200fe0625a4b7 + languageName: node + linkType: hard + +"remark-parse@npm:^11.0.0": + version: 11.0.0 + resolution: "remark-parse@npm:11.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/6eed15ddb8680eca93e04fcb2d1b8db65a743dcc0023f5007265dda558b09db595a087f622062ccad2630953cd5cddc1055ce491d25a81f3317c858348a8dd38 + languageName: node + linkType: hard + +"remark-rehype@npm:^11.0.0": + version: 11.1.2 + resolution: "remark-rehype@npm:11.1.2" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + mdast-util-to-hast: "npm:^13.0.0" + unified: "npm:^11.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/f9eccacfb596d9605581dc05bfad28635d6ded5dd0a18e88af5fd4df0d3fcf9612e1501d4513bc2164d833cfe9636dab20400080b09e53f155c6e1442a1231fb + languageName: node + linkType: hard + +"remark-stringify@npm:^11.0.0": + version: 11.0.0 + resolution: "remark-stringify@npm:11.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/0cdb37ce1217578f6f847c7ec9f50cbab35df5b9e3903d543e74b405404e67c07defcb23cd260a567b41b769400f6de03c2c3d9cd6ae7a6707d5c8d89ead489f + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 + languageName: node + linkType: hard + +"resolve@npm:^1.19.0": + version: 1.22.12 + resolution: "resolve@npm:1.22.12" + dependencies: + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/b16dc9b537c02e8c3388f7d3dcff9741d3071625f9a97ac1c885f2b0ca51e78df22328fb6d6ef214dd9101fb7cfc19aa2836fe3410402a94f3f7b8639c7149bf + languageName: node + linkType: hard + +"resolve@patch:resolve@npm%3A^1.19.0#optional!builtin": + version: 1.22.12 + resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" + dependencies: + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/fc6519984ae1f894d877c0060ba8b1f5ba3bc0e85a02f74e141929c118c23d74d9735619a9cc2965397387e514884245c65d72a40731dcb6cfc84c7bcdc8321e + languageName: node + linkType: hard + +"rettime@npm:^0.11.11": + version: 0.11.11 + resolution: "rettime@npm:0.11.11" + checksum: 10c0/021fc9d9870ce04f032952e63fc5576f3f8e7c9c15513b1a479a64646df90239802eef6d60a98cbfb6ac87bb623d4f120a8ee71193d02984e3d2915c28695f6e + languageName: node + linkType: hard + +"rolldown@npm:~1.2.0": + version: 1.2.1 + resolution: "rolldown@npm:1.2.1" + dependencies: + "@oxc-project/types": "npm:=0.142.0" + "@rolldown/binding-android-arm64": "npm:1.2.1" + "@rolldown/binding-darwin-arm64": "npm:1.2.1" + "@rolldown/binding-darwin-x64": "npm:1.2.1" + "@rolldown/binding-freebsd-x64": "npm:1.2.1" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.2.1" + "@rolldown/binding-linux-arm64-gnu": "npm:1.2.1" + "@rolldown/binding-linux-arm64-musl": "npm:1.2.1" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.2.1" + "@rolldown/binding-linux-s390x-gnu": "npm:1.2.1" + "@rolldown/binding-linux-x64-gnu": "npm:1.2.1" + "@rolldown/binding-linux-x64-musl": "npm:1.2.1" + "@rolldown/binding-openharmony-arm64": "npm:1.2.1" + "@rolldown/binding-wasm32-wasi": "npm:1.2.1" + "@rolldown/binding-win32-arm64-msvc": "npm:1.2.1" + "@rolldown/binding-win32-x64-msvc": "npm:1.2.1" + "@rolldown/pluginutils": "npm:^1.0.0" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: ./bin/cli.mjs + checksum: 10c0/d22c80c70d71a36abde7dca7217f66d94cdb5d0c03185205de808d368ab1b8fd5e78ff4db10efc69c5f68a5f9d03d59c8ace848f1f85c4aab79330162a10b3e7 + languageName: node + linkType: hard + +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 + languageName: node + linkType: hard + +"scroll-into-view-if-needed@npm:^3.1.0": + version: 3.1.0 + resolution: "scroll-into-view-if-needed@npm:3.1.0" + dependencies: + compute-scroll-into-view: "npm:^3.0.2" + checksum: 10c0/1f46b090e1e04fcfdef1e384f6d7e615f9f84d4176faf4dbba7347cc0a6e491e5d578eaf4dbe9618dd3d8d38efafde58535b3e00f2a21ce4178c14be364850ff + languageName: node + linkType: hard + +"semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: 10c0/e3d79b609071caa78bcb6ce2ad81c7966a46a7431d9d58b8800cfa9cb6a63699b3899a0e4bcce36167a284578212d9ae6942b6929ba4aa5015c079a67751d42d + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.7.3": + version: 7.8.5 + resolution: "semver@npm:7.8.5" + bin: + semver: bin/semver.js + checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c + languageName: node + linkType: hard + +"set-cookie-parser@npm:^2.6.0": + version: 2.7.2 + resolution: "set-cookie-parser@npm:2.7.2" + checksum: 10c0/4381a9eb7ee951dfe393fe7aacf76b9a3b4e93a684d2162ab35594fa4053cc82a4d7d7582bf397718012c9adcf839b8cd8f57c6c42901ea9effe33c752da4a45 + languageName: node + linkType: hard + +"set-cookie-parser@npm:^3.0.1": + version: 3.1.2 + resolution: "set-cookie-parser@npm:3.1.2" + checksum: 10c0/ea3d4fba5affd57f2a4c2e5172d701c4c17645879f9fddf1331622ae556210d840cdcb6666f0d71288ad545e31db5927bf49696051928e0186e079588cd5ddb0 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + +"signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + +"source-map@npm:^0.5.7": + version: 0.5.7 + resolution: "source-map@npm:0.5.7" + checksum: 10c0/904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 + languageName: node + linkType: hard + +"space-separated-tokens@npm:^2.0.0": + version: 2.0.2 + resolution: "space-separated-tokens@npm:2.0.2" + checksum: 10c0/6173e1d903dca41dcab6a2deed8b4caf61bd13b6d7af8374713500570aa929ff9414ae09a0519f4f8772df993300305a395d4871f35bc4ca72b6db57e1f30af8 + languageName: node + linkType: hard + +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"statuses@npm:^2.0.2": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + +"std-env@npm:^4.0.0-rc.1": + version: 4.2.0 + resolution: "std-env@npm:4.2.0" + checksum: 10c0/40ac525ce7b7c556abc332a7376f14356eeb1a7f17f6ff9a003eb9f52326ff1f3745d3e1b43452675b1ec6fcc319f1b1d6f3b0d386cf3f91058479ad883cff69 + languageName: node + linkType: hard + +"strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: 10c0/f5228a6e6b6393c57f52f62e673cfe3be3294b35d6f7842fc24b172ae0a6e6c209fa83241d0e433fc267c503bc2f4ffdbe41a9990ff8ffd5ac425ec0489417f7 + languageName: node + linkType: hard + +"string-convert@npm:^0.2.0": + version: 0.2.1 + resolution: "string-convert@npm:0.2.1" + checksum: 10c0/00673ed8a3106137395436537ace7d3672c91a3290da73466055daa0134331dc84bc58c54ba2d2ea40711adc5744426d3c8239dbfc30290438fa3e9ff65db528 + languageName: node + linkType: hard + +"string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"stringify-entities@npm:^4.0.0": + version: 4.0.4 + resolution: "stringify-entities@npm:4.0.4" + dependencies: + character-entities-html4: "npm:^2.0.0" + character-entities-legacy: "npm:^3.0.0" + checksum: 10c0/537c7e656354192406bdd08157d759cd615724e9d0873602d2c9b2f6a5c0a8d0b1d73a0a08677848105c5eebac6db037b57c0b3a4ec86331117fa7319ed50448 + languageName: node + linkType: hard + +"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: "npm:^1.0.0" + checksum: 10c0/ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679 + languageName: node + linkType: hard + +"style-to-js@npm:^1.0.0": + version: 1.1.21 + resolution: "style-to-js@npm:1.1.21" + dependencies: + style-to-object: "npm:1.0.14" + checksum: 10c0/94231aa80f58f442c3a5ae01a21d10701e5d62f96b4b3e52eab3499077ee52df203cc0df4a1a870707f5e99470859136ea8657b782a5f4ca7934e0ffe662a588 + languageName: node + linkType: hard + +"style-to-object@npm:1.0.14": + version: 1.0.14 + resolution: "style-to-object@npm:1.0.14" + dependencies: + inline-style-parser: "npm:0.2.7" + checksum: 10c0/854d9e9b77afc336e6d7b09348e7939f2617b34eb0895824b066d8cd1790284cb6d8b2ba36be88025b2595d715dba14b299ae76e4628a366541106f639e13679 + languageName: node + linkType: hard + +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: 10c0/a7128ad5a8ed72652c6eba46bed4f416521bc9745a460ef5741edc725252cebf36ee45e33a8615a7057403c93df0866ab9ee955960792db210bb80abd5ac6543 + languageName: node + linkType: hard + +"stylis@npm:^4.3.4": + version: 4.4.0 + resolution: "stylis@npm:4.4.0" + checksum: 10c0/259be096d90dfbfe903c8656dcb7591e52a421e577e950ef42ebd9ca02f387623a1165dd08761492fb6e92a7a562d62a53a694a10b0a2f6dcd7a0db107b4bf55 + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 + languageName: node + linkType: hard + +"swr@npm:^2.4.2": + version: 2.4.2 + resolution: "swr@npm:2.4.2" + dependencies: + dequal: "npm:^2.0.3" + use-sync-external-store: "npm:^1.6.0" + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/fdb54e3cb5e788d4aa53d7db7d541709807c0dde2af41c423e241274cd6595e66dca15a0a310e08a26a49391ff615c153aaa44710016249c2d4c0fe653cc970f + languageName: node + linkType: hard + +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + +"tagged-tag@npm:^1.0.0": + version: 1.0.0 + resolution: "tagged-tag@npm:1.0.0" + checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1 + languageName: node + linkType: hard + +"tar@npm:^7.5.4": + version: 7.5.22 + resolution: "tar@npm:7.5.22" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10c0/1311f6be85a8157ac4c9147bae43e13923d2a1aae15e4aa1bd5239e4e03d2cf53cfe103dde7f35832fbb4c938b042856bc8e9a0afd29abd05e2d1608788c4fea + languageName: node + linkType: hard + +"throttle-debounce@npm:^5.0.0, throttle-debounce@npm:^5.0.2": + version: 5.0.2 + resolution: "throttle-debounce@npm:5.0.2" + checksum: 10c0/9a10ac51400b353562770721718486847adb5d7287c94a0c0d47df5326e8d47e5d92fcb74dac53d6734efb9344a2d46d68c7f996c2d0aedfd11446522e4bb356 + languageName: node + linkType: hard + +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2": + version: 1.2.4 + resolution: "tinyexec@npm:1.2.4" + checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.17": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c + languageName: node + linkType: hard + +"tinyrainbow@npm:^3.1.0": + version: 3.1.1 + resolution: "tinyrainbow@npm:3.1.1" + checksum: 10c0/f9d2743832c6191f753408f36224fe817620b8abcef572b2e570204c673a901d753ff84ca8e7b88f9c79e934295b3ffc6fcbc56a06f126e24e1ec6186dcad40d + languageName: node + linkType: hard + +"tldts-core@npm:^7.4.9": + version: 7.4.9 + resolution: "tldts-core@npm:7.4.9" + checksum: 10c0/6ada3e0d66661fb7ccedc4a1753d9224c1fb565086221af3ca29d51eaaf4745078e556c4fb30469140b0cb00b0102d1ca49de15ad29c069c7957463d0601028a + languageName: node + linkType: hard + +"tldts@npm:^7.0.5": + version: 7.4.9 + resolution: "tldts@npm:7.4.9" + dependencies: + tldts-core: "npm:^7.4.9" + bin: + tldts: bin/cli.js + checksum: 10c0/5df4d25696dfb8cc009c3e0bea0e00bc1957c3a201155e2ea2027ce50c82b985250d1a4a301f3e5b280dd49215df9e97a92304196084872a59789e3f122f0a0c + languageName: node + linkType: hard + +"tough-cookie@npm:^6.0.1, tough-cookie@npm:^6.0.2": + version: 6.0.2 + resolution: "tough-cookie@npm:6.0.2" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10c0/5ff521a476a3c540821352125a5d481c8d2fe16035de7e0efda4df120f290c95500a0e9b51ea0aa56343955be482e014c30f5ba73e04b93ba138e4e855cb9e89 + languageName: node + linkType: hard + +"tr46@npm:^6.0.0": + version: 6.0.0 + resolution: "tr46@npm:6.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e + languageName: node + linkType: hard + +"trim-lines@npm:^3.0.0": + version: 3.0.1 + resolution: "trim-lines@npm:3.0.1" + checksum: 10c0/3a1611fa9e52aa56a94c69951a9ea15b8aaad760eaa26c56a65330dc8adf99cb282fc07cc9d94968b7d4d88003beba220a7278bbe2063328eb23fb56f9509e94 + languageName: node + linkType: hard + +"trough@npm:^2.0.0": + version: 2.2.0 + resolution: "trough@npm:2.2.0" + checksum: 10c0/58b671fc970e7867a48514168894396dd94e6d9d6456aca427cc299c004fe67f35ed7172a36449086b2edde10e78a71a284ec0076809add6834fb8f857ccb9b0 + languageName: node + linkType: hard + +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/767849383c114e7f1971fa976b20e73ac28fd0c70d8d65c0004790bf4d8f89888c7e4cf6d5949f9c1beae9bc3c64835bef77bbe27fddf45a3c7b60cebcf85c8c + languageName: node + linkType: hard + +"tslib@npm:^2.4.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: "npm:^1.2.1" + checksum: 10c0/7b3fd0ed43891e2080bf0c5c504b418fbb3e5c7b9708d3d015037ba2e6323a28152ec163bcb65212741fa5d2022e3075ac3c76440dbd344c9035f818e8ecee58 + languageName: node + linkType: hard + +"type-fest@npm:^5.5.0": + version: 5.8.0 + resolution: "type-fest@npm:5.8.0" + dependencies: + tagged-tag: "npm:^1.0.0" + checksum: 10c0/c8aae118a763d550a9552a511dff6b71840a23dab4edf693cf1c4df22596942794e6f6723389bd9036a90182249d915158bacf0815a1ae87f05f901b1d5f574e + languageName: node + linkType: hard + +"typescript-eslint@npm:^8.65.0": + version: 8.65.0 + resolution: "typescript-eslint@npm:8.65.0" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.65.0" + "@typescript-eslint/parser": "npm:8.65.0" + "@typescript-eslint/typescript-estree": "npm:8.65.0" + "@typescript-eslint/utils": "npm:8.65.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/d1f5eede08c6d0d500aa605a1de2a4e721c00e8f56f632581e082aac6d1d2b9d365ce692cf5e0e212ea78271a032c8785e0fc58aee276c7930f92fff6a3358c8 + languageName: node + linkType: hard + +"typescript@npm:^6.0.3": + version: 6.0.3 + resolution: "typescript@npm:6.0.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/4a25ff5045b984370f48f196b3a0120779b1b343d40b9a68d114ea5e5fff099809b2bb777576991a63a5cd59cf7bffd96ff6fe10afcefbcb8bd6fb96ad4b6606 + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^6.0.3#optional!builtin": + version: 6.0.3 + resolution: "typescript@patch:typescript@npm%3A6.0.3#optional!builtin::version=6.0.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/2f25c74e65663c248fa1ade2b8459d9ce5372ff9dad07067310f132966ebec1d93f6c42f0baf77a6b6a7a91460463f708e6887013aaade22111037457c6b25df + languageName: node + linkType: hard + +"undici-types@npm:~8.3.0": + version: 8.3.0 + resolution: "undici-types@npm:8.3.0" + checksum: 10c0/c8aa7e2fbebfce519654dafadc0ece59be888d2ccaf180fb4495da875e7b536d2456345c384069c7e6f3e9c9ab7435f074957da306f142343eee86ff8048855a + languageName: node + linkType: hard + +"undici@npm:^8.4.1, undici@npm:^8.9.0": + version: 8.9.0 + resolution: "undici@npm:8.9.0" + checksum: 10c0/e3d9fa35a9aa8360d9f56e66bd372f451ee053e25066a97fd9fab3816267035660f67870c6d709a58a5411551657af78c4475be5b31c113b04f70251309900d0 + languageName: node + linkType: hard + +"unified@npm:^11.0.0": + version: 11.0.5 + resolution: "unified@npm:11.0.5" + dependencies: + "@types/unist": "npm:^3.0.0" + bail: "npm:^2.0.0" + devlop: "npm:^1.0.0" + extend: "npm:^3.0.0" + is-plain-obj: "npm:^4.0.0" + trough: "npm:^2.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/53c8e685f56d11d9d458a43e0e74328a4d6386af51c8ac37a3dcabec74ce5026da21250590d4aff6733ccd7dc203116aae2b0769abc18cdf9639a54ae528dfc9 + languageName: node + linkType: hard + +"unist-util-is@npm:^6.0.0": + version: 6.0.1 + resolution: "unist-util-is@npm:6.0.1" + dependencies: + "@types/unist": "npm:^3.0.0" + checksum: 10c0/5a487d390193811d37a68264e204dbc7c15c40b8fc29b5515a535d921d071134f571d7b5cbd59bcd58d5ce1c0ab08f20fc4a1f0df2287a249c979267fc32ce06 + languageName: node + linkType: hard + +"unist-util-position@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-position@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + checksum: 10c0/dde3b31e314c98f12b4dc6402f9722b2bf35e96a4f2d463233dd90d7cde2d4928074a7a11eff0a5eb1f4e200f27fc1557e0a64a7e8e4da6558542f251b1b7400 + languageName: node + linkType: hard + +"unist-util-stringify-position@npm:^4.0.0": + version: 4.0.0 + resolution: "unist-util-stringify-position@npm:4.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + checksum: 10c0/dfe1dbe79ba31f589108cb35e523f14029b6675d741a79dea7e5f3d098785045d556d5650ec6a8338af11e9e78d2a30df12b1ee86529cded1098da3f17ee999e + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^6.0.0": + version: 6.0.2 + resolution: "unist-util-visit-parents@npm:6.0.2" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/f1e4019dbd930301825895e3737b1ee0cd682f7622ddd915062135cbb39f8c090aaece3a3b5eae1f2ea52ec33f0931abb8f8a8b5c48a511a4203e3d360a8cd49 + languageName: node + linkType: hard + +"unist-util-visit@npm:^5.0.0": + version: 5.1.0 + resolution: "unist-util-visit@npm:5.1.0" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + unist-util-visit-parents: "npm:^6.0.0" + checksum: 10c0/a56e1bbbf63fcb55abe379e660b9a3367787e8be1e2473bdb7e86cfa6f32b6c1fa0092432d7040b8a30b2fc674bbbe024ffe6d03c3d6bf4839b064f584463a4e + languageName: node + linkType: hard + +"until-async@npm:^3.0.2": + version: 3.0.2 + resolution: "until-async@npm:3.0.2" + checksum: 10c0/61c8b03895dbe18fe3d90316d0a1894e0c131ea4b1673f6ce78eed993d0bb81bbf4b7adf8477e9ff7725782a76767eed9d077561cfc9f89b4a1ebe61f7c9828e + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.2.3": + version: 1.2.3 + resolution: "update-browserslist-db@npm:1.2.3" + dependencies: + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10c0/13a00355ea822388f68af57410ce3255941d5fb9b7c49342c4709a07c9f230bbef7f7499ae0ca7e0de532e79a82cc0c4edbd125f1a323a1845bf914efddf8bec + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: "npm:^2.1.0" + checksum: 10c0/4ef57b45aa820d7ac6496e9208559986c665e49447cb072744c13b66925a362d96dd5a46c4530a6b8e203e5db5fe849369444440cb22ecfc26c679359e5dfa3c + languageName: node + linkType: hard + +"use-sync-external-store@npm:^1.6.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b + languageName: node + linkType: hard + +"vfile-message@npm:^4.0.0": + version: 4.0.3 + resolution: "vfile-message@npm:4.0.3" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-stringify-position: "npm:^4.0.0" + checksum: 10c0/33d9f219610d27987689bb14fa5573d2daa146941d1a05416dd7702c4215b23f44ed81d059e70d0e4e24f9a57d5f4dc9f18d35a993f04cf9446a7abe6d72d0c0 + languageName: node + linkType: hard + +"vfile@npm:^6.0.0": + version: 6.0.3 + resolution: "vfile@npm:6.0.3" + dependencies: + "@types/unist": "npm:^3.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10c0/e5d9eb4810623f23758cfc2205323e33552fb5972e5c2e6587babe08fe4d24859866277404fb9e2a20afb71013860d96ec806cb257536ae463c87d70022ab9ef + languageName: node + linkType: hard + +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.2.0": + version: 8.2.0 + resolution: "vite@npm:8.2.0" + dependencies: + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.33.0" + picomatch: "npm:^4.0.5" + postcss: "npm:^8.5.23" + rolldown: "npm:~1.2.0" + tinyglobby: "npm:^0.2.17" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/bd6a5e7b28973bac06f0e8e54833800e16d1bf38a8aef2acbfea5bbaed6d8fc715fd7cc9b0ec75ef1adbde149bb9167b085c17fe7edcd3a190b4eda16d474e5c + languageName: node + linkType: hard + +"vitest@npm:^4.1.10": + version: 4.1.10 + resolution: "vitest@npm:4.1.10" + dependencies: + "@vitest/expect": "npm:4.1.10" + "@vitest/mocker": "npm:4.1.10" + "@vitest/pretty-format": "npm:4.1.10" + "@vitest/runner": "npm:4.1.10" + "@vitest/snapshot": "npm:4.1.10" + "@vitest/spy": "npm:4.1.10" + "@vitest/utils": "npm:4.1.10" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.10 + "@vitest/browser-preview": 4.1.10 + "@vitest/browser-webdriverio": 4.1.10 + "@vitest/coverage-istanbul": 4.1.10 + "@vitest/coverage-v8": 4.1.10 + "@vitest/ui": 4.1.10 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: ./vitest.mjs + checksum: 10c0/ff07294a57f9c62f3b503f7cf88a52ee0753ed26389a49cda430387a3898f39d80af47180b0af19e27acab5bd11ae95706bd4b44ce8befc97d3ae49af6ca4fc1 + languageName: node + linkType: hard + +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + +"webidl-conversions@npm:^8.0.1": + version: 8.0.1 + resolution: "webidl-conversions@npm:8.0.1" + checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46 + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-mimetype@npm:5.0.0" + checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4 + languageName: node + linkType: hard + +"whatwg-url@npm:^16.0.0": + version: 16.0.1 + resolution: "whatwg-url@npm:16.0.1" + dependencies: + "@exodus/bytes": "npm:^1.11.0" + tr46: "npm:^6.0.0" + webidl-conversions: "npm:^8.0.1" + checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2 + languageName: node + linkType: hard + +"whatwg-url@npm:^17.1.0": + version: 17.1.0 + resolution: "whatwg-url@npm:17.1.0" + dependencies: + "@exodus/bytes": "npm:^1.15.1" + tr46: "npm:^6.0.0" + webidl-conversions: "npm:^8.0.1" + checksum: 10c0/08fe809e90e1d20f6de631c9cdf312623fe5526731dc176ad38967c7d0f34e6e9700cbc0bad5ecd3d5ea8c62737a487883948e85937432f3a598746d8e05e80e + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^7.0.0": + version: 7.0.0 + resolution: "which@npm:7.0.0" + dependencies: + isexe: "npm:^4.0.0" + bin: + node-which: bin/which.js + checksum: 10c0/ca0b54f198f78bbc4b7c02e34bda8d335cb352e0adb4cbca1c37b1a957af3a879a82c4c27ca6525bc942f548d8b64f816ef6528360af9f3de55ffb9b979b620d + languageName: node + linkType: hard + +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 10c0/e0e4a1ca27599c92a6ca4c32260e8a92e8a44f4ef6ef93f803f8ed823f486e0889fc0b93be4db59c8d51b3064951d25e43d434e95dc8c960cc3a63d65d00ba20 + languageName: node + linkType: hard + +"wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 10c0/c66a5c46bc89af1625476f7f0f2ec3653c1a1791d2f9407cfb4c2ba812a1e1c9941416d71ba9719876530e3340a99925f697142989371b72d93b9ee628afd8c1 + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard + +"yaml@npm:^1.10.0": + version: 1.10.3 + resolution: "yaml@npm:1.10.3" + checksum: 10c0/c309ff85a0a569a981d71ab9cf0fef68672a16b9cdf40639d1c3b30034f6cd16ee428602bd6d64ecf006f8c8bee499023cac236538f79898aa99fb5db529a2ed + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": + version: 17.7.3 + resolution: "yargs@npm:17.7.3" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/7a28572f7e785a57886e34fdbddb9b28756dec552e1453d5f6e7cdd00ad8721a4e8c4321d33683f5e61cacb36ad43258adbb48396b71ec4ed14abee0fc0d0c1f + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f + languageName: node + linkType: hard + +"zod-validation-error@npm:^3.5.0 || ^4.0.0": + version: 4.0.2 + resolution: "zod-validation-error@npm:4.0.2" + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + checksum: 10c0/0ccfec48c46de1be440b719cd02044d4abb89ed0e14c13e637cd55bf29102f67ccdba373f25def0fc7130e5f15025be4d557a7edcc95d5a3811599aade689e1b + languageName: node + linkType: hard + +"zod@npm:^3.25.0 || ^4.0.0": + version: 4.4.3 + resolution: "zod@npm:4.4.3" + checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3 + languageName: node + linkType: hard + +"zwitch@npm:^2.0.0": + version: 2.0.4 + resolution: "zwitch@npm:2.0.4" + checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e + languageName: node + linkType: hard From 22e01a6e6ac6f5f10db1ef6abeadb82701efc7be Mon Sep 17 00:00:00 2001 From: Nicholas Bucher Date: Wed, 26 Aug 2026 10:13:55 -0400 Subject: [PATCH 02/25] fix(ui): two chat defects, and the caret where the next word goes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tool call's payload reshuffled its properties while it was being read. The args arrive as a protobuf `Struct` flattened to plain JSON, and a `Struct` carries no field order — for a Go map, a different one every marshal — so re-reading the transcript during a live turn redrew an unchanged payload in a new order each time. Its keys are sorted at every depth now; array order is left alone, because there the order is the data. The fixture supplies its args out of alphabetical order on purpose, so the assertion tests the sort rather than the order it was handed. Reloading a conversation just started sent its opening message a second time. `AgentNewChatPage` hands the first message to `AgentChatPage` in `location.state`, which the browser keeps in the session history entry rather than in memory: it came back with the page, and the effect that sends it fired again. The comment there already said the entry had to be cleared once the turn was under way; it never was. Three places the page knew which box the reader was about to type in and made them click it first: the new-conversation page, a conversation opened from the rail, and a parked question whose single prose field is the only thing that can end the turn. That field takes Enter now, and hands the caret back to the composer afterwards — it exists only because the composer could not end the turn, so once it has, the next thing typed is an ordinary message. Only the single-field shape: with several questions, taking the caret would be choosing which one gets answered first, and Enter would send the one still being filled in. An open conversation needs an effect rather than `autoFocus`, because its composer mounts disabled while the instance is still being fetched, and a focus before the box can be typed in is no focus at all. It fires once per conversation, so one coming back from suspended cannot take the caret from wherever the reader has since put it. The notes under the conversation, model, MCP server, prompt and template tables explaining which side of the wire narrowed their rows are gone. The constraint they documented is real and now lives only in `playwright/DEFERRED.md`, rewritten to say so: these lists narrow in the browser, which is honest only while the RPC returns every row. The dashboard's recent-activity card is retitled "Recent agent conversations", which is what it lists. `KAGENT_DEV_CONTROLLER_URL` can be set in `.env` now, where a reader would look for it. `vite.config.ts` read it only from `process.env`, and Vite does not put `.env` values there, so a value put in the file did nothing. `.env.example` documents pointing it at the UI pod's nginx on 8080, which needs no port-forward beyond the one `dev-scripts/setup-cluster.sh` already holds open, and leaves it commented: unset still means the controller on 8083. And `.gitignore` covers `.next/`, `next-env.d.ts` and `playwright/test-results/`, left behind by the app this one replaced. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Nicholas Bucher --- ui/.env.example | 22 ++++- ui/.gitignore | 5 ++ ui/playwright/DEFERRED.md | 17 ++-- .../agent-templates/agent-templates.spec.ts | 15 +--- .../tests/agents/agent-conversations.spec.ts | 38 +++++--- ui/playwright/tests/chat/chat-errors.spec.ts | 89 ++++++++++++++++++- ui/playwright/tests/chat/chat.spec.ts | 16 ++++ .../tests/lists/list-filters.spec.ts | 58 ------------ ui/src/api/chat/mockChatClient.ts | 29 ++++-- ui/src/components/chat/AskUserPrompt.tsx | 46 +++++++++- ui/src/components/chat/ChatComposer.tsx | 30 ++++++- ui/src/components/chat/ChatTranscript.tsx | 9 ++ ui/src/components/chat/ToolCallCard.tsx | 10 +-- ui/src/components/chat/stableJson.test.ts | 36 ++++++++ ui/src/components/chat/stableJson.ts | 28 ++++++ ui/src/components/table/FilterBar.tsx | 45 +--------- ui/src/mocks/scenario.ts | 7 +- ui/src/pages/AgentChatPage.tsx | 47 +++++++++- ui/src/pages/AgentNewChatPage.tsx | 4 + ui/src/pages/AgentPage.tsx | 12 --- ui/src/pages/AgentTemplatesPage.tsx | 7 +- ui/src/pages/DashboardPage.tsx | 2 +- ui/src/pages/McpServersPage.tsx | 4 +- ui/src/pages/ModelsPage.tsx | 4 +- ui/src/pages/PromptsPage.tsx | 7 +- ui/vite.config.ts | 34 +++++-- 26 files changed, 425 insertions(+), 196 deletions(-) create mode 100644 ui/src/components/chat/stableJson.test.ts create mode 100644 ui/src/components/chat/stableJson.ts diff --git a/ui/.env.example b/ui/.env.example index 69b752691..34c741d3a 100644 --- a/ui/.env.example +++ b/ui/.env.example @@ -35,9 +35,29 @@ # ENABLE_MOCK_UI=true # Where the browser calls the API. Relative by default, which the dev server -# proxies to KAGENT_DEV_CONTROLLER_URL (default http://127.0.0.1:8083). +# proxies to KAGENT_DEV_CONTROLLER_URL. # API_BASE_URL=/api +# Where the dev server forwards those calls. Read by `vite.config.ts` rather than by +# the application, so it never reaches the browser. +# +# Unset, it goes to the controller on 8083, which needs its own port-forward: +# +# kubectl -n kagent port-forward svc/kagent-controller 8083:8083 +# +# Set to 8080 instead and it goes through the UI pod's nginx, which proxies `/api` and +# `/a2a` on to the controller from inside the cluster. That needs no second forward, +# because `dev-scripts/setup-cluster.sh` ends by holding this one open — and it puts +# the dev server on the same proxy hop a deployment takes rather than around it. +# +# Either way, when the forward it points at closes — Ctrl-C, a dropped connection, the +# script exiting — the page stops answering and the dev server logs `ECONNREFUSED` for +# that port. The UI one comes back with: +# +# kubectl -n kagent port-forward svc/kagent-ui 8080:8080 +# +# KAGENT_DEV_CONTROLLER_URL=http://127.0.0.1:8080 + # Chat stream inactivity timeout, in milliseconds. # STREAM_TIMEOUT_MS=1800000 diff --git a/ui/.gitignore b/ui/.gitignore index 4ee1d16ce..4c21954dc 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -37,3 +37,8 @@ yarn-error.log* # The saved sign-in for the live suite. A real credential — never committed. playwright/.auth/ + +# Leftovers from the old Next.js UI, which this app replaced. +/.next +/next-env.d.ts +/playwright/test-results diff --git a/ui/playwright/DEFERRED.md b/ui/playwright/DEFERRED.md index c1c1fc5b8..7b3ecf6aa 100644 --- a/ui/playwright/DEFERRED.md +++ b/ui/playwright/DEFERRED.md @@ -255,15 +255,14 @@ case-insensitive substring `filter` over the fields the row displays, and a sort enum whose every order ends in a unique column so a page token names exactly one row. The commentary in that file is worth reading before adding a fifth variant of it. -**Until then the pages are client-side and say so on the page**, naming the RPC -(`models-read-note`, `mcp-servers-read-note`, `prompts-read-note`), and -`tests/lists/list-filters.spec.ts` asserts that they do. That is defensible only while -the response is the whole list. **The moment any of these three RPCs starts paging, its -page must lose its client-side search and sort in the same change** — a filter over a -page reports "no matches" about a row on page nine, which is the defect the substrate -page was rewritten to remove. `substrate.spec.ts`'s "the paged tables do not pretend to -sort, and the inline ones do" is the assertion that draws the line; the last step of -`list-filters.spec.ts` keeps these pages on the correct side of it. +**Until then the pages narrow in the browser, which is defensible only while the +response is the whole list.** The pages used to say so in a note under the table naming +the RPC; that note was removed as commentary a reader has no use for, so this file is +now the only place the reasoning is written down. **The moment any of these three RPCs +starts paging, its page must lose its client-side search and sort in the same change** — +a filter over a page reports "no matches" about a row on page nine, which is the defect +the substrate page was rewritten to remove. `substrate.spec.ts`'s "the paged tables do +not pretend to sort, and the inline ones do" is the assertion that draws the line. The prompts page is a partial exception worth not losing: `ListPromptTemplates` takes a namespace, so `usePrompts` fans out one call per namespace and its **namespace filter is diff --git a/ui/playwright/tests/agent-templates/agent-templates.spec.ts b/ui/playwright/tests/agent-templates/agent-templates.spec.ts index 47fac3b96..fe6046487 100644 --- a/ui/playwright/tests/agent-templates/agent-templates.spec.ts +++ b/ui/playwright/tests/agent-templates/agent-templates.spec.ts @@ -341,20 +341,9 @@ test("agent templates: the list narrows like every other landing page", async ({ await expect(page.getByTestId("templates-filters-search")).toHaveValue("note-taker"); }); - await test.step("4. the page says where its narrowing happens", async () => { - // `ListAgentTemplates` takes no page, sort or search parameter, so this narrowing is - // the browser's. Saying so is what stops a reader assuming a search box searched the - // cluster — the defect the substrate page was fixed for. + await test.step("4. columns sort", async () => { + // The search is cleared first: sorting one row proves nothing. await page.getByTestId("templates-filters-search").fill(""); - await expect(page.getByTestId("templates-read-note")).toContainText( - "ListAgentTemplates", - ); - await expect(page.getByTestId("templates-read-note")).toContainText( - "refuses an empty one", - ); - }); - - await test.step("5. columns sort", async () => { const first = async () => (await dataRows(page).first().textContent()) ?? ""; const before = await first(); await page.getByRole("columnheader", { name: /Template/ }).click(); diff --git a/ui/playwright/tests/agents/agent-conversations.spec.ts b/ui/playwright/tests/agents/agent-conversations.spec.ts index 6d1532c47..d44fd0665 100644 --- a/ui/playwright/tests/agents/agent-conversations.spec.ts +++ b/ui/playwright/tests/agents/agent-conversations.spec.ts @@ -78,15 +78,6 @@ test("agents: one agent lists its own conversations, and only its own", async ({ "Drafting the runbook", ); }); - - await test.step("5. and the page says the narrowing was the server's", async () => { - // Because it decides whether "no conversations match" is true. This list is - // paged; a browser-side filter over one page would report an empty agent that - // has forty conversations. - await expect(page.getByTestId("conversations-read-note")).toContainText( - "ListAgentInstances narrows to this agent on the server", - ); - }); }); test("agents: a conversation is named by the reader, and never renders as a bare UUID", async ({ @@ -392,7 +383,32 @@ test("agents: a conversation is created by its first message, not by the click", ); }); - await test.step("5. and the agent's list is the proof, with one more row", async () => { + await test.step("5. and the message is not left behind for a reload to send again", async () => { + /* + * Reported as a defect: reloading a conversation just started sent its opening + * message a second time — a whole extra turn, from a page the reader only asked + * to redraw. + * + * The cause is that router state is not in memory. `AgentNewChatPage` hands the + * text over in `location.state`, which the browser keeps in the session history + * entry, so it came back with the page and the effect that sends it fired again. + * The page clears it once the turn is under way. + * + * Asserted against the history entry rather than by reloading, because the + * fixture backend keeps its writes in the page's own memory: a reload starts a + * backend that has never heard of this conversation, so there would be no + * transcript to count a second message in. The whole entry is searched rather + * than a router-specific field, so this keeps holding if the router changes where + * it files state. + */ + const entry = await page.evaluate(() => JSON.stringify(window.history.state ?? null)); + expect( + entry, + "the opening message must not survive in the history entry", + ).not.toContain("crashlooping"); + }); + + await test.step("6. and the agent's list is the proof, with one more row", async () => { // Back through the rail rather than by reloading: the fixture backend keeps writes // in the page's own memory, so a full page load would start a backend that has // never heard of this conversation. @@ -403,7 +419,7 @@ test("agents: a conversation is created by its first message, not by the click", await expect(dataRows(page)).toHaveCount(before + 1, { timeout: 30_000 }); }); - await test.step("6. an agent with no ready revision cannot start one, and says why", async () => { + await test.step("7. an agent with no ready revision cannot start one, and says why", async () => { await loadPage(page, agentPage(agents.preparing)); await expectSettled(page); diff --git a/ui/playwright/tests/chat/chat-errors.spec.ts b/ui/playwright/tests/chat/chat-errors.spec.ts index 58cb0a033..e0b5d670f 100644 --- a/ui/playwright/tests/chat/chat-errors.spec.ts +++ b/ui/playwright/tests/chat/chat-errors.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from "../../fixtures/test"; -import { agentChat, instances, loadPage, withScenario } from "../../helpers/app"; +import { + agentChat, + agentNewChat, + agents, + instances, + loadPage, + withScenario, +} from "../../helpers/app"; /** * Chat — the failure journeys. @@ -272,3 +279,83 @@ test("chat: a question can be discarded instead of answered", async ({ page }) = await expect(page.getByTestId("chat-turn-error")).toHaveCount(0); }); }); + +/** + * Where the caret is, which decides whether answering needs the mouse. + * + * Reported as three separate irritations with one shape: the page knows exactly which + * box the reader is about to type in, and made them click it first. A parked question + * with a single prose field is the clearest case — the turn cannot end until something + * is typed there, and there is nothing else on the page to type in. + * + * Only the single-field shape. Two questions, or any question with choices, and there + * is no one field the caret obviously belongs in; taking it would be the page choosing + * which question gets answered first. + */ +test("chat: a question with one prose field takes the caret, and Enter answers it", async ({ + page, +}) => { + await test.step("1. a turn parks on a single prose question", async () => { + await page.goto(`${AGENT_CHAT}?chat=asks-text`); + await page.getByTestId("chat-input").fill("Order me a pizza"); + await page.getByTestId("chat-send").click(); + + await expect(page.getByTestId("chat-awaiting-reply")).toBeVisible({ timeout: 20_000 }); + // One question, and no choices — the shape the rest of this test is about. + await expect(page.getByTestId("chat-question")).toHaveCount(1); + await expect(page.getByTestId("chat-choices-0")).toHaveCount(0); + }); + + await test.step("2. the field has the caret already", async () => { + await expect(page.getByTestId("chat-answer-text-0")).toBeFocused(); + }); + + await test.step("3. Enter sends it, without reaching for the button", async () => { + // Typed with the keyboard rather than filled, because what is under test is that + // the caret was already in the right place — `fill` would put it there itself and + // pass whether or not step 2 held. + await page.keyboard.type("Extra napkins"); + await page.keyboard.press("Enter"); + + // The structured answer arrived, which is the same proof the choices journey + // above uses: the fixture says "I did not catch a choice in that" when the + // metadata is missing or its correlation id is wrong. + await expect(page.getByTestId("chat-message").last()).toContainText( + "Noted: Extra napkins", + { timeout: 20_000 }, + ); + await expect(page.getByTestId("chat-awaiting-reply")).toHaveCount(0); + }); + + await test.step("4. and the caret comes back to the composer", async () => { + // The field it was in is gone with the question, so a caret left there is a caret + // nowhere — and the next thing typed is an ordinary message. + await expect(page.getByTestId("chat-input")).toBeFocused(); + }); +}); + +test("chat: opening a conversation puts the caret in its box", async ({ page }) => { + /* + * Both ways in, because they reach the composer differently and only one of them + * could be done declaratively. + * + * The new-conversation page is two lines of text and one box, so `autoFocus` on + * mount is the whole of it. An existing conversation mounts its composer *disabled* — + * `canSend` is read from an instance still being fetched — so a focus on mount is a + * focus that never happens, and the page waits for the state that enables the box. + * That difference is why the second step is not a duplicate of the first. + */ + await test.step("1. a conversation that does not exist yet", async () => { + await loadPage(page, agentNewChat(agents.k8s)); + await expect(page.getByTestId("new-chat-composer")).toBeVisible(); + await expect(page.getByTestId("chat-input")).toBeFocused(); + }); + + await test.step("2. and one opened from the rail, whose box starts disabled", async () => { + await loadPage(page, AGENT_CHAT); + // Waited for rather than assumed: this is the transition the effect exists for, + // and asserting focus before it would pass on the wrong thing. + await expect(page.getByTestId("chat-input")).toBeEnabled({ timeout: 30_000 }); + await expect(page.getByTestId("chat-input")).toBeFocused(); + }); +}); diff --git a/ui/playwright/tests/chat/chat.spec.ts b/ui/playwright/tests/chat/chat.spec.ts index 82fdb5ead..050274083 100644 --- a/ui/playwright/tests/chat/chat.spec.ts +++ b/ui/playwright/tests/chat/chat.spec.ts @@ -85,6 +85,22 @@ test("chat: history, sending, streaming, and tool rendering", async ({ page }) = await expect(call).toHaveCount(1); await expect(call).toHaveAttribute("data-tool-name", "k8s_get_events"); + /* + * Reported as a defect: the payload's properties changed places while the reader + * was reading them, because the transcript is re-read while a turn is live and + * these args arrive as a protobuf `Struct` — no field order at all, so a Go map + * gave a different one every time. Printed in a fixed order instead. + * + * The fixture supplies `resource` before `namespace` precisely so this asserts + * the sort rather than the order it was handed. + */ + const payload = call.getByTestId("chat-tool-payload"); + const printed = (await payload.textContent()) ?? ""; + expect( + printed.indexOf('"namespace"'), + "a tool's payload should read in a fixed order, whatever order it arrived in", + ).toBeLessThan(printed.indexOf('"resource"')); + const result = page.getByTestId("chat-tool-result"); await expect(result).toHaveCount(1); await expect(result).toContainText("liveness probe failed"); diff --git a/ui/playwright/tests/lists/list-filters.spec.ts b/ui/playwright/tests/lists/list-filters.spec.ts index bd81ef432..7cf236b17 100644 --- a/ui/playwright/tests/lists/list-filters.spec.ts +++ b/ui/playwright/tests/lists/list-filters.spec.ts @@ -221,64 +221,6 @@ test("lists: a narrowed view is an address, so it survives a reload", async ({ p }); }); -test("lists: each page says where its narrowing happens, and names the RPC", async ({ - page, -}) => { - /* - * The honesty requirement, asserted rather than trusted. A search box and a sort - * arrow look identical whether the server did the work or the browser did, and the - * difference decides whether "no matches" is true. These three reads return the - * whole list, so the browser can answer completely — and the page says so, naming - * the RPC, so the claim can be checked against the proto rather than believed. - */ - await test.step("1. models names ListModelConfigs", async () => { - await loadPage(page, routes.models, { title: "Models" }); - await expect(page.getByTestId("models-read-note")).toContainText( - "ListModelConfigs", - ); - await expect(page.getByTestId("models-read-note")).toContainText( - "takes no page, sort or search parameter", - ); - }); - - await test.step("2. MCP servers names ListToolServers", async () => { - await loadPage(page, routes.mcpServers, { title: "MCP servers" }); - await expect(page.getByTestId("mcp-servers-read-note")).toContainText( - "ListToolServers", - ); - }); - - await test.step("3. prompts names its one genuinely server-side filter", async () => { - // Not the same claim as the other two. `ListPromptTemplates` takes a namespace and - // rejects a request without one, so the namespace filter here really is sent to - // the server — one read per namespace chosen — while the search and sort are not. - // Saying "everything is client-side" would be as wrong as saying the opposite. - await loadPage(page, routes.prompts, { title: "Prompts" }); - const note = page.getByTestId("prompts-read-note"); - await expect(note).toContainText("ListPromptTemplates"); - await expect(note).toContainText("the request carries a namespace"); - }); - - await test.step("4. the substrate page still makes the opposite claim, correctly", async () => { - // The contrast is the point, and it is worth pinning that this work did not blur - // it: those tables are paged by the server, so they offer no sort at all and say - // what order the server applied. If a later change gave them a client-side sorter - // to match these pages, this step is what would object. - await loadPage(page, routes.substrate, { title: "Substrate" }); - await expectSettled(page); - - const headers = page.getByTestId("substrate-actors-table").locator("th"); - await expect(headers.first()).toBeVisible(); - const sortable = await headers.evaluateAll( - (cells) => - cells.filter((cell) => cell.className.includes("column-has-sorters")).length, - ); - expect(sortable, "a server-paged table must not offer a sort it cannot honour").toBe( - 0, - ); - }); -}); - test("lists: prompts asks the server for exactly the namespaces chosen", async ({ page, }) => { diff --git a/ui/src/api/chat/mockChatClient.ts b/ui/src/api/chat/mockChatClient.ts index ac61a4d2f..bc84ed831 100644 --- a/ui/src/api/chat/mockChatClient.ts +++ b/ui/src/api/chat/mockChatClient.ts @@ -34,6 +34,7 @@ const TIMING = { slow: { step: 1_200, word: 400 }, error: { step: 300, word: 45 }, asks: { step: 300, word: 45 }, + "asks-text": { step: 300, word: 45 }, } as const; /** @@ -47,6 +48,8 @@ const QUESTION = "Which pizza toppings would you like? You can choose more than const TOPPINGS = ["Pepperoni", "Mushroom", "Pineapple"]; const SIZE_QUESTION = "What size pizza would you like?"; const SIZES = ["Small", "Medium", "Large"]; +/** The one-field variant: no choices offered, so the agent wants prose. */ +const NOTE_QUESTION = "What should I put on the order note?"; /** The correlation id, which a real answer echoes verbatim. */ const REQUEST_ID = "adk-mock-ask-1"; @@ -279,7 +282,7 @@ export class MockChatClient implements ChatClient { // is not something a backend would have recorded as the turn's result. this.persist(sessionId); - if (scenario === "asks") { + if (scenario === "asks" || scenario === "asks-text") { /* * The turn ends by asking rather than by finishing. * @@ -290,10 +293,13 @@ export class MockChatClient implements ChatClient { * nothing tells them the conversation is now stuck — which is precisely why * it read as an agent that had simply broken. */ - const questions = [ - { question: SIZE_QUESTION, choices: SIZES, multiple: false }, - { question: QUESTION, choices: TOPPINGS, multiple: true }, - ]; + const questions = + scenario === "asks-text" + ? [{ question: NOTE_QUESTION, choices: [], multiple: false }] + : [ + { question: SIZE_QUESTION, choices: SIZES, multiple: false }, + { question: QUESTION, choices: TOPPINGS, multiple: true }, + ]; const call = dataMessage(`${taskId}-ask-call`, taskId, "tool_call", { id: `call-ask-${taskId}`, @@ -306,7 +312,12 @@ export class MockChatClient implements ChatClient { // The question also arrives as prose, exactly as it does on the wire — the // structured payload is *beside* it rather than instead of it, so a reader // whose build cannot render the choices still sees what was asked. - const asked = message(`${taskId}-ask`, "agent", SIZE_QUESTION, taskId); + const asked = message( + `${taskId}-ask`, + "agent", + scenario === "asks-text" ? NOTE_QUESTION : SIZE_QUESTION, + taskId, + ); transcript.push(asked); this.persist(sessionId); const request: PendingRequest = { @@ -460,7 +471,11 @@ const SEEDED_TRANSCRIPTS: Record ChatMessage[]> = { dataMessage("seed-1-call", "seed-task-1", "tool_call", { id: "call-seed-1", name: "k8s_get_events", - args: { namespace: "shop", resource: "deployment/checkout" }, + // Out of alphabetical order on purpose. These args reach the app as a protobuf + // `Struct`, whose field order is whatever the sender emitted — for a Go map, a + // different order every marshal. A fixture in tidy order would let a renderer + // that prints the wire order look correct while the real page reshuffled. + args: { resource: "deployment/checkout", namespace: "shop" }, }), dataMessage("seed-1-result", "seed-task-1", "tool_result", { id: "call-seed-1", diff --git a/ui/src/components/chat/AskUserPrompt.tsx b/ui/src/components/chat/AskUserPrompt.tsx index ecc972be4..468d057a6 100644 --- a/ui/src/components/chat/AskUserPrompt.tsx +++ b/ui/src/components/chat/AskUserPrompt.tsx @@ -31,12 +31,21 @@ export function AskUserPrompt({ isBusy, onAnswer, onDismiss, + onAnswered, }: { request: PendingRequest; /** A turn is in flight — the answer is on its way, or something else is. */ isBusy: boolean; onAnswer: (answers: readonly string[][]) => void; onDismiss: () => void; + /** + * The answer has gone, and the caret belongs somewhere else now. + * + * This field exists only because the composer could not end the turn; once it has, + * the next thing typed is an ordinary message. Leaving the caret in a field that is + * now disabled makes the reader find the composer with the mouse to carry on. + */ + onAnswered?: () => void; }) { const theme = useTheme(); @@ -60,6 +69,23 @@ export function AskUserPrompt({ const setAnswer = (index: number, value: string[]) => setAnswers((current) => current.map((entry, at) => (at === index ? value : entry))); + /* + * One question, answered in prose. + * + * The case where this panel is a single text field, which is the only shape where + * Enter can mean "send": with two questions it would send the one still being + * filled in, and against choices there is nothing to press Enter in. + */ + const isSoleTextQuestion = + questions.length === 1 && questions[0].choices.length === 0; + + function submit() { + if (!answered || isSent || isBusy) return; + setSent(true); + onAnswer(answers); + onAnswered?.(); + } + const discard = ( diff --git a/ui/src/components/chat/ChatComposer.tsx b/ui/src/components/chat/ChatComposer.tsx index 3ed04c97b..e351bfde5 100644 --- a/ui/src/components/chat/ChatComposer.tsx +++ b/ui/src/components/chat/ChatComposer.tsx @@ -1,9 +1,13 @@ -import { useState } from "react"; +import { useImperativeHandle, useRef, useState, type Ref } from "react"; import { Button, Input, Space } from "antd"; +import type { TextAreaRef } from "antd/es/input/TextArea"; import { useTheme } from "@emotion/react"; import { Send, Square } from "lucide-react"; import type { ChatController } from "@/api"; +/** What a page can ask of the box from outside it: put the caret back in it. */ +export type ChatComposerHandle = { focus: () => void }; + /** * The message box. * @@ -25,6 +29,8 @@ export function ChatComposer({ onCancel, disabled = false, variant = "docked", + autoFocus = false, + ref, }: { send: (text: string) => Promise; isStreaming?: boolean; @@ -54,9 +60,29 @@ export function ChatComposer({ * instead of cutting across it. */ variant?: "docked" | "inviting"; + /** + * Take the caret on arrival. + * + * For a page whose whole purpose is this box — a conversation that does not exist + * yet has nothing else to read, so a reader who has to click before typing is being + * asked to say twice that they came here to talk. + */ + autoFocus?: boolean; + /** + * A way back to the caret for whatever took it. + * + * Answering the agent's question happens in a field inside the transcript, and + * when that field is finished with, the next thing typed belongs here. Exposed as + * a handle rather than found in the DOM by the answering component, which has no + * business knowing this box exists. + */ + ref?: Ref; }) { const theme = useTheme(); const [draft, setDraft] = useState(""); + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }), []); async function submit() { const text = draft.trim(); @@ -77,6 +103,8 @@ export function ChatComposer({ }} > setDraft(event.target.value)} diff --git a/ui/src/components/chat/ChatTranscript.tsx b/ui/src/components/chat/ChatTranscript.tsx index 4815cf885..c54466d01 100644 --- a/ui/src/components/chat/ChatTranscript.tsx +++ b/ui/src/components/chat/ChatTranscript.tsx @@ -30,8 +30,16 @@ const PHASE_LABEL: Partial> = { export function ChatTranscript({ chat, sessionId, + onAnswered, }: { chat: ChatController; + /** + * An `ask_user` answer has just gone. + * + * Forwarded rather than acted on: the composer the caret belongs in afterwards is + * the page's, not this transcript's. + */ + onAnswered?: () => void; /** * The conversation being shown, handed to each message. * @@ -306,6 +314,7 @@ export function ChatTranscript({ isBusy={chat.phase === "streaming"} onAnswer={(answers) => void chat.answerQuestion(answers)} onDismiss={() => void chat.dismissQuestion()} + onAnswered={onAnswered} /> ) : null} diff --git a/ui/src/components/chat/ToolCallCard.tsx b/ui/src/components/chat/ToolCallCard.tsx index 0b32da5f2..022a46f73 100644 --- a/ui/src/components/chat/ToolCallCard.tsx +++ b/ui/src/components/chat/ToolCallCard.tsx @@ -2,6 +2,7 @@ import { Tag, Typography } from "antd"; import { useTheme } from "@emotion/react"; import { ChevronRight, Wrench } from "lucide-react"; import type { ChatDataPart } from "@/api"; +import { stableJson } from "./stableJson"; const { Text } = Typography; @@ -74,7 +75,7 @@ export function ToolCallCard({ part }: { part: ChatDataPart }) { /** Pulls the readable payload out of either shape, and notices a failure. */ function describe(part: ChatDataPart): { body: string; failed: boolean } { if (part.dataKind === "tool_call") { - return { body: JSON.stringify(part.data.args ?? {}, null, 2), failed: false }; + return { body: stableJson(part.data.args ?? {}), failed: false }; } const response = part.data.response; @@ -93,12 +94,9 @@ function describe(part: ChatDataPart): { body: string; failed: boolean } { const payload = output ?? result; return { - body: - typeof payload === "string" - ? payload - : JSON.stringify(payload ?? response, null, 2), + body: typeof payload === "string" ? payload : stableJson(payload ?? response), failed: isError === true || error !== undefined, }; } - return { body: JSON.stringify(part.data, null, 2), failed: false }; + return { body: stableJson(part.data), failed: false }; } diff --git a/ui/src/components/chat/stableJson.test.ts b/ui/src/components/chat/stableJson.test.ts new file mode 100644 index 000000000..b0d0d0dcd --- /dev/null +++ b/ui/src/components/chat/stableJson.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { stableJson } from "./stableJson"; + +describe("stableJson", () => { + it("prints the same text whatever order the keys arrived in", () => { + const one = { questions: [], header: "Approach", multiSelect: false }; + const other = { multiSelect: false, questions: [], header: "Approach" }; + + expect(stableJson(one)).toBe(stableJson(other)); + }); + + it("sorts keys at every depth", () => { + const payload = { b: 1, a: { d: 2, c: 3 } }; + + expect(stableJson(payload)).toBe('{\n "a": {\n "c": 3,\n "d": 2\n },\n "b": 1\n}'); + }); + + it("leaves array order alone, because there the order is the data", () => { + const payload = { questions: [{ header: "second" }, { header: "first" }] }; + + expect(stableJson(payload)).toContain('"second"'); + expect(stableJson(payload).indexOf('"second"')).toBeLessThan( + stableJson(payload).indexOf('"first"'), + ); + }); + + it("passes through values that have no keys to sort", () => { + expect(stableJson("text")).toBe('"text"'); + expect(stableJson(null)).toBe("null"); + expect(stableJson(7)).toBe("7"); + }); + + it("keeps a null inside an object rather than treating it as a nested object", () => { + expect(stableJson({ b: null, a: 1 })).toBe('{\n "a": 1,\n "b": null\n}'); + }); +}); diff --git a/ui/src/components/chat/stableJson.ts b/ui/src/components/chat/stableJson.ts new file mode 100644 index 000000000..f4f979232 --- /dev/null +++ b/ui/src/components/chat/stableJson.ts @@ -0,0 +1,28 @@ +/** + * JSON for display, with object keys in a fixed order. + * + * A tool's payload reaches us as a protobuf `Struct` flattened to plain JSON, and + * the order of a `Struct`'s fields is whatever the sender happened to emit — for a + * Go map, a different order on every marshal. Printed as-is, a payload that has + * not changed at all reshuffles its lines each time the transcript is fetched, so + * the value someone was reading moves out from under them mid-read. + * + * Sorting is only about the printed form: nothing downstream reads this string, so + * a fixed order costs nothing and makes the same payload render the same way every + * time. Array order is left alone — there it is the data, not an accident. + */ +export function stableJson(value: unknown): string { + return JSON.stringify(withSortedKeys(value), null, 2); +} + +function withSortedKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(withSortedKeys); + if (value === null || typeof value !== "object") return value; + + const record = value as Record; + return Object.fromEntries( + Object.keys(record) + .sort() + .map((key) => [key, withSortedKeys(record[key])]), + ); +} diff --git a/ui/src/components/table/FilterBar.tsx b/ui/src/components/table/FilterBar.tsx index 7ef8d97b0..95b8f3eca 100644 --- a/ui/src/components/table/FilterBar.tsx +++ b/ui/src/components/table/FilterBar.tsx @@ -1,12 +1,10 @@ import type { ReactNode } from "react"; -import { Select, Tag, Typography } from "antd"; +import { Select, Tag } from "antd"; import { useTheme } from "@emotion/react"; import { X } from "lucide-react"; import { SearchInput } from "./SearchInput"; import type { ListView } from "./useListView"; -const { Text } = Typography; - /** * The controls above a list, and the row of pills that says what they are doing. * @@ -226,44 +224,3 @@ export function FilterBar({ ); } - -/** - * Where the narrowing happens, said on the page. - * - * A search box and a sort arrow look identical whether the server did the work or the - * browser did, and the difference decides whether a "no matches" is true. This page's - * reads return the whole list in one message — the RPC named here takes no page, - * filter or sort parameter — so narrowing in the browser searches everything there is, - * and the answer is complete. - * - * That is the opposite of the substrate page's actor and worker tables, which are - * paged by the server: there, a local filter would search one page out of hundreds of - * thousands and report "no matches" about a row on page nine. Those tables offer no - * sort at all for exactly that reason. The distinction is not a detail of style; it is - * the difference between a control that tells the truth and one that does not. - */ -export function WholeListNote({ - rpc, - testId, - children, -}: { - /** The RPC the page reads, named so the gap can be looked up rather than trusted. */ - rpc: string; - testId: string; - /** Anything this page narrows on the server after all, said in the same breath. */ - children?: ReactNode; -}) { - const theme = useTheme(); - - return ( - - {rpc} takes no page, sort or search parameter, so the whole list is read and - searching and sorting here cover every row rather than just this page. - {children ? " " : null} - {children} - - ); -} diff --git a/ui/src/mocks/scenario.ts b/ui/src/mocks/scenario.ts index 09f071786..14d82de7b 100644 --- a/ui/src/mocks/scenario.ts +++ b/ui/src/mocks/scenario.ts @@ -87,8 +87,13 @@ function isScenario(value: string | null): value is MockScenario { * controller refuse every further message until it is answered or given up. It is * its own value rather than a variation of `ok` because the state persists in the * conversation: a reload lands back in it, which is the case worth driving. + * + * `asks-text` is the same parked turn asking a different shape of question, and it is + * a scenario of its own because the shape decides the controls: `asks` offers choices, + * `asks-text` offers one prose field — the only shape where the field can take the + * caret on arrival and Enter can mean "send". */ -export const CHAT_SCENARIOS = ["ok", "error", "slow", "asks"] as const; +export const CHAT_SCENARIOS = ["ok", "error", "slow", "asks", "asks-text"] as const; export type ChatScenario = (typeof CHAT_SCENARIOS)[number]; diff --git a/ui/src/pages/AgentChatPage.tsx b/ui/src/pages/AgentChatPage.tsx index 001c1d42d..bddfa151e 100644 --- a/ui/src/pages/AgentChatPage.tsx +++ b/ui/src/pages/AgentChatPage.tsx @@ -3,7 +3,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom"; import { Alert, Button, Tooltip } from "antd"; import { FileText, PanelRightClose, PanelRightOpen, Share2 } from "lucide-react"; import { useTheme } from "@emotion/react"; -import { ChatComposer } from "@/components/chat/ChatComposer"; +import { ChatComposer, type ChatComposerHandle } from "@/components/chat/ChatComposer"; import { ShareDialog } from "@/components/chat/ShareDialog"; import { AgentRail } from "@/components/agent/AgentRail"; import { iconControlStyles } from "@/components/agent/controlStyles"; @@ -242,6 +242,30 @@ export function AgentChatPage() { }); } + /** The message box, so the caret can be handed back to it after a question. */ + const composerRef = useRef(null); + + /* + * Opening a conversation puts the caret in its box. + * + * Not `autoFocus` on the composer, which fires once when it mounts — and it mounts + * disabled, because `canSend` is read from an instance that has not been fetched + * yet. A focus that happens before the box can be typed in is a focus that does not + * happen at all, so this waits for the state that enables it. + * + * Once per conversation, tracked rather than left to fire whenever `canSend` is + * true. A conversation that comes back from suspended flips it mid-visit, and the + * caret is by then wherever the reader put it — quite possibly in the field + * answering a question, which is the one place taking it from would cost them + * typing. + */ + const focusedFor = useRef(undefined); + useEffect(() => { + if (!id || !canSend || focusedFor.current === id) return; + focusedFor.current = id; + composerRef.current?.focus(); + }, [id, canSend]); + /* * The message this conversation was created for, sent once on arrival. * @@ -284,6 +308,16 @@ export function AgentChatPage() { * after the turn is under way. */ void chat.send(pending); + /* + * And now cleared, because `location.state` is kept in the browser's session + * history rather than in memory: it survives a refresh, so a reader who reloaded + * a conversation they had just started watched its opening message be sent all + * over again — a second turn, from a page they only asked to redraw. + * + * A `replace` to the same address, which leaves the transcript alone: the read + * above is keyed on the conversation, and that has not changed. + */ + navigate(`${location.pathname}${location.search}`, { replace: true, state: null }); // Keyed on the conversation, not on `chat`: the controller is rebuilt every render // and depending on it would re-run this on each one. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -519,7 +553,15 @@ export function AgentChatPage() { {/* The transcript owns its own scrolling now, so this passes it the room to do it in and nothing else. */} - + composerRef.current?.focus()} + /> {/* Stuck to the foot of the viewport rather than the foot of the page. The page is what scrolls, so a composer in normal flow would be below the @@ -540,6 +582,7 @@ export function AgentChatPage() { }} > diff --git a/ui/src/pages/AgentPage.tsx b/ui/src/pages/AgentPage.tsx index e5ec13c7a..d3cd3274e 100644 --- a/ui/src/pages/AgentPage.tsx +++ b/ui/src/pages/AgentPage.tsx @@ -685,18 +685,6 @@ export function AgentPage() { }, })} /> - - - ListAgentInstances narrows to this agent on the server: it takes the - template and the harness and resolves them through each conversation’s - prepared revision, so this is the agent’s conversations rather than the - namespace’s filtered afterwards. It is paged, and every page is followed - before anything is rendered — so searching and sorting here cover every - conversation with this agent, not just the first page of them. - {/* diff --git a/ui/src/pages/AgentTemplatesPage.tsx b/ui/src/pages/AgentTemplatesPage.tsx index d73149745..ccdb65ee3 100644 --- a/ui/src/pages/AgentTemplatesPage.tsx +++ b/ui/src/pages/AgentTemplatesPage.tsx @@ -5,7 +5,7 @@ import type { ColumnsType } from "antd/es/table"; import { RefreshButton } from "@/components/table/RefreshButton"; import { useTheme } from "@emotion/react"; import { ChevronRight } from "lucide-react"; -import { FilterBar, WholeListNote } from "@/components/table/FilterBar"; +import { FilterBar } from "@/components/table/FilterBar"; import { useListView } from "@/components/table/useListView"; import { listTableChange, matchesQuery, paginationFor } from "@/components/table/listTable"; import { DeleteResourceButton } from "@/components/table/DeleteResourceButton"; @@ -342,11 +342,6 @@ export function AgentTemplatesTab() { }, })} /> - - It is read one namespace at a time, because the service validates its namespace - first and refuses an empty one rather than treating it as a wildcard. Any - namespace that refuses is named above. - ); } diff --git a/ui/src/pages/DashboardPage.tsx b/ui/src/pages/DashboardPage.tsx index 0dc1c956d..c3afff5bf 100644 --- a/ui/src/pages/DashboardPage.tsx +++ b/ui/src/pages/DashboardPage.tsx @@ -171,7 +171,7 @@ export function DashboardPage() { /> - + {agents.error ? ( - - ); diff --git a/ui/src/pages/ModelsPage.tsx b/ui/src/pages/ModelsPage.tsx index b410e81d5..71e36ff75 100644 --- a/ui/src/pages/ModelsPage.tsx +++ b/ui/src/pages/ModelsPage.tsx @@ -9,7 +9,7 @@ import { buildPath, paths } from "@/router/routes"; import { apiClient, parseRef, useModels, type ModelConfig } from "@/api"; import { DeleteResourceButton } from "@/components/table/DeleteResourceButton"; import { RefreshButton } from "@/components/table/RefreshButton"; -import { FilterBar, WholeListNote } from "@/components/table/FilterBar"; +import { FilterBar } from "@/components/table/FilterBar"; import { useListView } from "@/components/table/useListView"; import { byText, @@ -264,8 +264,6 @@ export function ModelsPage() { : " ", }} /> - - ); diff --git a/ui/src/pages/PromptsPage.tsx b/ui/src/pages/PromptsPage.tsx index 3914be6bc..60a437a08 100644 --- a/ui/src/pages/PromptsPage.tsx +++ b/ui/src/pages/PromptsPage.tsx @@ -9,7 +9,7 @@ import { buildPath, paths } from "@/router/routes"; import { apiClient, useNamespaces, usePrompts, type PromptTemplateSummary } from "@/api"; import { DeleteResourceButton } from "@/components/table/DeleteResourceButton"; import { RefreshButton } from "@/components/table/RefreshButton"; -import { FilterBar, WholeListNote } from "@/components/table/FilterBar"; +import { FilterBar } from "@/components/table/FilterBar"; import { useListView } from "@/components/table/useListView"; import { byNumber, @@ -268,11 +268,6 @@ export function PromptsPage() { : " ", }} /> - - - The namespace filter is the exception: the request carries a namespace, so - choosing namespaces asks the server for exactly those and nothing else. - ); diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 88bf6c21c..f0af12450 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -4,9 +4,32 @@ import react from "@vitejs/plugin-react"; import path from "node:path"; import { CORE_ENV_KEYS, ENV_DEFAULTS } from "./src/env.ts"; -/** Where the controller listens during local development. */ -const CONTROLLER_URL = - process.env.KAGENT_DEV_CONTROLLER_URL ?? "http://127.0.0.1:8083"; +/** + * Where the dev server forwards `/api` and `/a2a`. + * + * Read from `.env` as well as the shell, which takes a `loadEnv` rather than a + * `process.env` lookup: Vite does not put `.env` values on `process.env`, so a + * setting read only from there is one a reader can put in their `.env` and watch do + * nothing. `.env.example` ships this pointed at the UI pod's nginx on 8080, which is + * the forward `dev-scripts/setup-cluster.sh` already holds open. + * + * The shell still wins, for the same reason it wins over the inlined settings below: + * a one-off override on the command line should not mean editing a file. + * + * The fallback is the controller's own port, for a checkout with no `.env` at all. + */ +function devProxy(mode: string) { + const fromFiles = loadEnv(mode, import.meta.dirname, ""); + const target = + process.env.KAGENT_DEV_CONTROLLER_URL || + fromFiles.KAGENT_DEV_CONTROLLER_URL || + "http://127.0.0.1:8083"; + + return { + "/api": { target, changeOrigin: true }, + "/a2a": { target, changeOrigin: true }, + }; +} /** * Extra keys the dev server will pass through beyond the application's own. @@ -113,10 +136,7 @@ export default defineConfig(({ mode }) => ({ // talking to a real controller uses the same relative URLs as production. // Requests are only proxied in live mode; the mock worker intercepts first // otherwise, so these rules are inert by default. - proxy: { - "/api": { target: CONTROLLER_URL, changeOrigin: true }, - "/a2a": { target: CONTROLLER_URL, changeOrigin: true }, - }, + proxy: devProxy(mode), }, preview: { port: Number(process.env.UI_LOOP_PORT ?? 8001), From d03656fd4dfa5d3632c603b8ad4071edb5810d50 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Wed, 26 Aug 2026 14:42:41 +0000 Subject: [PATCH 03/25] refactor: simplify UI backend support Signed-off-by: Eitan Yarmush --- go/api/database/client.go | 28 - .../kagent/api/v1alpha1/agent_instances.pb.go | 28 +- .../gen/kagent/api/v1alpha1/harnesses.pb.go | 289 +----- .../kagent/api/v1alpha1/harnesses_grpc.pb.go | 76 -- go/api/gen/kagent/api/v1alpha1/system.pb.go | 981 +----------------- .../gen/kagent/api/v1alpha1/system_grpc.pb.go | 178 +--- .../database/client_agent_instance_test.go | 175 ---- go/core/internal/database/client_postgres.go | 120 +-- .../database/gen/agent_instance_tasks.sql.go | 45 +- go/core/internal/database/gen/querier.go | 13 +- .../database/queries/agent_instance_tasks.sql | 13 - go/core/internal/grpcserver/agenttemplate.go | 4 +- .../grpcserver/agenttemplate_harness_test.go | 18 - go/core/internal/grpcserver/harness.go | 40 +- go/core/internal/grpcserver/policy.go | 5 - .../internal/grpcserver/protovalidate_test.go | 27 + go/core/internal/grpcserver/system.go | 243 +---- .../internal/service/agenttemplate/service.go | 167 +-- go/core/internal/service/harness/service.go | 170 +-- .../internal/service/harness/service_test.go | 45 +- go/core/internal/service/kubecrud/service.go | 185 ++++ go/core/internal/service/system/service.go | 39 +- .../internal/service/system/service_test.go | 19 - go/core/internal/service/system/substrate.go | 720 ------------- .../internal/service/system/substrate_test.go | 491 --------- .../internal/service/system/substratecache.go | 149 --- .../service/system/substratecache_test.go | 133 --- go/core/pkg/sandboxbackend/substrate/list.go | 46 - go/core/v2/a2agateway/gateway.go | 66 +- go/core/v2/a2agateway/gateway_test.go | 271 ----- go/core/v2/agentinstance/service.go | 96 -- go/core/v2/agentinstance/service_test.go | 136 --- .../kagent/api/v1alpha1/agent_instances.proto | 32 +- proto/kagent/api/v1alpha1/harnesses.proto | 19 - proto/kagent/api/v1alpha1/system.proto | 206 ---- ui/playwright/helpers/mockCalls.ts | 3 - .../tests/substrate/substrate-polling.spec.ts | 14 +- ui/src/api/grpc/operations.ts | 228 ++-- .../kagent/api/v1alpha1/agent_instances_pb.ts | 6 +- .../kagent/api/v1alpha1/harnesses_pb.ts | 99 +- .../kagent/api/v1alpha1/system_pb.ts | 524 +--------- ui/src/mocks/transport.ts | 270 +---- 42 files changed, 574 insertions(+), 5843 deletions(-) create mode 100644 go/core/internal/service/kubecrud/service.go delete mode 100644 go/core/internal/service/system/substrate.go delete mode 100644 go/core/internal/service/system/substrate_test.go delete mode 100644 go/core/internal/service/system/substratecache.go delete mode 100644 go/core/internal/service/system/substratecache_test.go diff --git a/go/api/database/client.go b/go/api/database/client.go index d388016b4..f7751c453 100644 --- a/go/api/database/client.go +++ b/go/api/database/client.go @@ -21,22 +21,6 @@ var ErrAgentInstanceConflict = errors.New("AgentInstance lifecycle operation con var ErrAgentInstanceTaskConflict = errors.New("AgentInstance already has an active task") -// TaskParkedAwaitingUser reports whether a task stopped to wait on a human -// rather than because it is being executed. Such a task is non-terminal, so it -// holds the instance's single active-task slot, but no execution is in flight: -// the runtime has asked a question (`ask_user`, a tool approval) and is waiting -// for the answer. -// -// The distinction has to live in one place because two callers act on it in -// opposite directions — a suspend must leave a parked turn alone, since the -// question is still valid and the reader may answer it after resuming, while a -// send has to report the parked turn as the reason it was refused. Getting -// either backwards destroys a pending question or hides why a conversation -// stopped answering. -func TaskParkedAwaitingUser(state a2a.TaskState) bool { - return state == a2a.TaskStateInputRequired || state == a2a.TaskStateAuthRequired -} - var ErrAgentInstanceNotQuiescent = errors.New("AgentInstance has no quiescent turn boundary") type QueryOptions struct { @@ -160,18 +144,6 @@ type Client interface { // InterruptActiveAgentInstanceTask fails the expected task and records an // interruption. It returns false if that task is no longer active. InterruptActiveAgentInstanceTask(context.Context, string, string) (bool, error) - // AbandonActiveAgentInstanceTask cancels the expected task, releasing the - // instance's active-task slot for a turn that was parked awaiting the reader. - // It returns false if that task is no longer active. - AbandonActiveAgentInstanceTask(context.Context, string, string) (bool, error) - // ClaimParkedAgentInstanceTask moves a task waiting on the reader into a - // working state so a reply can be delivered, returning the task as it was - // parked and whether this call claimed it. A second caller is refused, which - // is what stops a duplicate reply being delivered twice. - ClaimParkedAgentInstanceTask(context.Context, string, string) (*a2a.Task, bool, error) - // RestoreParkedAgentInstanceTask puts a claimed task back as it was, for a - // reply that never reached the runtime. - RestoreParkedAgentInstanceTask(context.Context, string, *a2a.Task) error StoreAgentInstanceTaskEvent(context.Context, string, *a2a.Task, a2a.Event, *AgentInstanceTaskSnapshot) error GetAgentInstanceTask(context.Context, string, string) (*a2a.Task, error) ListAgentInstanceTasks(context.Context, string, string, a2a.TaskState, *time.Time, int) ([]*a2a.Task, int, error) diff --git a/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go b/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go index 7ea004c69..b5ca6a42c 100644 --- a/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/agent_instances.pb.go @@ -398,9 +398,7 @@ type CreateAgentInstanceRequest struct { Harness string `protobuf:"bytes,2,opt,name=harness,proto3" json:"harness,omitempty"` AgentTemplate string `protobuf:"bytes,3,opt,name=agent_template,json=agentTemplate,proto3" json:"agent_template,omitempty"` RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - // Optional display name. Omit it to create an unnamed conversation. Unvalidated - // because empty is the ordinary case: a conversation is usually named later, or - // never. + // Optional display name. Empty means unnamed. Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1571,39 +1569,39 @@ const file_kagent_api_v1alpha1_agent_instances_proto_rawDesc = "" + "\x04name\x18\x0e \x01(\tR\x04name\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd5\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x94\x02\n" + "\x1aCreateAgentInstanceRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x12!\n" + "\aharness\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\aharness\x12.\n" + "\x0eagent_template\x18\x03 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\ragentTemplate\x12)\n" + "\n" + "request_id\x18\x04 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x01R\trequestId\x12\x12\n" + - "\x04name\x18\x05 \x01(\tR\x04name\"h\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\trequestId\x12Q\n" + + "\x04name\x18\x05 \x01(\tB=\xbaH:r8\x18\xc8\x0123^(?:$|[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}]*[^\\p{Z}\\p{Cc}])?)$R\x04name\"h\n" + "\x1bCreateAgentInstanceResponse\x12I\n" + "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"u\n" + "\x17GetAgentInstanceRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x123\n" + "\x11agent_instance_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x0fagentInstanceId\"e\n" + "\x18GetAgentInstanceResponse\x12I\n" + - "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"\x80\x03\n" + + "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"\xb2\x04\n" + "\x19ListAgentInstancesRequest\x12%\n" + "\tnamespace\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tnamespace\x12b\n" + "\fmatch_labels\x18\x02 \x03(\v2?.kagent.api.v1alpha1.ListAgentInstancesRequest.MatchLabelsEntryR\vmatchLabels\x12!\n" + "\fall_creators\x18\x03 \x01(\bR\vallCreators\x124\n" + - "\x04page\x18\x04 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12%\n" + - "\x0eagent_template\x18\x05 \x01(\tR\ragentTemplate\x12\x18\n" + - "\aharness\x18\x06 \x01(\tR\aharness\x1a>\n" + + "\x04page\x18\x04 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12~\n" + + "\x0eagent_template\x18\x05 \x01(\tBW\xbaHTrR\x18\xfd\x012M^(?:$|[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*)$R\ragentTemplate\x12q\n" + + "\aharness\x18\x06 \x01(\tBW\xbaHTrR\x18\xfd\x012M^(?:$|[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*)$R\aharness\x1a>\n" + "\x10MatchLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa0\x01\n" + "\x1aListAgentInstancesResponse\x12K\n" + "\x0fagent_instances\x18\x01 \x03(\v2\".kagent.api.v1alpha1.AgentInstanceR\x0eagentInstances\x125\n" + - "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\"z\n" + - "\x1aRenameAgentInstanceRequest\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12*\n" + - "\x11agent_instance_id\x18\x02 \x01(\tR\x0fagentInstanceId\x12\x12\n" + - "\x04name\x18\x03 \x01(\tR\x04name\"h\n" + + "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\"\xf1\x01\n" + + "\x1aRenameAgentInstanceRequest\x12J\n" + + "\tnamespace\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$R\tnamespace\x124\n" + + "\x11agent_instance_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0fagentInstanceId\x12Q\n" + + "\x04name\x18\x03 \x01(\tB=\xbaH:r8\x18\xc8\x0123^(?:$|[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}]*[^\\p{Z}\\p{Cc}])?)$R\x04name\"h\n" + "\x1bRenameAgentInstanceResponse\x12I\n" + "\x0eagent_instance\x18\x01 \x01(\v2\".kagent.api.v1alpha1.AgentInstanceR\ragentInstance\"y\n" + "\x1bSuspendAgentInstanceRequest\x12%\n" + diff --git a/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go b/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go index 080dc50ad..2cb2082b0 100644 --- a/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/harnesses.pb.go @@ -193,94 +193,6 @@ func (x *ListHarnessesResponse) GetHarnesses() []*Harness { return nil } -type GetHarnessRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetHarnessRequest) Reset() { - *x = GetHarnessRequest{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetHarnessRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetHarnessRequest) ProtoMessage() {} - -func (x *GetHarnessRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetHarnessRequest.ProtoReflect.Descriptor instead. -func (*GetHarnessRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{3} -} - -func (x *GetHarnessRequest) GetRef() *ResourceReference { - if x != nil { - return x.Ref - } - return nil -} - -type GetHarnessResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Harness *Harness `protobuf:"bytes,1,opt,name=harness,proto3" json:"harness,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetHarnessResponse) Reset() { - *x = GetHarnessResponse{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetHarnessResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetHarnessResponse) ProtoMessage() {} - -func (x *GetHarnessResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetHarnessResponse.ProtoReflect.Descriptor instead. -func (*GetHarnessResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{4} -} - -func (x *GetHarnessResponse) GetHarness() *Harness { - if x != nil { - return x.Harness - } - return nil -} - type CreateHarnessRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` @@ -291,7 +203,7 @@ type CreateHarnessRequest struct { func (x *CreateHarnessRequest) Reset() { *x = CreateHarnessRequest{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[5] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -303,7 +215,7 @@ func (x *CreateHarnessRequest) String() string { func (*CreateHarnessRequest) ProtoMessage() {} func (x *CreateHarnessRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[5] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -316,7 +228,7 @@ func (x *CreateHarnessRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateHarnessRequest.ProtoReflect.Descriptor instead. func (*CreateHarnessRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{5} + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{3} } func (x *CreateHarnessRequest) GetRef() *ResourceReference { @@ -342,7 +254,7 @@ type CreateHarnessResponse struct { func (x *CreateHarnessResponse) Reset() { *x = CreateHarnessResponse{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[6] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -354,7 +266,7 @@ func (x *CreateHarnessResponse) String() string { func (*CreateHarnessResponse) ProtoMessage() {} func (x *CreateHarnessResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[6] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -367,7 +279,7 @@ func (x *CreateHarnessResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateHarnessResponse.ProtoReflect.Descriptor instead. func (*CreateHarnessResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{6} + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{4} } func (x *CreateHarnessResponse) GetHarness() *Harness { @@ -377,102 +289,6 @@ func (x *CreateHarnessResponse) GetHarness() *Harness { return nil } -type UpdateHarnessRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` - Resource *StructuredObject `protobuf:"bytes,2,opt,name=resource,proto3" json:"resource,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateHarnessRequest) Reset() { - *x = UpdateHarnessRequest{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateHarnessRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateHarnessRequest) ProtoMessage() {} - -func (x *UpdateHarnessRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdateHarnessRequest.ProtoReflect.Descriptor instead. -func (*UpdateHarnessRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{7} -} - -func (x *UpdateHarnessRequest) GetRef() *ResourceReference { - if x != nil { - return x.Ref - } - return nil -} - -func (x *UpdateHarnessRequest) GetResource() *StructuredObject { - if x != nil { - return x.Resource - } - return nil -} - -type UpdateHarnessResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Harness *Harness `protobuf:"bytes,1,opt,name=harness,proto3" json:"harness,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateHarnessResponse) Reset() { - *x = UpdateHarnessResponse{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateHarnessResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateHarnessResponse) ProtoMessage() {} - -func (x *UpdateHarnessResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdateHarnessResponse.ProtoReflect.Descriptor instead. -func (*UpdateHarnessResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{8} -} - -func (x *UpdateHarnessResponse) GetHarness() *Harness { - if x != nil { - return x.Harness - } - return nil -} - type DeleteHarnessRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Ref *ResourceReference `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` @@ -482,7 +298,7 @@ type DeleteHarnessRequest struct { func (x *DeleteHarnessRequest) Reset() { *x = DeleteHarnessRequest{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[9] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -494,7 +310,7 @@ func (x *DeleteHarnessRequest) String() string { func (*DeleteHarnessRequest) ProtoMessage() {} func (x *DeleteHarnessRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[9] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -507,7 +323,7 @@ func (x *DeleteHarnessRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteHarnessRequest.ProtoReflect.Descriptor instead. func (*DeleteHarnessRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{9} + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{5} } func (x *DeleteHarnessRequest) GetRef() *ResourceReference { @@ -525,7 +341,7 @@ type DeleteHarnessResponse struct { func (x *DeleteHarnessResponse) Reset() { *x = DeleteHarnessResponse{} - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[10] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -537,7 +353,7 @@ func (x *DeleteHarnessResponse) String() string { func (*DeleteHarnessResponse) ProtoMessage() {} func (x *DeleteHarnessResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[10] + mi := &file_kagent_api_v1alpha1_harnesses_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -550,7 +366,7 @@ func (x *DeleteHarnessResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteHarnessResponse.ProtoReflect.Descriptor instead. func (*DeleteHarnessResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{10} + return file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP(), []int{6} } var File_kagent_api_v1alpha1_harnesses_proto protoreflect.FileDescriptor @@ -567,30 +383,18 @@ const file_kagent_api_v1alpha1_harnesses_proto_rawDesc = "" + "\x14ListHarnessesRequest\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\"S\n" + "\x15ListHarnessesResponse\x12:\n" + - "\tharnesses\x18\x01 \x03(\v2\x1c.kagent.api.v1alpha1.HarnessR\tharnesses\"M\n" + - "\x11GetHarnessRequest\x128\n" + - "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\"L\n" + - "\x12GetHarnessResponse\x126\n" + - "\aharness\x18\x01 \x01(\v2\x1c.kagent.api.v1alpha1.HarnessR\aharness\"\x93\x01\n" + + "\tharnesses\x18\x01 \x03(\v2\x1c.kagent.api.v1alpha1.HarnessR\tharnesses\"\x93\x01\n" + "\x14CreateHarnessRequest\x128\n" + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\"O\n" + "\x15CreateHarnessResponse\x126\n" + - "\aharness\x18\x01 \x01(\v2\x1c.kagent.api.v1alpha1.HarnessR\aharness\"\x93\x01\n" + - "\x14UpdateHarnessRequest\x128\n" + - "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\x12A\n" + - "\bresource\x18\x02 \x01(\v2%.kagent.api.v1alpha1.StructuredObjectR\bresource\"O\n" + - "\x15UpdateHarnessResponse\x126\n" + "\aharness\x18\x01 \x01(\v2\x1c.kagent.api.v1alpha1.HarnessR\aharness\"P\n" + "\x14DeleteHarnessRequest\x128\n" + "\x03ref\x18\x01 \x01(\v2&.kagent.api.v1alpha1.ResourceReferenceR\x03ref\"\x17\n" + - "\x15DeleteHarnessResponse2\x8f\x04\n" + + "\x15DeleteHarnessResponse2\xc8\x02\n" + "\x0eHarnessService\x12f\n" + - "\rListHarnesses\x12).kagent.api.v1alpha1.ListHarnessesRequest\x1a*.kagent.api.v1alpha1.ListHarnessesResponse\x12]\n" + - "\n" + - "GetHarness\x12&.kagent.api.v1alpha1.GetHarnessRequest\x1a'.kagent.api.v1alpha1.GetHarnessResponse\x12f\n" + + "\rListHarnesses\x12).kagent.api.v1alpha1.ListHarnessesRequest\x1a*.kagent.api.v1alpha1.ListHarnessesResponse\x12f\n" + "\rCreateHarness\x12).kagent.api.v1alpha1.CreateHarnessRequest\x1a*.kagent.api.v1alpha1.CreateHarnessResponse\x12f\n" + - "\rUpdateHarness\x12).kagent.api.v1alpha1.UpdateHarnessRequest\x1a*.kagent.api.v1alpha1.UpdateHarnessResponse\x12f\n" + "\rDeleteHarness\x12).kagent.api.v1alpha1.DeleteHarnessRequest\x1a*.kagent.api.v1alpha1.DeleteHarnessResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" var ( @@ -605,50 +409,37 @@ func file_kagent_api_v1alpha1_harnesses_proto_rawDescGZIP() []byte { return file_kagent_api_v1alpha1_harnesses_proto_rawDescData } -var file_kagent_api_v1alpha1_harnesses_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_kagent_api_v1alpha1_harnesses_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_kagent_api_v1alpha1_harnesses_proto_goTypes = []any{ (*Harness)(nil), // 0: kagent.api.v1alpha1.Harness (*ListHarnessesRequest)(nil), // 1: kagent.api.v1alpha1.ListHarnessesRequest (*ListHarnessesResponse)(nil), // 2: kagent.api.v1alpha1.ListHarnessesResponse - (*GetHarnessRequest)(nil), // 3: kagent.api.v1alpha1.GetHarnessRequest - (*GetHarnessResponse)(nil), // 4: kagent.api.v1alpha1.GetHarnessResponse - (*CreateHarnessRequest)(nil), // 5: kagent.api.v1alpha1.CreateHarnessRequest - (*CreateHarnessResponse)(nil), // 6: kagent.api.v1alpha1.CreateHarnessResponse - (*UpdateHarnessRequest)(nil), // 7: kagent.api.v1alpha1.UpdateHarnessRequest - (*UpdateHarnessResponse)(nil), // 8: kagent.api.v1alpha1.UpdateHarnessResponse - (*DeleteHarnessRequest)(nil), // 9: kagent.api.v1alpha1.DeleteHarnessRequest - (*DeleteHarnessResponse)(nil), // 10: kagent.api.v1alpha1.DeleteHarnessResponse - (*ResourceReference)(nil), // 11: kagent.api.v1alpha1.ResourceReference - (*StructuredObject)(nil), // 12: kagent.api.v1alpha1.StructuredObject + (*CreateHarnessRequest)(nil), // 3: kagent.api.v1alpha1.CreateHarnessRequest + (*CreateHarnessResponse)(nil), // 4: kagent.api.v1alpha1.CreateHarnessResponse + (*DeleteHarnessRequest)(nil), // 5: kagent.api.v1alpha1.DeleteHarnessRequest + (*DeleteHarnessResponse)(nil), // 6: kagent.api.v1alpha1.DeleteHarnessResponse + (*ResourceReference)(nil), // 7: kagent.api.v1alpha1.ResourceReference + (*StructuredObject)(nil), // 8: kagent.api.v1alpha1.StructuredObject } var file_kagent_api_v1alpha1_harnesses_proto_depIdxs = []int32{ - 11, // 0: kagent.api.v1alpha1.Harness.ref:type_name -> kagent.api.v1alpha1.ResourceReference - 12, // 1: kagent.api.v1alpha1.Harness.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 7, // 0: kagent.api.v1alpha1.Harness.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 8, // 1: kagent.api.v1alpha1.Harness.resource:type_name -> kagent.api.v1alpha1.StructuredObject 0, // 2: kagent.api.v1alpha1.ListHarnessesResponse.harnesses:type_name -> kagent.api.v1alpha1.Harness - 11, // 3: kagent.api.v1alpha1.GetHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference - 0, // 4: kagent.api.v1alpha1.GetHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness - 11, // 5: kagent.api.v1alpha1.CreateHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference - 12, // 6: kagent.api.v1alpha1.CreateHarnessRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject - 0, // 7: kagent.api.v1alpha1.CreateHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness - 11, // 8: kagent.api.v1alpha1.UpdateHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference - 12, // 9: kagent.api.v1alpha1.UpdateHarnessRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject - 0, // 10: kagent.api.v1alpha1.UpdateHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness - 11, // 11: kagent.api.v1alpha1.DeleteHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference - 1, // 12: kagent.api.v1alpha1.HarnessService.ListHarnesses:input_type -> kagent.api.v1alpha1.ListHarnessesRequest - 3, // 13: kagent.api.v1alpha1.HarnessService.GetHarness:input_type -> kagent.api.v1alpha1.GetHarnessRequest - 5, // 14: kagent.api.v1alpha1.HarnessService.CreateHarness:input_type -> kagent.api.v1alpha1.CreateHarnessRequest - 7, // 15: kagent.api.v1alpha1.HarnessService.UpdateHarness:input_type -> kagent.api.v1alpha1.UpdateHarnessRequest - 9, // 16: kagent.api.v1alpha1.HarnessService.DeleteHarness:input_type -> kagent.api.v1alpha1.DeleteHarnessRequest - 2, // 17: kagent.api.v1alpha1.HarnessService.ListHarnesses:output_type -> kagent.api.v1alpha1.ListHarnessesResponse - 4, // 18: kagent.api.v1alpha1.HarnessService.GetHarness:output_type -> kagent.api.v1alpha1.GetHarnessResponse - 6, // 19: kagent.api.v1alpha1.HarnessService.CreateHarness:output_type -> kagent.api.v1alpha1.CreateHarnessResponse - 8, // 20: kagent.api.v1alpha1.HarnessService.UpdateHarness:output_type -> kagent.api.v1alpha1.UpdateHarnessResponse - 10, // 21: kagent.api.v1alpha1.HarnessService.DeleteHarness:output_type -> kagent.api.v1alpha1.DeleteHarnessResponse - 17, // [17:22] is the sub-list for method output_type - 12, // [12:17] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 7, // 3: kagent.api.v1alpha1.CreateHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 8, // 4: kagent.api.v1alpha1.CreateHarnessRequest.resource:type_name -> kagent.api.v1alpha1.StructuredObject + 0, // 5: kagent.api.v1alpha1.CreateHarnessResponse.harness:type_name -> kagent.api.v1alpha1.Harness + 7, // 6: kagent.api.v1alpha1.DeleteHarnessRequest.ref:type_name -> kagent.api.v1alpha1.ResourceReference + 1, // 7: kagent.api.v1alpha1.HarnessService.ListHarnesses:input_type -> kagent.api.v1alpha1.ListHarnessesRequest + 3, // 8: kagent.api.v1alpha1.HarnessService.CreateHarness:input_type -> kagent.api.v1alpha1.CreateHarnessRequest + 5, // 9: kagent.api.v1alpha1.HarnessService.DeleteHarness:input_type -> kagent.api.v1alpha1.DeleteHarnessRequest + 2, // 10: kagent.api.v1alpha1.HarnessService.ListHarnesses:output_type -> kagent.api.v1alpha1.ListHarnessesResponse + 4, // 11: kagent.api.v1alpha1.HarnessService.CreateHarness:output_type -> kagent.api.v1alpha1.CreateHarnessResponse + 6, // 12: kagent.api.v1alpha1.HarnessService.DeleteHarness:output_type -> kagent.api.v1alpha1.DeleteHarnessResponse + 10, // [10:13] is the sub-list for method output_type + 7, // [7:10] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_kagent_api_v1alpha1_harnesses_proto_init() } @@ -663,7 +454,7 @@ func file_kagent_api_v1alpha1_harnesses_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_harnesses_proto_rawDesc), len(file_kagent_api_v1alpha1_harnesses_proto_rawDesc)), NumEnums: 0, - NumMessages: 11, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go index 1047715ce..de34653a5 100644 --- a/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/harnesses_grpc.pb.go @@ -20,9 +20,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( HarnessService_ListHarnesses_FullMethodName = "/kagent.api.v1alpha1.HarnessService/ListHarnesses" - HarnessService_GetHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/GetHarness" HarnessService_CreateHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/CreateHarness" - HarnessService_UpdateHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/UpdateHarness" HarnessService_DeleteHarness_FullMethodName = "/kagent.api.v1alpha1.HarnessService/DeleteHarness" ) @@ -44,9 +42,7 @@ const ( // so this is a separate service rather than more RPCs on AgentService. type HarnessServiceClient interface { ListHarnesses(ctx context.Context, in *ListHarnessesRequest, opts ...grpc.CallOption) (*ListHarnessesResponse, error) - GetHarness(ctx context.Context, in *GetHarnessRequest, opts ...grpc.CallOption) (*GetHarnessResponse, error) CreateHarness(ctx context.Context, in *CreateHarnessRequest, opts ...grpc.CallOption) (*CreateHarnessResponse, error) - UpdateHarness(ctx context.Context, in *UpdateHarnessRequest, opts ...grpc.CallOption) (*UpdateHarnessResponse, error) DeleteHarness(ctx context.Context, in *DeleteHarnessRequest, opts ...grpc.CallOption) (*DeleteHarnessResponse, error) } @@ -68,16 +64,6 @@ func (c *harnessServiceClient) ListHarnesses(ctx context.Context, in *ListHarnes return out, nil } -func (c *harnessServiceClient) GetHarness(ctx context.Context, in *GetHarnessRequest, opts ...grpc.CallOption) (*GetHarnessResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetHarnessResponse) - err := c.cc.Invoke(ctx, HarnessService_GetHarness_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *harnessServiceClient) CreateHarness(ctx context.Context, in *CreateHarnessRequest, opts ...grpc.CallOption) (*CreateHarnessResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreateHarnessResponse) @@ -88,16 +74,6 @@ func (c *harnessServiceClient) CreateHarness(ctx context.Context, in *CreateHarn return out, nil } -func (c *harnessServiceClient) UpdateHarness(ctx context.Context, in *UpdateHarnessRequest, opts ...grpc.CallOption) (*UpdateHarnessResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(UpdateHarnessResponse) - err := c.cc.Invoke(ctx, HarnessService_UpdateHarness_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *harnessServiceClient) DeleteHarness(ctx context.Context, in *DeleteHarnessRequest, opts ...grpc.CallOption) (*DeleteHarnessResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteHarnessResponse) @@ -126,9 +102,7 @@ func (c *harnessServiceClient) DeleteHarness(ctx context.Context, in *DeleteHarn // so this is a separate service rather than more RPCs on AgentService. type HarnessServiceServer interface { ListHarnesses(context.Context, *ListHarnessesRequest) (*ListHarnessesResponse, error) - GetHarness(context.Context, *GetHarnessRequest) (*GetHarnessResponse, error) CreateHarness(context.Context, *CreateHarnessRequest) (*CreateHarnessResponse, error) - UpdateHarness(context.Context, *UpdateHarnessRequest) (*UpdateHarnessResponse, error) DeleteHarness(context.Context, *DeleteHarnessRequest) (*DeleteHarnessResponse, error) mustEmbedUnimplementedHarnessServiceServer() } @@ -143,15 +117,9 @@ type UnimplementedHarnessServiceServer struct{} func (UnimplementedHarnessServiceServer) ListHarnesses(context.Context, *ListHarnessesRequest) (*ListHarnessesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListHarnesses not implemented") } -func (UnimplementedHarnessServiceServer) GetHarness(context.Context, *GetHarnessRequest) (*GetHarnessResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetHarness not implemented") -} func (UnimplementedHarnessServiceServer) CreateHarness(context.Context, *CreateHarnessRequest) (*CreateHarnessResponse, error) { return nil, status.Error(codes.Unimplemented, "method CreateHarness not implemented") } -func (UnimplementedHarnessServiceServer) UpdateHarness(context.Context, *UpdateHarnessRequest) (*UpdateHarnessResponse, error) { - return nil, status.Error(codes.Unimplemented, "method UpdateHarness not implemented") -} func (UnimplementedHarnessServiceServer) DeleteHarness(context.Context, *DeleteHarnessRequest) (*DeleteHarnessResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeleteHarness not implemented") } @@ -194,24 +162,6 @@ func _HarnessService_ListHarnesses_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -func _HarnessService_GetHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetHarnessRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HarnessServiceServer).GetHarness(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HarnessService_GetHarness_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HarnessServiceServer).GetHarness(ctx, req.(*GetHarnessRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _HarnessService_CreateHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateHarnessRequest) if err := dec(in); err != nil { @@ -230,24 +180,6 @@ func _HarnessService_CreateHarness_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -func _HarnessService_UpdateHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpdateHarnessRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HarnessServiceServer).UpdateHarness(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: HarnessService_UpdateHarness_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HarnessServiceServer).UpdateHarness(ctx, req.(*UpdateHarnessRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _HarnessService_DeleteHarness_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteHarnessRequest) if err := dec(in); err != nil { @@ -277,18 +209,10 @@ var HarnessService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListHarnesses", Handler: _HarnessService_ListHarnesses_Handler, }, - { - MethodName: "GetHarness", - Handler: _HarnessService_GetHarness_Handler, - }, { MethodName: "CreateHarness", Handler: _HarnessService_CreateHarness_Handler, }, - { - MethodName: "UpdateHarness", - Handler: _HarnessService_UpdateHarness_Handler, - }, { MethodName: "DeleteHarness", Handler: _HarnessService_DeleteHarness_Handler, diff --git a/go/api/gen/kagent/api/v1alpha1/system.pb.go b/go/api/gen/kagent/api/v1alpha1/system.pb.go index 1d84c6da9..ba2d169ff 100644 --- a/go/api/gen/kagent/api/v1alpha1/system.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/system.pb.go @@ -10,7 +10,6 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -23,177 +22,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// SubstrateSortOrder is the direction a paged substrate read is sorted in. -type SubstrateSortOrder int32 - -const ( - // Unspecified sorts ascending, which is what every default order below reads - // naturally in. - SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED SubstrateSortOrder = 0 - SubstrateSortOrder_SUBSTRATE_SORT_ORDER_ASCENDING SubstrateSortOrder = 1 - SubstrateSortOrder_SUBSTRATE_SORT_ORDER_DESCENDING SubstrateSortOrder = 2 -) - -// Enum value maps for SubstrateSortOrder. -var ( - SubstrateSortOrder_name = map[int32]string{ - 0: "SUBSTRATE_SORT_ORDER_UNSPECIFIED", - 1: "SUBSTRATE_SORT_ORDER_ASCENDING", - 2: "SUBSTRATE_SORT_ORDER_DESCENDING", - } - SubstrateSortOrder_value = map[string]int32{ - "SUBSTRATE_SORT_ORDER_UNSPECIFIED": 0, - "SUBSTRATE_SORT_ORDER_ASCENDING": 1, - "SUBSTRATE_SORT_ORDER_DESCENDING": 2, - } -) - -func (x SubstrateSortOrder) Enum() *SubstrateSortOrder { - p := new(SubstrateSortOrder) - *p = x - return p -} - -func (x SubstrateSortOrder) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SubstrateSortOrder) Descriptor() protoreflect.EnumDescriptor { - return file_kagent_api_v1alpha1_system_proto_enumTypes[0].Descriptor() -} - -func (SubstrateSortOrder) Type() protoreflect.EnumType { - return &file_kagent_api_v1alpha1_system_proto_enumTypes[0] -} - -func (x SubstrateSortOrder) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SubstrateSortOrder.Descriptor instead. -func (SubstrateSortOrder) EnumDescriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{0} -} - -// SubstrateActorSortField is the column ListSubstrateActors orders by. -// -// Every order ends in the actor id, which is unique — so a page token, which is -// the sort key of the last row already sent, always identifies exactly one row. -// A key that could tie would skip or repeat rows at a page boundary. -type SubstrateActorSortField int32 - -const ( - // Unspecified groups by status and orders by id within each group. That is the - // order the inventory is most usefully read in, and it is stable: ate-api - // returns actors in whatever order it holds them, so an unsorted list puts a - // different actor on every page each time it is asked. - SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED SubstrateActorSortField = 0 - SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_STATUS SubstrateActorSortField = 1 - SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID SubstrateActorSortField = 2 - SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE SubstrateActorSortField = 3 - SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD SubstrateActorSortField = 4 -) - -// Enum value maps for SubstrateActorSortField. -var ( - SubstrateActorSortField_name = map[int32]string{ - 0: "SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED", - 1: "SUBSTRATE_ACTOR_SORT_FIELD_STATUS", - 2: "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID", - 3: "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE", - 4: "SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD", - } - SubstrateActorSortField_value = map[string]int32{ - "SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED": 0, - "SUBSTRATE_ACTOR_SORT_FIELD_STATUS": 1, - "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID": 2, - "SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE": 3, - "SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD": 4, - } -) - -func (x SubstrateActorSortField) Enum() *SubstrateActorSortField { - p := new(SubstrateActorSortField) - *p = x - return p -} - -func (x SubstrateActorSortField) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SubstrateActorSortField) Descriptor() protoreflect.EnumDescriptor { - return file_kagent_api_v1alpha1_system_proto_enumTypes[1].Descriptor() -} - -func (SubstrateActorSortField) Type() protoreflect.EnumType { - return &file_kagent_api_v1alpha1_system_proto_enumTypes[1] -} - -func (x SubstrateActorSortField) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SubstrateActorSortField.Descriptor instead. -func (SubstrateActorSortField) EnumDescriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{1} -} - -// SubstrateWorkerSortField is the column ListSubstrateWorkers orders by. -// Every order ends in the worker pod, which is unique within its namespace. -type SubstrateWorkerSortField int32 - -const ( - // Unspecified groups by pool and orders by pod within each group. - SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED SubstrateWorkerSortField = 0 - SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POOL SubstrateWorkerSortField = 1 - SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POD SubstrateWorkerSortField = 2 - SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_ACTOR SubstrateWorkerSortField = 3 -) - -// Enum value maps for SubstrateWorkerSortField. -var ( - SubstrateWorkerSortField_name = map[int32]string{ - 0: "SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED", - 1: "SUBSTRATE_WORKER_SORT_FIELD_POOL", - 2: "SUBSTRATE_WORKER_SORT_FIELD_POD", - 3: "SUBSTRATE_WORKER_SORT_FIELD_ACTOR", - } - SubstrateWorkerSortField_value = map[string]int32{ - "SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED": 0, - "SUBSTRATE_WORKER_SORT_FIELD_POOL": 1, - "SUBSTRATE_WORKER_SORT_FIELD_POD": 2, - "SUBSTRATE_WORKER_SORT_FIELD_ACTOR": 3, - } -) - -func (x SubstrateWorkerSortField) Enum() *SubstrateWorkerSortField { - p := new(SubstrateWorkerSortField) - *p = x - return p -} - -func (x SubstrateWorkerSortField) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (SubstrateWorkerSortField) Descriptor() protoreflect.EnumDescriptor { - return file_kagent_api_v1alpha1_system_proto_enumTypes[2].Descriptor() -} - -func (SubstrateWorkerSortField) Type() protoreflect.EnumType { - return &file_kagent_api_v1alpha1_system_proto_enumTypes[2] -} - -func (x SubstrateWorkerSortField) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use SubstrateWorkerSortField.Descriptor instead. -func (SubstrateWorkerSortField) EnumDescriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{2} -} - type GetVersionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -630,598 +458,6 @@ func (x *GetSubstrateStatusResponse) GetWorkers() []*SubstrateWorker { return nil } -type GetSubstrateSummaryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Namespace narrows the inventory. Empty means every namespace the - // controller observes, as it does on GetSubstrateStatusRequest. - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSubstrateSummaryRequest) Reset() { - *x = GetSubstrateSummaryRequest{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSubstrateSummaryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSubstrateSummaryRequest) ProtoMessage() {} - -func (x *GetSubstrateSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSubstrateSummaryRequest.ProtoReflect.Descriptor instead. -func (*GetSubstrateSummaryRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{9} -} - -func (x *GetSubstrateSummaryRequest) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -// SubstrateStatusCount is how many rows carry one status. -// -// Status is a plain string on the wire rather than an enum: ate-api and the -// ActorTemplate controller each fill it in their own vocabulary, so a closed -// set here would drop a status a newer substrate reports. Counting whatever -// arrives keeps the tally complete even when a value is one this build has -// never seen. -type SubstrateStatusCount struct { - state protoimpl.MessageState `protogen:"open.v1"` - Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` - Count int32 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SubstrateStatusCount) Reset() { - *x = SubstrateStatusCount{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SubstrateStatusCount) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SubstrateStatusCount) ProtoMessage() {} - -func (x *SubstrateStatusCount) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SubstrateStatusCount.ProtoReflect.Descriptor instead. -func (*SubstrateStatusCount) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{10} -} - -func (x *SubstrateStatusCount) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *SubstrateStatusCount) GetCount() int32 { - if x != nil { - return x.Count - } - return 0 -} - -type GetSubstrateSummaryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Enabled is false when the controller has no ate-api endpoint configured, - // which is an ordinary deployment rather than a failure. - Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` - // AteApiError is set when ate-api answered with an error on an otherwise - // successful read: the Kubernetes-derived halves below are complete while the - // runtime counts may be short. Distinct from the RPC failing, and worth - // reporting differently. - AteApiError string `protobuf:"bytes,2,opt,name=ate_api_error,json=ateApiError,proto3" json:"ate_api_error,omitempty"` - // Worker pools and actor templates are bounded by how the cluster is - // configured rather than by how much work it is doing — a handful either way - // — so they ride inline instead of costing two more round trips. - WorkerPools []*SubstrateWorkerPool `protobuf:"bytes,3,rep,name=worker_pools,json=workerPools,proto3" json:"worker_pools,omitempty"` - ActorTemplates []*SubstrateActorTemplate `protobuf:"bytes,4,rep,name=actor_templates,json=actorTemplates,proto3" json:"actor_templates,omitempty"` - // Totals over everything in scope, before any filter. - ActorCount int32 `protobuf:"varint,5,opt,name=actor_count,json=actorCount,proto3" json:"actor_count,omitempty"` - WorkerCount int32 `protobuf:"varint,6,opt,name=worker_count,json=workerCount,proto3" json:"worker_count,omitempty"` - // RunningActorCount and BusyWorkerCount are the numerators the inventory is - // actually read by: how much of what exists is doing something. A worker is - // busy when an actor is placed on it. - RunningActorCount int32 `protobuf:"varint,7,opt,name=running_actor_count,json=runningActorCount,proto3" json:"running_actor_count,omitempty"` - BusyWorkerCount int32 `protobuf:"varint,8,opt,name=busy_worker_count,json=busyWorkerCount,proto3" json:"busy_worker_count,omitempty"` - // ActorStatusCounts is every status present, with how many actors hold it, - // ordered by status. The whole distribution rather than the running count - // alone, so a caller can say what the rest are without reading them. - ActorStatusCounts []*SubstrateStatusCount `protobuf:"bytes,9,rep,name=actor_status_counts,json=actorStatusCounts,proto3" json:"actor_status_counts,omitempty"` - // ComputedAt is when this answer was produced, which is not necessarily now. - // - // The substrate reads are memoised for a fraction of a second, because each one - // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - // them. A cache is also exactly how a polling control becomes a lie, so the age - // travels with the answer: a caller can say "as of 0.4s ago" rather than - // implying "now", and a reader can tell a stalled cluster from a stalled read. - ComputedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=computed_at,json=computedAt,proto3" json:"computed_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSubstrateSummaryResponse) Reset() { - *x = GetSubstrateSummaryResponse{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSubstrateSummaryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSubstrateSummaryResponse) ProtoMessage() {} - -func (x *GetSubstrateSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSubstrateSummaryResponse.ProtoReflect.Descriptor instead. -func (*GetSubstrateSummaryResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{11} -} - -func (x *GetSubstrateSummaryResponse) GetEnabled() bool { - if x != nil { - return x.Enabled - } - return false -} - -func (x *GetSubstrateSummaryResponse) GetAteApiError() string { - if x != nil { - return x.AteApiError - } - return "" -} - -func (x *GetSubstrateSummaryResponse) GetWorkerPools() []*SubstrateWorkerPool { - if x != nil { - return x.WorkerPools - } - return nil -} - -func (x *GetSubstrateSummaryResponse) GetActorTemplates() []*SubstrateActorTemplate { - if x != nil { - return x.ActorTemplates - } - return nil -} - -func (x *GetSubstrateSummaryResponse) GetActorCount() int32 { - if x != nil { - return x.ActorCount - } - return 0 -} - -func (x *GetSubstrateSummaryResponse) GetWorkerCount() int32 { - if x != nil { - return x.WorkerCount - } - return 0 -} - -func (x *GetSubstrateSummaryResponse) GetRunningActorCount() int32 { - if x != nil { - return x.RunningActorCount - } - return 0 -} - -func (x *GetSubstrateSummaryResponse) GetBusyWorkerCount() int32 { - if x != nil { - return x.BusyWorkerCount - } - return 0 -} - -func (x *GetSubstrateSummaryResponse) GetActorStatusCounts() []*SubstrateStatusCount { - if x != nil { - return x.ActorStatusCounts - } - return nil -} - -func (x *GetSubstrateSummaryResponse) GetComputedAt() *timestamppb.Timestamp { - if x != nil { - return x.ComputedAt - } - return nil -} - -type ListSubstrateActorsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - // Filter is matched case-insensitively as a substring against the actor's id, - // status, actor template and worker pod — the fields a row displays. Empty - // matches everything. - Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` - Page *PageRequest `protobuf:"bytes,3,opt,name=page,proto3" json:"page,omitempty"` - // Sorting is server-side because the rows are paged: ordering a page that has - // already been fetched reorders a hundred rows out of hundreds of thousands, - // which looks like sorting and is not. - SortField SubstrateActorSortField `protobuf:"varint,4,opt,name=sort_field,json=sortField,proto3,enum=kagent.api.v1alpha1.SubstrateActorSortField" json:"sort_field,omitempty"` - SortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"sort_order,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSubstrateActorsRequest) Reset() { - *x = ListSubstrateActorsRequest{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSubstrateActorsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSubstrateActorsRequest) ProtoMessage() {} - -func (x *ListSubstrateActorsRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSubstrateActorsRequest.ProtoReflect.Descriptor instead. -func (*ListSubstrateActorsRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{12} -} - -func (x *ListSubstrateActorsRequest) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *ListSubstrateActorsRequest) GetFilter() string { - if x != nil { - return x.Filter - } - return "" -} - -func (x *ListSubstrateActorsRequest) GetPage() *PageRequest { - if x != nil { - return x.Page - } - return nil -} - -func (x *ListSubstrateActorsRequest) GetSortField() SubstrateActorSortField { - if x != nil { - return x.SortField - } - return SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED -} - -func (x *ListSubstrateActorsRequest) GetSortOrder() SubstrateSortOrder { - if x != nil { - return x.SortOrder - } - return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED -} - -type ListSubstrateActorsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Actors []*SubstrateActor `protobuf:"bytes,1,rep,name=actors,proto3" json:"actors,omitempty"` - Page *PageResponse `protobuf:"bytes,2,opt,name=page,proto3" json:"page,omitempty"` - // TotalSize is how many actors match the filter across every page, so a - // caller can say "20 of 4,312" rather than implying the page is the whole - // result. - TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` - // The order actually applied, so a caller can say how the rows are sorted - // rather than assuming its request was honoured. An unspecified field and an - // unspecified order both resolve to a concrete value here. - AppliedSortField SubstrateActorSortField `protobuf:"varint,4,opt,name=applied_sort_field,json=appliedSortField,proto3,enum=kagent.api.v1alpha1.SubstrateActorSortField" json:"applied_sort_field,omitempty"` - AppliedSortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=applied_sort_order,json=appliedSortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"applied_sort_order,omitempty"` - // ComputedAt is when this answer was produced, which is not necessarily now. - // - // The substrate reads are memoised for a fraction of a second, because each one - // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - // them. A cache is also exactly how a polling control becomes a lie, so the age - // travels with the answer: a caller can say "as of 0.4s ago" rather than - // implying "now", and a reader can tell a stalled cluster from a stalled read. - ComputedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=computed_at,json=computedAt,proto3" json:"computed_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSubstrateActorsResponse) Reset() { - *x = ListSubstrateActorsResponse{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSubstrateActorsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSubstrateActorsResponse) ProtoMessage() {} - -func (x *ListSubstrateActorsResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSubstrateActorsResponse.ProtoReflect.Descriptor instead. -func (*ListSubstrateActorsResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{13} -} - -func (x *ListSubstrateActorsResponse) GetActors() []*SubstrateActor { - if x != nil { - return x.Actors - } - return nil -} - -func (x *ListSubstrateActorsResponse) GetPage() *PageResponse { - if x != nil { - return x.Page - } - return nil -} - -func (x *ListSubstrateActorsResponse) GetTotalSize() int32 { - if x != nil { - return x.TotalSize - } - return 0 -} - -func (x *ListSubstrateActorsResponse) GetAppliedSortField() SubstrateActorSortField { - if x != nil { - return x.AppliedSortField - } - return SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED -} - -func (x *ListSubstrateActorsResponse) GetAppliedSortOrder() SubstrateSortOrder { - if x != nil { - return x.AppliedSortOrder - } - return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED -} - -func (x *ListSubstrateActorsResponse) GetComputedAt() *timestamppb.Timestamp { - if x != nil { - return x.ComputedAt - } - return nil -} - -type ListSubstrateWorkersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - // Filter is matched case-insensitively as a substring against the worker's - // namespace, pod, pool and placed actor. - Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` - Page *PageRequest `protobuf:"bytes,3,opt,name=page,proto3" json:"page,omitempty"` - SortField SubstrateWorkerSortField `protobuf:"varint,4,opt,name=sort_field,json=sortField,proto3,enum=kagent.api.v1alpha1.SubstrateWorkerSortField" json:"sort_field,omitempty"` - SortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"sort_order,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSubstrateWorkersRequest) Reset() { - *x = ListSubstrateWorkersRequest{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSubstrateWorkersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSubstrateWorkersRequest) ProtoMessage() {} - -func (x *ListSubstrateWorkersRequest) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSubstrateWorkersRequest.ProtoReflect.Descriptor instead. -func (*ListSubstrateWorkersRequest) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{14} -} - -func (x *ListSubstrateWorkersRequest) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *ListSubstrateWorkersRequest) GetFilter() string { - if x != nil { - return x.Filter - } - return "" -} - -func (x *ListSubstrateWorkersRequest) GetPage() *PageRequest { - if x != nil { - return x.Page - } - return nil -} - -func (x *ListSubstrateWorkersRequest) GetSortField() SubstrateWorkerSortField { - if x != nil { - return x.SortField - } - return SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED -} - -func (x *ListSubstrateWorkersRequest) GetSortOrder() SubstrateSortOrder { - if x != nil { - return x.SortOrder - } - return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED -} - -type ListSubstrateWorkersResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Workers []*SubstrateWorker `protobuf:"bytes,1,rep,name=workers,proto3" json:"workers,omitempty"` - Page *PageResponse `protobuf:"bytes,2,opt,name=page,proto3" json:"page,omitempty"` - TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` - AppliedSortField SubstrateWorkerSortField `protobuf:"varint,4,opt,name=applied_sort_field,json=appliedSortField,proto3,enum=kagent.api.v1alpha1.SubstrateWorkerSortField" json:"applied_sort_field,omitempty"` - AppliedSortOrder SubstrateSortOrder `protobuf:"varint,5,opt,name=applied_sort_order,json=appliedSortOrder,proto3,enum=kagent.api.v1alpha1.SubstrateSortOrder" json:"applied_sort_order,omitempty"` - // ComputedAt is when this answer was produced, which is not necessarily now. - // - // The substrate reads are memoised for a fraction of a second, because each one - // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - // them. A cache is also exactly how a polling control becomes a lie, so the age - // travels with the answer: a caller can say "as of 0.4s ago" rather than - // implying "now", and a reader can tell a stalled cluster from a stalled read. - ComputedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=computed_at,json=computedAt,proto3" json:"computed_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSubstrateWorkersResponse) Reset() { - *x = ListSubstrateWorkersResponse{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSubstrateWorkersResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSubstrateWorkersResponse) ProtoMessage() {} - -func (x *ListSubstrateWorkersResponse) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSubstrateWorkersResponse.ProtoReflect.Descriptor instead. -func (*ListSubstrateWorkersResponse) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{15} -} - -func (x *ListSubstrateWorkersResponse) GetWorkers() []*SubstrateWorker { - if x != nil { - return x.Workers - } - return nil -} - -func (x *ListSubstrateWorkersResponse) GetPage() *PageResponse { - if x != nil { - return x.Page - } - return nil -} - -func (x *ListSubstrateWorkersResponse) GetTotalSize() int32 { - if x != nil { - return x.TotalSize - } - return 0 -} - -func (x *ListSubstrateWorkersResponse) GetAppliedSortField() SubstrateWorkerSortField { - if x != nil { - return x.AppliedSortField - } - return SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED -} - -func (x *ListSubstrateWorkersResponse) GetAppliedSortOrder() SubstrateSortOrder { - if x != nil { - return x.AppliedSortOrder - } - return SubstrateSortOrder_SUBSTRATE_SORT_ORDER_UNSPECIFIED -} - -func (x *ListSubstrateWorkersResponse) GetComputedAt() *timestamppb.Timestamp { - if x != nil { - return x.ComputedAt - } - return nil -} - type SubstrateWorkerPool struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -1234,7 +470,7 @@ type SubstrateWorkerPool struct { func (x *SubstrateWorkerPool) Reset() { *x = SubstrateWorkerPool{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[16] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1246,7 +482,7 @@ func (x *SubstrateWorkerPool) String() string { func (*SubstrateWorkerPool) ProtoMessage() {} func (x *SubstrateWorkerPool) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[16] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1259,7 +495,7 @@ func (x *SubstrateWorkerPool) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateWorkerPool.ProtoReflect.Descriptor instead. func (*SubstrateWorkerPool) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{16} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{9} } func (x *SubstrateWorkerPool) GetNamespace() string { @@ -1307,7 +543,7 @@ type SubstrateActorTemplate struct { func (x *SubstrateActorTemplate) Reset() { *x = SubstrateActorTemplate{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[17] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1319,7 +555,7 @@ func (x *SubstrateActorTemplate) String() string { func (*SubstrateActorTemplate) ProtoMessage() {} func (x *SubstrateActorTemplate) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[17] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1332,7 +568,7 @@ func (x *SubstrateActorTemplate) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateActorTemplate.ProtoReflect.Descriptor instead. func (*SubstrateActorTemplate) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{17} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{10} } func (x *SubstrateActorTemplate) GetNamespace() string { @@ -1418,7 +654,7 @@ type SubstrateActor struct { func (x *SubstrateActor) Reset() { *x = SubstrateActor{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[18] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1430,7 +666,7 @@ func (x *SubstrateActor) String() string { func (*SubstrateActor) ProtoMessage() {} func (x *SubstrateActor) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[18] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1443,7 +679,7 @@ func (x *SubstrateActor) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateActor.ProtoReflect.Descriptor instead. func (*SubstrateActor) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{18} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{11} } func (x *SubstrateActor) GetActorId() string { @@ -1546,7 +782,7 @@ type SubstrateWorker struct { func (x *SubstrateWorker) Reset() { *x = SubstrateWorker{} - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[19] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1558,7 +794,7 @@ func (x *SubstrateWorker) String() string { func (*SubstrateWorker) ProtoMessage() {} func (x *SubstrateWorker) ProtoReflect() protoreflect.Message { - mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[19] + mi := &file_kagent_api_v1alpha1_system_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1571,7 +807,7 @@ func (x *SubstrateWorker) ProtoReflect() protoreflect.Message { // Deprecated: Use SubstrateWorker.ProtoReflect.Descriptor instead. func (*SubstrateWorker) Descriptor() ([]byte, []int) { - return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{19} + return file_kagent_api_v1alpha1_system_proto_rawDescGZIP(), []int{12} } func (x *SubstrateWorker) GetWorkerNamespace() string { @@ -1634,7 +870,7 @@ var File_kagent_api_v1alpha1_system_proto protoreflect.FileDescriptor const file_kagent_api_v1alpha1_system_proto_rawDesc = "" + "\n" + - " kagent/api/v1alpha1/system.proto\x12\x13kagent.api.v1alpha1\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a kagent/api/v1alpha1/common.proto\"\x13\n" + + " kagent/api/v1alpha1/system.proto\x12\x13kagent.api.v1alpha1\x1a\x1cgoogle/protobuf/struct.proto\"\x13\n" + "\x11GetVersionRequest\"y\n" + "\x12GetVersionResponse\x12%\n" + "\x0ekagent_version\x18\x01 \x01(\tR\rkagentVersion\x12\x1d\n" + @@ -1661,60 +897,7 @@ const file_kagent_api_v1alpha1_system_proto_rawDesc = "" + "\fworker_pools\x18\x03 \x03(\v2(.kagent.api.v1alpha1.SubstrateWorkerPoolR\vworkerPools\x12T\n" + "\x0factor_templates\x18\x04 \x03(\v2+.kagent.api.v1alpha1.SubstrateActorTemplateR\x0eactorTemplates\x12;\n" + "\x06actors\x18\x05 \x03(\v2#.kagent.api.v1alpha1.SubstrateActorR\x06actors\x12>\n" + - "\aworkers\x18\x06 \x03(\v2$.kagent.api.v1alpha1.SubstrateWorkerR\aworkers\":\n" + - "\x1aGetSubstrateSummaryRequest\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\"D\n" + - "\x14SubstrateStatusCount\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" + - "\x05count\x18\x02 \x01(\x05R\x05count\"\xb6\x04\n" + - "\x1bGetSubstrateSummaryResponse\x12\x18\n" + - "\aenabled\x18\x01 \x01(\bR\aenabled\x12\"\n" + - "\rate_api_error\x18\x02 \x01(\tR\vateApiError\x12K\n" + - "\fworker_pools\x18\x03 \x03(\v2(.kagent.api.v1alpha1.SubstrateWorkerPoolR\vworkerPools\x12T\n" + - "\x0factor_templates\x18\x04 \x03(\v2+.kagent.api.v1alpha1.SubstrateActorTemplateR\x0eactorTemplates\x12\x1f\n" + - "\vactor_count\x18\x05 \x01(\x05R\n" + - "actorCount\x12!\n" + - "\fworker_count\x18\x06 \x01(\x05R\vworkerCount\x12.\n" + - "\x13running_actor_count\x18\a \x01(\x05R\x11runningActorCount\x12*\n" + - "\x11busy_worker_count\x18\b \x01(\x05R\x0fbusyWorkerCount\x12Y\n" + - "\x13actor_status_counts\x18\t \x03(\v2).kagent.api.v1alpha1.SubstrateStatusCountR\x11actorStatusCounts\x12;\n" + - "\vcomputed_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "computedAt\"\x9d\x02\n" + - "\x1aListSubstrateActorsRequest\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + - "\x06filter\x18\x02 \x01(\tR\x06filter\x124\n" + - "\x04page\x18\x03 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12K\n" + - "\n" + - "sort_field\x18\x04 \x01(\x0e2,.kagent.api.v1alpha1.SubstrateActorSortFieldR\tsortField\x12F\n" + - "\n" + - "sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\tsortOrder\"\xa0\x03\n" + - "\x1bListSubstrateActorsResponse\x12;\n" + - "\x06actors\x18\x01 \x03(\v2#.kagent.api.v1alpha1.SubstrateActorR\x06actors\x125\n" + - "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\x12\x1d\n" + - "\n" + - "total_size\x18\x03 \x01(\x05R\ttotalSize\x12Z\n" + - "\x12applied_sort_field\x18\x04 \x01(\x0e2,.kagent.api.v1alpha1.SubstrateActorSortFieldR\x10appliedSortField\x12U\n" + - "\x12applied_sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\x10appliedSortOrder\x12;\n" + - "\vcomputed_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "computedAt\"\x9f\x02\n" + - "\x1bListSubstrateWorkersRequest\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + - "\x06filter\x18\x02 \x01(\tR\x06filter\x124\n" + - "\x04page\x18\x03 \x01(\v2 .kagent.api.v1alpha1.PageRequestR\x04page\x12L\n" + - "\n" + - "sort_field\x18\x04 \x01(\x0e2-.kagent.api.v1alpha1.SubstrateWorkerSortFieldR\tsortField\x12F\n" + - "\n" + - "sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\tsortOrder\"\xa5\x03\n" + - "\x1cListSubstrateWorkersResponse\x12>\n" + - "\aworkers\x18\x01 \x03(\v2$.kagent.api.v1alpha1.SubstrateWorkerR\aworkers\x125\n" + - "\x04page\x18\x02 \x01(\v2!.kagent.api.v1alpha1.PageResponseR\x04page\x12\x1d\n" + - "\n" + - "total_size\x18\x03 \x01(\x05R\ttotalSize\x12[\n" + - "\x12applied_sort_field\x18\x04 \x01(\x0e2-.kagent.api.v1alpha1.SubstrateWorkerSortFieldR\x10appliedSortField\x12U\n" + - "\x12applied_sort_order\x18\x05 \x01(\x0e2'.kagent.api.v1alpha1.SubstrateSortOrderR\x10appliedSortOrder\x12;\n" + - "\vcomputed_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "computedAt\"\x84\x01\n" + + "\aworkers\x18\x06 \x03(\v2$.kagent.api.v1alpha1.SubstrateWorkerR\aworkers\"\x84\x01\n" + "\x13SubstrateWorkerPool\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" + @@ -1756,31 +939,13 @@ const file_kagent_api_v1alpha1_system_proto_rawDesc = "" + "\x0eactor_template\x18\x05 \x01(\tR\ractorTemplate\x12\x19\n" + "\bactor_id\x18\x06 \x01(\tR\aactorId\x12\x0e\n" + "\x02ip\x18\a \x01(\tR\x02ip\x12\x18\n" + - "\aversion\x18\b \x01(\x03R\aversion*\x83\x01\n" + - "\x12SubstrateSortOrder\x12$\n" + - " SUBSTRATE_SORT_ORDER_UNSPECIFIED\x10\x00\x12\"\n" + - "\x1eSUBSTRATE_SORT_ORDER_ASCENDING\x10\x01\x12#\n" + - "\x1fSUBSTRATE_SORT_ORDER_DESCENDING\x10\x02*\xef\x01\n" + - "\x17SubstrateActorSortField\x12*\n" + - "&SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED\x10\x00\x12%\n" + - "!SUBSTRATE_ACTOR_SORT_FIELD_STATUS\x10\x01\x12'\n" + - "#SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID\x10\x02\x12-\n" + - ")SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE\x10\x03\x12)\n" + - "%SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD\x10\x04*\xb9\x01\n" + - "\x18SubstrateWorkerSortField\x12+\n" + - "'SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED\x10\x00\x12$\n" + - " SUBSTRATE_WORKER_SORT_FIELD_POOL\x10\x01\x12#\n" + - "\x1fSUBSTRATE_WORKER_SORT_FIELD_POD\x10\x02\x12%\n" + - "!SUBSTRATE_WORKER_SORT_FIELD_ACTOR\x10\x032\xac\x06\n" + + "\aversion\x18\b \x01(\x03R\aversion2\xbb\x03\n" + "\rSystemService\x12]\n" + "\n" + "GetVersion\x12&.kagent.api.v1alpha1.GetVersionRequest\x1a'.kagent.api.v1alpha1.GetVersionResponse\x12i\n" + "\x0eGetCurrentUser\x12*.kagent.api.v1alpha1.GetCurrentUserRequest\x1a+.kagent.api.v1alpha1.GetCurrentUserResponse\x12i\n" + "\x0eListNamespaces\x12*.kagent.api.v1alpha1.ListNamespacesRequest\x1a+.kagent.api.v1alpha1.ListNamespacesResponse\x12u\n" + - "\x12GetSubstrateStatus\x12..kagent.api.v1alpha1.GetSubstrateStatusRequest\x1a/.kagent.api.v1alpha1.GetSubstrateStatusResponse\x12x\n" + - "\x13GetSubstrateSummary\x12/.kagent.api.v1alpha1.GetSubstrateSummaryRequest\x1a0.kagent.api.v1alpha1.GetSubstrateSummaryResponse\x12x\n" + - "\x13ListSubstrateActors\x12/.kagent.api.v1alpha1.ListSubstrateActorsRequest\x1a0.kagent.api.v1alpha1.ListSubstrateActorsResponse\x12{\n" + - "\x14ListSubstrateWorkers\x120.kagent.api.v1alpha1.ListSubstrateWorkersRequest\x1a1.kagent.api.v1alpha1.ListSubstrateWorkersResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" + "\x12GetSubstrateStatus\x12..kagent.api.v1alpha1.GetSubstrateStatusRequest\x1a/.kagent.api.v1alpha1.GetSubstrateStatusResponseBIZGgithub.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1b\x06proto3" var ( file_kagent_api_v1alpha1_system_proto_rawDescOnce sync.Once @@ -1794,83 +959,43 @@ func file_kagent_api_v1alpha1_system_proto_rawDescGZIP() []byte { return file_kagent_api_v1alpha1_system_proto_rawDescData } -var file_kagent_api_v1alpha1_system_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_kagent_api_v1alpha1_system_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_kagent_api_v1alpha1_system_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_kagent_api_v1alpha1_system_proto_goTypes = []any{ - (SubstrateSortOrder)(0), // 0: kagent.api.v1alpha1.SubstrateSortOrder - (SubstrateActorSortField)(0), // 1: kagent.api.v1alpha1.SubstrateActorSortField - (SubstrateWorkerSortField)(0), // 2: kagent.api.v1alpha1.SubstrateWorkerSortField - (*GetVersionRequest)(nil), // 3: kagent.api.v1alpha1.GetVersionRequest - (*GetVersionResponse)(nil), // 4: kagent.api.v1alpha1.GetVersionResponse - (*GetCurrentUserRequest)(nil), // 5: kagent.api.v1alpha1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 6: kagent.api.v1alpha1.GetCurrentUserResponse - (*ListNamespacesRequest)(nil), // 7: kagent.api.v1alpha1.ListNamespacesRequest - (*Namespace)(nil), // 8: kagent.api.v1alpha1.Namespace - (*ListNamespacesResponse)(nil), // 9: kagent.api.v1alpha1.ListNamespacesResponse - (*GetSubstrateStatusRequest)(nil), // 10: kagent.api.v1alpha1.GetSubstrateStatusRequest - (*GetSubstrateStatusResponse)(nil), // 11: kagent.api.v1alpha1.GetSubstrateStatusResponse - (*GetSubstrateSummaryRequest)(nil), // 12: kagent.api.v1alpha1.GetSubstrateSummaryRequest - (*SubstrateStatusCount)(nil), // 13: kagent.api.v1alpha1.SubstrateStatusCount - (*GetSubstrateSummaryResponse)(nil), // 14: kagent.api.v1alpha1.GetSubstrateSummaryResponse - (*ListSubstrateActorsRequest)(nil), // 15: kagent.api.v1alpha1.ListSubstrateActorsRequest - (*ListSubstrateActorsResponse)(nil), // 16: kagent.api.v1alpha1.ListSubstrateActorsResponse - (*ListSubstrateWorkersRequest)(nil), // 17: kagent.api.v1alpha1.ListSubstrateWorkersRequest - (*ListSubstrateWorkersResponse)(nil), // 18: kagent.api.v1alpha1.ListSubstrateWorkersResponse - (*SubstrateWorkerPool)(nil), // 19: kagent.api.v1alpha1.SubstrateWorkerPool - (*SubstrateActorTemplate)(nil), // 20: kagent.api.v1alpha1.SubstrateActorTemplate - (*SubstrateActor)(nil), // 21: kagent.api.v1alpha1.SubstrateActor - (*SubstrateWorker)(nil), // 22: kagent.api.v1alpha1.SubstrateWorker - (*structpb.Struct)(nil), // 23: google.protobuf.Struct - (*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp - (*PageRequest)(nil), // 25: kagent.api.v1alpha1.PageRequest - (*PageResponse)(nil), // 26: kagent.api.v1alpha1.PageResponse + (*GetVersionRequest)(nil), // 0: kagent.api.v1alpha1.GetVersionRequest + (*GetVersionResponse)(nil), // 1: kagent.api.v1alpha1.GetVersionResponse + (*GetCurrentUserRequest)(nil), // 2: kagent.api.v1alpha1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 3: kagent.api.v1alpha1.GetCurrentUserResponse + (*ListNamespacesRequest)(nil), // 4: kagent.api.v1alpha1.ListNamespacesRequest + (*Namespace)(nil), // 5: kagent.api.v1alpha1.Namespace + (*ListNamespacesResponse)(nil), // 6: kagent.api.v1alpha1.ListNamespacesResponse + (*GetSubstrateStatusRequest)(nil), // 7: kagent.api.v1alpha1.GetSubstrateStatusRequest + (*GetSubstrateStatusResponse)(nil), // 8: kagent.api.v1alpha1.GetSubstrateStatusResponse + (*SubstrateWorkerPool)(nil), // 9: kagent.api.v1alpha1.SubstrateWorkerPool + (*SubstrateActorTemplate)(nil), // 10: kagent.api.v1alpha1.SubstrateActorTemplate + (*SubstrateActor)(nil), // 11: kagent.api.v1alpha1.SubstrateActor + (*SubstrateWorker)(nil), // 12: kagent.api.v1alpha1.SubstrateWorker + (*structpb.Struct)(nil), // 13: google.protobuf.Struct } var file_kagent_api_v1alpha1_system_proto_depIdxs = []int32{ - 23, // 0: kagent.api.v1alpha1.GetCurrentUserResponse.claims:type_name -> google.protobuf.Struct - 8, // 1: kagent.api.v1alpha1.ListNamespacesResponse.namespaces:type_name -> kagent.api.v1alpha1.Namespace - 19, // 2: kagent.api.v1alpha1.GetSubstrateStatusResponse.worker_pools:type_name -> kagent.api.v1alpha1.SubstrateWorkerPool - 20, // 3: kagent.api.v1alpha1.GetSubstrateStatusResponse.actor_templates:type_name -> kagent.api.v1alpha1.SubstrateActorTemplate - 21, // 4: kagent.api.v1alpha1.GetSubstrateStatusResponse.actors:type_name -> kagent.api.v1alpha1.SubstrateActor - 22, // 5: kagent.api.v1alpha1.GetSubstrateStatusResponse.workers:type_name -> kagent.api.v1alpha1.SubstrateWorker - 19, // 6: kagent.api.v1alpha1.GetSubstrateSummaryResponse.worker_pools:type_name -> kagent.api.v1alpha1.SubstrateWorkerPool - 20, // 7: kagent.api.v1alpha1.GetSubstrateSummaryResponse.actor_templates:type_name -> kagent.api.v1alpha1.SubstrateActorTemplate - 13, // 8: kagent.api.v1alpha1.GetSubstrateSummaryResponse.actor_status_counts:type_name -> kagent.api.v1alpha1.SubstrateStatusCount - 24, // 9: kagent.api.v1alpha1.GetSubstrateSummaryResponse.computed_at:type_name -> google.protobuf.Timestamp - 25, // 10: kagent.api.v1alpha1.ListSubstrateActorsRequest.page:type_name -> kagent.api.v1alpha1.PageRequest - 1, // 11: kagent.api.v1alpha1.ListSubstrateActorsRequest.sort_field:type_name -> kagent.api.v1alpha1.SubstrateActorSortField - 0, // 12: kagent.api.v1alpha1.ListSubstrateActorsRequest.sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder - 21, // 13: kagent.api.v1alpha1.ListSubstrateActorsResponse.actors:type_name -> kagent.api.v1alpha1.SubstrateActor - 26, // 14: kagent.api.v1alpha1.ListSubstrateActorsResponse.page:type_name -> kagent.api.v1alpha1.PageResponse - 1, // 15: kagent.api.v1alpha1.ListSubstrateActorsResponse.applied_sort_field:type_name -> kagent.api.v1alpha1.SubstrateActorSortField - 0, // 16: kagent.api.v1alpha1.ListSubstrateActorsResponse.applied_sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder - 24, // 17: kagent.api.v1alpha1.ListSubstrateActorsResponse.computed_at:type_name -> google.protobuf.Timestamp - 25, // 18: kagent.api.v1alpha1.ListSubstrateWorkersRequest.page:type_name -> kagent.api.v1alpha1.PageRequest - 2, // 19: kagent.api.v1alpha1.ListSubstrateWorkersRequest.sort_field:type_name -> kagent.api.v1alpha1.SubstrateWorkerSortField - 0, // 20: kagent.api.v1alpha1.ListSubstrateWorkersRequest.sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder - 22, // 21: kagent.api.v1alpha1.ListSubstrateWorkersResponse.workers:type_name -> kagent.api.v1alpha1.SubstrateWorker - 26, // 22: kagent.api.v1alpha1.ListSubstrateWorkersResponse.page:type_name -> kagent.api.v1alpha1.PageResponse - 2, // 23: kagent.api.v1alpha1.ListSubstrateWorkersResponse.applied_sort_field:type_name -> kagent.api.v1alpha1.SubstrateWorkerSortField - 0, // 24: kagent.api.v1alpha1.ListSubstrateWorkersResponse.applied_sort_order:type_name -> kagent.api.v1alpha1.SubstrateSortOrder - 24, // 25: kagent.api.v1alpha1.ListSubstrateWorkersResponse.computed_at:type_name -> google.protobuf.Timestamp - 3, // 26: kagent.api.v1alpha1.SystemService.GetVersion:input_type -> kagent.api.v1alpha1.GetVersionRequest - 5, // 27: kagent.api.v1alpha1.SystemService.GetCurrentUser:input_type -> kagent.api.v1alpha1.GetCurrentUserRequest - 7, // 28: kagent.api.v1alpha1.SystemService.ListNamespaces:input_type -> kagent.api.v1alpha1.ListNamespacesRequest - 10, // 29: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:input_type -> kagent.api.v1alpha1.GetSubstrateStatusRequest - 12, // 30: kagent.api.v1alpha1.SystemService.GetSubstrateSummary:input_type -> kagent.api.v1alpha1.GetSubstrateSummaryRequest - 15, // 31: kagent.api.v1alpha1.SystemService.ListSubstrateActors:input_type -> kagent.api.v1alpha1.ListSubstrateActorsRequest - 17, // 32: kagent.api.v1alpha1.SystemService.ListSubstrateWorkers:input_type -> kagent.api.v1alpha1.ListSubstrateWorkersRequest - 4, // 33: kagent.api.v1alpha1.SystemService.GetVersion:output_type -> kagent.api.v1alpha1.GetVersionResponse - 6, // 34: kagent.api.v1alpha1.SystemService.GetCurrentUser:output_type -> kagent.api.v1alpha1.GetCurrentUserResponse - 9, // 35: kagent.api.v1alpha1.SystemService.ListNamespaces:output_type -> kagent.api.v1alpha1.ListNamespacesResponse - 11, // 36: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:output_type -> kagent.api.v1alpha1.GetSubstrateStatusResponse - 14, // 37: kagent.api.v1alpha1.SystemService.GetSubstrateSummary:output_type -> kagent.api.v1alpha1.GetSubstrateSummaryResponse - 16, // 38: kagent.api.v1alpha1.SystemService.ListSubstrateActors:output_type -> kagent.api.v1alpha1.ListSubstrateActorsResponse - 18, // 39: kagent.api.v1alpha1.SystemService.ListSubstrateWorkers:output_type -> kagent.api.v1alpha1.ListSubstrateWorkersResponse - 33, // [33:40] is the sub-list for method output_type - 26, // [26:33] is the sub-list for method input_type - 26, // [26:26] is the sub-list for extension type_name - 26, // [26:26] is the sub-list for extension extendee - 0, // [0:26] is the sub-list for field type_name + 13, // 0: kagent.api.v1alpha1.GetCurrentUserResponse.claims:type_name -> google.protobuf.Struct + 5, // 1: kagent.api.v1alpha1.ListNamespacesResponse.namespaces:type_name -> kagent.api.v1alpha1.Namespace + 9, // 2: kagent.api.v1alpha1.GetSubstrateStatusResponse.worker_pools:type_name -> kagent.api.v1alpha1.SubstrateWorkerPool + 10, // 3: kagent.api.v1alpha1.GetSubstrateStatusResponse.actor_templates:type_name -> kagent.api.v1alpha1.SubstrateActorTemplate + 11, // 4: kagent.api.v1alpha1.GetSubstrateStatusResponse.actors:type_name -> kagent.api.v1alpha1.SubstrateActor + 12, // 5: kagent.api.v1alpha1.GetSubstrateStatusResponse.workers:type_name -> kagent.api.v1alpha1.SubstrateWorker + 0, // 6: kagent.api.v1alpha1.SystemService.GetVersion:input_type -> kagent.api.v1alpha1.GetVersionRequest + 2, // 7: kagent.api.v1alpha1.SystemService.GetCurrentUser:input_type -> kagent.api.v1alpha1.GetCurrentUserRequest + 4, // 8: kagent.api.v1alpha1.SystemService.ListNamespaces:input_type -> kagent.api.v1alpha1.ListNamespacesRequest + 7, // 9: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:input_type -> kagent.api.v1alpha1.GetSubstrateStatusRequest + 1, // 10: kagent.api.v1alpha1.SystemService.GetVersion:output_type -> kagent.api.v1alpha1.GetVersionResponse + 3, // 11: kagent.api.v1alpha1.SystemService.GetCurrentUser:output_type -> kagent.api.v1alpha1.GetCurrentUserResponse + 6, // 12: kagent.api.v1alpha1.SystemService.ListNamespaces:output_type -> kagent.api.v1alpha1.ListNamespacesResponse + 8, // 13: kagent.api.v1alpha1.SystemService.GetSubstrateStatus:output_type -> kagent.api.v1alpha1.GetSubstrateStatusResponse + 10, // [10:14] is the sub-list for method output_type + 6, // [6:10] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_kagent_api_v1alpha1_system_proto_init() } @@ -1878,20 +1003,18 @@ func file_kagent_api_v1alpha1_system_proto_init() { if File_kagent_api_v1alpha1_system_proto != nil { return } - file_kagent_api_v1alpha1_common_proto_init() type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_kagent_api_v1alpha1_system_proto_rawDesc), len(file_kagent_api_v1alpha1_system_proto_rawDesc)), - NumEnums: 3, - NumMessages: 20, + NumEnums: 0, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, GoTypes: file_kagent_api_v1alpha1_system_proto_goTypes, DependencyIndexes: file_kagent_api_v1alpha1_system_proto_depIdxs, - EnumInfos: file_kagent_api_v1alpha1_system_proto_enumTypes, MessageInfos: file_kagent_api_v1alpha1_system_proto_msgTypes, }.Build() File_kagent_api_v1alpha1_system_proto = out.File diff --git a/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go b/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go index cddc38a96..53c072b16 100644 --- a/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go +++ b/go/api/gen/kagent/api/v1alpha1/system_grpc.pb.go @@ -19,13 +19,10 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - SystemService_GetVersion_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetVersion" - SystemService_GetCurrentUser_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetCurrentUser" - SystemService_ListNamespaces_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListNamespaces" - SystemService_GetSubstrateStatus_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetSubstrateStatus" - SystemService_GetSubstrateSummary_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetSubstrateSummary" - SystemService_ListSubstrateActors_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListSubstrateActors" - SystemService_ListSubstrateWorkers_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListSubstrateWorkers" + SystemService_GetVersion_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetVersion" + SystemService_GetCurrentUser_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetCurrentUser" + SystemService_ListNamespaces_FullMethodName = "/kagent.api.v1alpha1.SystemService/ListNamespaces" + SystemService_GetSubstrateStatus_FullMethodName = "/kagent.api.v1alpha1.SystemService/GetSubstrateStatus" ) // SystemServiceClient is the client API for SystemService service. @@ -35,38 +32,7 @@ type SystemServiceClient interface { GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) GetCurrentUser(ctx context.Context, in *GetCurrentUserRequest, opts ...grpc.CallOption) (*GetCurrentUserResponse, error) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) - // GetSubstrateStatus returns the entire inventory in one message: every - // worker pool, actor template, actor and worker, unpaginated and unfiltered. - // - // It does not survive a real cluster. A deployment reporting 103,134 actors - // answers with a message the gRPC client refuses outright — "trying to send - // message larger than max (43016460 vs. 16777216)" — so the caller gets no - // inventory at all rather than a large one. Raising the ceiling moves the - // number without changing the shape. - // - // Prefer GetSubstrateSummary with ListSubstrateActors and - // ListSubstrateWorkers, which bound what any single response can carry. This - // RPC is kept for callers that predate them and for the small clusters where - // it still works. GetSubstrateStatus(ctx context.Context, in *GetSubstrateStatusRequest, opts ...grpc.CallOption) (*GetSubstrateStatusResponse, error) - // GetSubstrateSummary returns counts computed server-side, plus the two lists - // that are inherently small. - // - // This is the only honest source of a total. A caller that counts a page and - // presents the result as a total reports "3 actors" for a cluster running a - // hundred thousand, which is the specific failure the paged RPCs below would - // otherwise introduce. - GetSubstrateSummary(ctx context.Context, in *GetSubstrateSummaryRequest, opts ...grpc.CallOption) (*GetSubstrateSummaryResponse, error) - // ListSubstrateActors pages the actors, narrowing them server-side. - // - // Paged because this is one of the two lists whose length is set by the - // cluster rather than by configuration, and filtered server-side for the same - // reason: narrowing a page that has already been fetched searches only what - // was fetched, so a match on page nine reads on screen as "no matches". - ListSubstrateActors(ctx context.Context, in *ListSubstrateActorsRequest, opts ...grpc.CallOption) (*ListSubstrateActorsResponse, error) - // ListSubstrateWorkers pages the worker assignments. The mirror of - // ListSubstrateActors. - ListSubstrateWorkers(ctx context.Context, in *ListSubstrateWorkersRequest, opts ...grpc.CallOption) (*ListSubstrateWorkersResponse, error) } type systemServiceClient struct { @@ -117,36 +83,6 @@ func (c *systemServiceClient) GetSubstrateStatus(ctx context.Context, in *GetSub return out, nil } -func (c *systemServiceClient) GetSubstrateSummary(ctx context.Context, in *GetSubstrateSummaryRequest, opts ...grpc.CallOption) (*GetSubstrateSummaryResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetSubstrateSummaryResponse) - err := c.cc.Invoke(ctx, SystemService_GetSubstrateSummary_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *systemServiceClient) ListSubstrateActors(ctx context.Context, in *ListSubstrateActorsRequest, opts ...grpc.CallOption) (*ListSubstrateActorsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ListSubstrateActorsResponse) - err := c.cc.Invoke(ctx, SystemService_ListSubstrateActors_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *systemServiceClient) ListSubstrateWorkers(ctx context.Context, in *ListSubstrateWorkersRequest, opts ...grpc.CallOption) (*ListSubstrateWorkersResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ListSubstrateWorkersResponse) - err := c.cc.Invoke(ctx, SystemService_ListSubstrateWorkers_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - // SystemServiceServer is the server API for SystemService service. // All implementations must embed UnimplementedSystemServiceServer // for forward compatibility. @@ -154,38 +90,7 @@ type SystemServiceServer interface { GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) GetCurrentUser(context.Context, *GetCurrentUserRequest) (*GetCurrentUserResponse, error) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) - // GetSubstrateStatus returns the entire inventory in one message: every - // worker pool, actor template, actor and worker, unpaginated and unfiltered. - // - // It does not survive a real cluster. A deployment reporting 103,134 actors - // answers with a message the gRPC client refuses outright — "trying to send - // message larger than max (43016460 vs. 16777216)" — so the caller gets no - // inventory at all rather than a large one. Raising the ceiling moves the - // number without changing the shape. - // - // Prefer GetSubstrateSummary with ListSubstrateActors and - // ListSubstrateWorkers, which bound what any single response can carry. This - // RPC is kept for callers that predate them and for the small clusters where - // it still works. GetSubstrateStatus(context.Context, *GetSubstrateStatusRequest) (*GetSubstrateStatusResponse, error) - // GetSubstrateSummary returns counts computed server-side, plus the two lists - // that are inherently small. - // - // This is the only honest source of a total. A caller that counts a page and - // presents the result as a total reports "3 actors" for a cluster running a - // hundred thousand, which is the specific failure the paged RPCs below would - // otherwise introduce. - GetSubstrateSummary(context.Context, *GetSubstrateSummaryRequest) (*GetSubstrateSummaryResponse, error) - // ListSubstrateActors pages the actors, narrowing them server-side. - // - // Paged because this is one of the two lists whose length is set by the - // cluster rather than by configuration, and filtered server-side for the same - // reason: narrowing a page that has already been fetched searches only what - // was fetched, so a match on page nine reads on screen as "no matches". - ListSubstrateActors(context.Context, *ListSubstrateActorsRequest) (*ListSubstrateActorsResponse, error) - // ListSubstrateWorkers pages the worker assignments. The mirror of - // ListSubstrateActors. - ListSubstrateWorkers(context.Context, *ListSubstrateWorkersRequest) (*ListSubstrateWorkersResponse, error) mustEmbedUnimplementedSystemServiceServer() } @@ -208,15 +113,6 @@ func (UnimplementedSystemServiceServer) ListNamespaces(context.Context, *ListNam func (UnimplementedSystemServiceServer) GetSubstrateStatus(context.Context, *GetSubstrateStatusRequest) (*GetSubstrateStatusResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSubstrateStatus not implemented") } -func (UnimplementedSystemServiceServer) GetSubstrateSummary(context.Context, *GetSubstrateSummaryRequest) (*GetSubstrateSummaryResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetSubstrateSummary not implemented") -} -func (UnimplementedSystemServiceServer) ListSubstrateActors(context.Context, *ListSubstrateActorsRequest) (*ListSubstrateActorsResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListSubstrateActors not implemented") -} -func (UnimplementedSystemServiceServer) ListSubstrateWorkers(context.Context, *ListSubstrateWorkersRequest) (*ListSubstrateWorkersResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListSubstrateWorkers not implemented") -} func (UnimplementedSystemServiceServer) mustEmbedUnimplementedSystemServiceServer() {} func (UnimplementedSystemServiceServer) testEmbeddedByValue() {} @@ -310,60 +206,6 @@ func _SystemService_GetSubstrateStatus_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } -func _SystemService_GetSubstrateSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetSubstrateSummaryRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SystemServiceServer).GetSubstrateSummary(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SystemService_GetSubstrateSummary_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SystemServiceServer).GetSubstrateSummary(ctx, req.(*GetSubstrateSummaryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SystemService_ListSubstrateActors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ListSubstrateActorsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SystemServiceServer).ListSubstrateActors(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SystemService_ListSubstrateActors_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SystemServiceServer).ListSubstrateActors(ctx, req.(*ListSubstrateActorsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _SystemService_ListSubstrateWorkers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ListSubstrateWorkersRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SystemServiceServer).ListSubstrateWorkers(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: SystemService_ListSubstrateWorkers_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SystemServiceServer).ListSubstrateWorkers(ctx, req.(*ListSubstrateWorkersRequest)) - } - return interceptor(ctx, in, info, handler) -} - // SystemService_ServiceDesc is the grpc.ServiceDesc for SystemService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -387,18 +229,6 @@ var SystemService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetSubstrateStatus", Handler: _SystemService_GetSubstrateStatus_Handler, }, - { - MethodName: "GetSubstrateSummary", - Handler: _SystemService_GetSubstrateSummary_Handler, - }, - { - MethodName: "ListSubstrateActors", - Handler: _SystemService_ListSubstrateActors_Handler, - }, - { - MethodName: "ListSubstrateWorkers", - Handler: _SystemService_ListSubstrateWorkers_Handler, - }, }, Streams: []grpc.StreamDesc{}, Metadata: "kagent/api/v1alpha1/system.proto", diff --git a/go/core/internal/database/client_agent_instance_test.go b/go/core/internal/database/client_agent_instance_test.go index 22faf5ede..753bfffd1 100644 --- a/go/core/internal/database/client_agent_instance_test.go +++ b/go/core/internal/database/client_agent_instance_test.go @@ -786,178 +786,3 @@ func TestListAgentInstancesFiltersByAgentPair(t *testing.T) { }) } } - -// TestAbandonActiveAgentInstanceTaskFreesTheSlotForTheNextTurn is the store -// primitive behind a reader-requested cancel. -// -// A parked turn no longer blocks the next one: the active-task query now excludes -// INPUT_REQUIRED and AUTH_REQUIRED, so an unanswered question does not wedge the -// instance the way it used to. This test asserted that wedge as a fact, which it is -// not any more. -// -// Abandoning is still the primitive it was, and still worth having. A parked task -// cannot reach a terminal state on its own — nothing clears it, because the question -// stays valid until the reader gives it up — so this is how a reader says they will -// not be answering, and how the task gets an ending rather than staying open forever. -func TestAbandonActiveAgentInstanceTaskFreesTheSlotForTheNextTurn(t *testing.T) { - db := setupTestDB(t) - ctx := context.Background() - if _, err := db.Exec(ctx, ` - INSERT INTO a2a_context (id, namespace, user_id) - VALUES ('instance-1', 'team-a', 'alice'); - - INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data) - VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\x00') - `); err != nil { - t.Fatal(err) - } - client := NewClient(db) - - parked := newAgentInstanceTask("task-1", "message-1") - if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-1"), parked); err != nil { - t.Fatal(err) - } - parked.Status.State = a2a.TaskStateInputRequired - if err := client.StoreAgentInstanceTaskEvent(ctx, "instance-1", parked, parked, nil); err != nil { - t.Fatal(err) - } - if abandoned, err := client.AbandonActiveAgentInstanceTask(ctx, "instance-1", "different-task"); err != nil || abandoned { - t.Fatalf("AbandonActiveAgentInstanceTask(wrong task) = %v, %v", abandoned, err) - } - if abandoned, err := client.AbandonActiveAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || !abandoned { - t.Fatalf("AbandonActiveAgentInstanceTask() = %v, %v", abandoned, err) - } - stored, created, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-2"), newAgentInstanceTask("task-2", "message-2")) - if err != nil || !created || stored.ID != "task-2" { - t.Fatalf("send after abandoning = %#v, created %v, error %v", stored, created, err) - } - - closed, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") - if err != nil { - t.Fatal(err) - } - // Canceled, not failed: nothing went wrong with that turn, and its own message - // has to say what happened rather than borrowing the interruption wording. - if closed.Status.State != a2a.TaskStateCanceled { - t.Fatalf("abandoned task state = %s, want %s", closed.Status.State, a2a.TaskStateCanceled) - } - last := closed.History[len(closed.History)-1] - if last.Role != a2a.MessageRoleAgent || len(last.Parts) == 0 { - t.Fatalf("abandoned task's last message = %#v", last) - } - if text, ok := last.Parts[0].Content.(a2a.Text); !ok || string(text) != taskAbandonedMessage { - t.Fatalf("abandoned task's explanation = %#v, want the abandoned wording", last.Parts[0].Content) - } -} - -// TestClaimParkedAgentInstanceTaskIsTheReplayGuard pins the property the reply -// path relies on for idempotency, against real Postgres. The claim is the guard: -// it needs no extra bookkeeping because moving the task out of its parked state -// under a row lock is exactly what makes a second reply refusable. -func TestClaimParkedAgentInstanceTaskIsTheReplayGuard(t *testing.T) { - db := setupTestDB(t) - ctx := context.Background() - if _, err := db.Exec(ctx, ` - INSERT INTO a2a_context (id, namespace, user_id) - VALUES ('instance-1', 'team-a', 'alice'); - - INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data) - VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\x00') - `); err != nil { - t.Fatal(err) - } - client := NewClient(db) - - task := newAgentInstanceTask("task-1", "message-1") - if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-1"), task); err != nil { - t.Fatal(err) - } - - // A turn that is working, not parked, cannot be replied to. - if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || claimed { - t.Fatalf("ClaimParkedAgentInstanceTask(working) = %v, %v", claimed, err) - } - - task.Status.State = a2a.TaskStateInputRequired - if err := client.StoreAgentInstanceTaskEvent(ctx, "instance-1", task, task, nil); err != nil { - t.Fatal(err) - } - - // A reply naming a different task is refused, so it cannot answer for another. - if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-2"); err != nil || claimed { - t.Fatalf("ClaimParkedAgentInstanceTask(other task) = %v, %v", claimed, err) - } - - parked, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1") - if err != nil || !claimed { - t.Fatalf("ClaimParkedAgentInstanceTask() = %v, %v", claimed, err) - } - // The returned task is the parked one, which is what a failed delivery restores. - if parked.Status.State != a2a.TaskStateInputRequired { - t.Fatalf("returned task state = %s, want the parked state", parked.Status.State) - } - // The stored task has moved on, which is what refuses the duplicate below. - claimedTask, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") - if err != nil || claimedTask.Status.State != a2a.TaskStateWorking { - t.Fatalf("claimed task state = %v (%v), want working", claimedTask.Status.State, err) - } - if _, again, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || again { - t.Fatalf("second ClaimParkedAgentInstanceTask() = %v, %v — a duplicate reply was not refused", again, err) - } - - // Restoring puts the question back, and it is claimable again. - if err := client.RestoreParkedAgentInstanceTask(ctx, "instance-1", parked); err != nil { - t.Fatal(err) - } - restored, err := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") - if err != nil || restored.Status.State != a2a.TaskStateInputRequired { - t.Fatalf("restored task state = %v (%v), want the parked state", restored.Status.State, err) - } - if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || !claimed { - t.Fatalf("restored question is not answerable: %v, %v", claimed, err) - } - - // Restoring puts the question back, and a new turn is now allowed alongside it. - // - // This asserted the opposite until the active-task query stopped counting - // INPUT_REQUIRED: a standing question used to occupy the instance's one slot, so a - // reader who never answered could not start another turn at all. Answering is still - // the way to finish *that* turn; it is no longer the only way to have any turn. - if err := client.RestoreParkedAgentInstanceTask(ctx, "instance-1", parked); err != nil { - t.Fatal(err) - } - if _, _, err := client.CreateAgentInstanceTask(ctx, "instance-1", []byte("request-2"), newAgentInstanceTask("task-2", "message-2")); err != nil { - t.Fatalf("new turn while a question stands = %v, want it accepted", err) - } - // And the question is still there to be answered, rather than having been - // displaced by the turn that started beside it. - restored, restoreErr := client.GetAgentInstanceTask(ctx, "instance-1", "task-1") - if restoreErr != nil || !dbpkg.TaskParkedAwaitingUser(restored.Status.State) { - t.Fatalf("restored task = %v (%v), want it still parked", restored.Status.State, restoreErr) - } -} - -// TestClaimParkedAgentInstanceTaskRefusesATaskThatIsNotThere covers a reply naming a -// task this instance does not have. -// -// It used to answer `ErrNotFound`, because it asked for the instance's active task and -// there was none. It now addresses the task by id — a parked task stopped being the -// active one — so "no such task" and "that task is not parked" are the same answer: -// nothing was claimed, and the caller refuses the reply on that alone. -func TestClaimParkedAgentInstanceTaskRefusesATaskThatIsNotThere(t *testing.T) { - db := setupTestDB(t) - ctx := context.Background() - if _, err := db.Exec(ctx, ` - INSERT INTO a2a_context (id, namespace, user_id) - VALUES ('instance-1', 'team-a', 'alice'); - - INSERT INTO agent_instance (id, namespace, user_id, request_id, context_id, state, data) - VALUES ('instance-1', 'team-a', 'alice', 'request-1', 'instance-1', 'READY', '\x00') - `); err != nil { - t.Fatal(err) - } - client := NewClient(db) - if _, claimed, err := client.ClaimParkedAgentInstanceTask(ctx, "instance-1", "task-1"); err != nil || claimed { - t.Fatalf("ClaimParkedAgentInstanceTask() for a task that is not there = %v, %v", claimed, err) - } -} diff --git a/go/core/internal/database/client_postgres.go b/go/core/internal/database/client_postgres.go index 0c2cad789..8bba9bfc5 100644 --- a/go/core/internal/database/client_postgres.go +++ b/go/core/internal/database/client_postgres.go @@ -1060,12 +1060,6 @@ func (c *postgresClient) CreateAgentInstanceTask(ctx context.Context, instanceID // longer has an active execution for it. const taskInterruptedMessage = "The turn was interrupted before it completed, and the process running it is no longer reporting progress." -// taskAbandonedMessage explains a task closed because it was waiting on the -// reader and the reader started a new turn instead. It is deliberately not the -// interrupted wording: nothing went wrong, so saying the runtime stopped -// reporting progress would be a falsehood in the transcript. -const taskAbandonedMessage = "This turn was waiting for a reply and was closed when a new message started the next turn." - func (c *postgresClient) GetActiveAgentInstanceTask(ctx context.Context, instanceID string) (*a2a.Task, error) { row, err := c.q.GetActiveAgentInstanceTask(ctx, instanceID) if err != nil { @@ -1081,120 +1075,14 @@ func (c *postgresClient) GetActiveAgentInstanceTask(ctx context.Context, instanc // InterruptActiveAgentInstanceTask atomically fails taskID only if it is still the // instance's active task. func (c *postgresClient) InterruptActiveAgentInstanceTask(ctx context.Context, instanceID, taskID string) (bool, error) { - return c.terminateAgentInstanceTask(ctx, instanceID, taskID, a2a.TaskStateFailed, taskInterruptedMessage, true) -} - -// AbandonActiveAgentInstanceTask closes a task that was parked awaiting the -// reader, so the instance's single active-task slot is released. It is canceled -// rather than failed because nothing failed: the turn was waiting for input -// that never came. -func (c *postgresClient) AbandonActiveAgentInstanceTask(ctx context.Context, instanceID, taskID string) (bool, error) { - return c.terminateAgentInstanceTask(ctx, instanceID, taskID, a2a.TaskStateCanceled, taskAbandonedMessage, false) -} - -// ClaimParkedAgentInstanceTask moves a task that is waiting on the reader into -// TASK_STATE_WORKING so a reply can be delivered, and reports whether this call -// is the one that did it. -// -// The row lock is what makes it a replay guard: a duplicate reply serialises -// behind the first, sees a task that is no longer parked, and is refused rather -// than delivered twice. The returned task is the parked one as it stood *before* -// the claim, so a caller whose delivery fails can put the question back. -func (c *postgresClient) ClaimParkedAgentInstanceTask(ctx context.Context, instanceID, taskID string) (*a2a.Task, bool, error) { - var parked *a2a.Task - claimed := false - err := c.withTx(ctx, func(q *dbgen.Queries) error { - // By id, not "the active task". A parked turn no longer holds the instance's - // slot — an unanswered question must not stop the next turn — so asking for the - // active task finds something else, or nothing, and the reply lands nowhere. - row, err := q.LockAgentInstanceTask(ctx, dbgen.LockAgentInstanceTaskParams{ - ContextID: instanceID, ID: taskID, - }) - // Not claimed rather than an error: a reply naming a task this instance does - // not have is the same answer as one naming a task that is not parked — there - // is nothing here to reply to. Both leave `claimed` false, and the caller - // refuses the reply on that alone. - if errors.Is(err, pgx.ErrNoRows) { - return nil - } - if err != nil { - return fmt.Errorf("lock AgentInstance task: %w", err) - } - task, err := unmarshalAgentInstanceTask(row.Data) - if err != nil { - return err - } - if !dbpkg.TaskParkedAwaitingUser(task.Status.State) { - return nil - } - working := *task - now := time.Now() - working.Status = a2a.TaskStatus{State: a2a.TaskStateWorking, Timestamp: &now} - data, err := marshalAgentInstanceTask(&working) - if err != nil { - return err - } - if err := q.UpsertAgentInstanceTask(ctx, dbgen.UpsertAgentInstanceTaskParams{ - ContextID: instanceID, ID: string(working.ID), State: string(working.Status.State), - StatusTimestamp: working.Status.Timestamp, Data: data, - }); err != nil { - return fmt.Errorf("claim parked AgentInstance task %s: %w", working.ID, err) - } - parked, claimed = task, true - return nil - }) - if err != nil { - return nil, false, err - } - return parked, claimed, nil -} - -// RestoreParkedAgentInstanceTask puts a claimed task back exactly as it was, for -// a reply that never reached the runtime. The question stays answerable, which -// failing the task would not allow. -func (c *postgresClient) RestoreParkedAgentInstanceTask(ctx context.Context, instanceID string, task *a2a.Task) error { - data, err := marshalAgentInstanceTask(task) - if err != nil { - return err - } - if err := c.q.UpsertAgentInstanceTask(ctx, dbgen.UpsertAgentInstanceTaskParams{ - ContextID: instanceID, ID: string(task.ID), State: string(task.Status.State), - StatusTimestamp: task.Status.Timestamp, Data: data, - }); err != nil { - return fmt.Errorf("restore parked AgentInstance task %s: %w", task.ID, err) - } - return nil -} - -// terminateAgentInstanceTask atomically moves taskID to a terminal state. -// -// `requireActive` says which task the caller means, and the two callers mean -// different things. Interrupting is about the turn *in flight*, so it must not touch a -// task that a concurrent turn has already replaced — it asks for the active task and -// gives up unless that is the one named. Abandoning is about a turn parked awaiting an -// answer, and a parked task is no longer the active one: it stopped holding the -// instance's slot when the active-task query began excluding INPUT_REQUIRED, so asking -// for the active task would find something else, or nothing, and quietly do nothing. -func (c *postgresClient) terminateAgentInstanceTask( - ctx context.Context, instanceID, taskID string, state a2a.TaskState, reason string, - requireActive bool, -) (bool, error) { interruptedTask := false err := c.withTx(ctx, func(q *dbgen.Queries) error { - var row dbgen.AgentInstanceTask - var err error - if requireActive { - row, err = q.LockActiveAgentInstanceTask(ctx, instanceID) - } else { - row, err = q.LockAgentInstanceTask(ctx, dbgen.LockAgentInstanceTaskParams{ - ContextID: instanceID, ID: taskID, - }) - } + row, err := q.LockActiveAgentInstanceTask(ctx, instanceID) if errors.Is(err, pgx.ErrNoRows) { return nil } if err != nil { - return fmt.Errorf("lock AgentInstance task: %w", err) + return fmt.Errorf("lock active AgentInstance task: %w", err) } if row.ID != taskID { return nil @@ -1206,10 +1094,10 @@ func (c *postgresClient) terminateAgentInstanceTask( if err := loadAgentInstanceTaskHistories(ctx, q, instanceID, []*a2a.Task{task}); err != nil { return err } - interrupted := a2a.NewMessageForTask(a2a.MessageRoleAgent, task, a2a.NewTextPart(reason)) + interrupted := a2a.NewMessageForTask(a2a.MessageRoleAgent, task, a2a.NewTextPart(taskInterruptedMessage)) now := time.Now() task.History = append(task.History, interrupted) - task.Status = a2a.TaskStatus{State: state, Message: interrupted, Timestamp: &now} + task.Status = a2a.TaskStatus{State: a2a.TaskStateFailed, Message: interrupted, Timestamp: &now} data, err := marshalAgentInstanceTask(task) if err != nil { return err diff --git a/go/core/internal/database/gen/agent_instance_tasks.sql.go b/go/core/internal/database/gen/agent_instance_tasks.sql.go index 9bac4cf46..7068230ae 100644 --- a/go/core/internal/database/gen/agent_instance_tasks.sql.go +++ b/go/core/internal/database/gen/agent_instance_tasks.sql.go @@ -363,51 +363,10 @@ WHERE context_id = $1 FOR UPDATE ` -func (q *Queries) LockActiveAgentInstanceTask(ctx context.Context, contextID string) (AgentInstanceTask, error) { - row := q.db.QueryRow(ctx, lockActiveAgentInstanceTask, contextID) - var i AgentInstanceTask - err := row.Scan( - &i.ContextID, - &i.ID, - &i.State, - &i.StatusTimestamp, - &i.Data, - &i.CreatedAt, - &i.UpdatedAt, - &i.InitialMessageID, - &i.RequestHash, - &i.SnapshotAtespace, - &i.SnapshotName, - &i.SnapshotUid, - &i.SnapshotContentScope, - &i.HistorySequence, - ) - return i, err -} - -const lockAgentInstanceTask = `-- name: LockAgentInstanceTask :one -SELECT context_id, id, state, status_timestamp, data, created_at, updated_at, initial_message_id, request_hash, snapshot_atespace, snapshot_name, snapshot_uid, snapshot_content_scope, history_sequence FROM agent_instance_task -WHERE context_id = $1 AND id = $2 -FOR UPDATE -` - -type LockAgentInstanceTaskParams struct { - ContextID string - ID string -} - // LockActiveAgentInstanceTask holds the instance's non-terminal task for the // rest of the transaction so reclamation cannot overwrite concurrent progress. -// -// One task by id, whatever state it is in. -// -// Distinct from LockActiveAgentInstanceTask, which finds whichever task currently -// holds the instance's turn — and deliberately no longer counts a parked one, since a -// question awaiting an answer must not stop the next turn starting. The parked-task -// operations still need to reach that exact task to answer it or give it up, so they -// name it instead of asking for the active one. -func (q *Queries) LockAgentInstanceTask(ctx context.Context, arg LockAgentInstanceTaskParams) (AgentInstanceTask, error) { - row := q.db.QueryRow(ctx, lockAgentInstanceTask, arg.ContextID, arg.ID) +func (q *Queries) LockActiveAgentInstanceTask(ctx context.Context, contextID string) (AgentInstanceTask, error) { + row := q.db.QueryRow(ctx, lockActiveAgentInstanceTask, contextID) var i AgentInstanceTask err := row.Scan( &i.ContextID, diff --git a/go/core/internal/database/gen/querier.go b/go/core/internal/database/gen/querier.go index 94e424507..393f75149 100644 --- a/go/core/internal/database/gen/querier.go +++ b/go/core/internal/database/gen/querier.go @@ -118,19 +118,10 @@ type Querier interface { ListTools(ctx context.Context) ([]Tool, error) ListToolsForServer(ctx context.Context, arg ListToolsForServerParams) ([]Tool, error) ListUnreferencedRuntimeRevisions(ctx context.Context) ([]RuntimeRevision, error) - LockActiveAgentInstanceTask(ctx context.Context, contextID string) (AgentInstanceTask, error) - LockAgentInstance(ctx context.Context, id string) (AgentInstance, error) // LockActiveAgentInstanceTask holds the instance's non-terminal task for the // rest of the transaction so reclamation cannot overwrite concurrent progress. - // - // One task by id, whatever state it is in. - // - // Distinct from LockActiveAgentInstanceTask, which finds whichever task currently - // holds the instance's turn — and deliberately no longer counts a parked one, since a - // question awaiting an answer must not stop the next turn starting. The parked-task - // operations still need to reach that exact task to answer it or give it up, so they - // name it instead of asking for the active one. - LockAgentInstanceTask(ctx context.Context, arg LockAgentInstanceTaskParams) (AgentInstanceTask, error) + LockActiveAgentInstanceTask(ctx context.Context, contextID string) (AgentInstanceTask, error) + LockAgentInstance(ctx context.Context, id string) (AgentInstance, error) LockReadyAgentInstanceCheckpoint(ctx context.Context, arg LockReadyAgentInstanceCheckpointParams) (AgentInstanceCheckpoint, error) MarkAgentInstanceReady(ctx context.Context, arg MarkAgentInstanceReadyParams) (AgentInstance, error) MarkRuntimeRevisionSuccessful(ctx context.Context, arg MarkRuntimeRevisionSuccessfulParams) error diff --git a/go/core/internal/database/queries/agent_instance_tasks.sql b/go/core/internal/database/queries/agent_instance_tasks.sql index 50e6d85f6..17ee40bc1 100644 --- a/go/core/internal/database/queries/agent_instance_tasks.sql +++ b/go/core/internal/database/queries/agent_instance_tasks.sql @@ -98,19 +98,6 @@ INSERT INTO agent_instance_task ( -- LockActiveAgentInstanceTask holds the instance's non-terminal task for the -- rest of the transaction so reclamation cannot overwrite concurrent progress. --- name: LockAgentInstanceTask :one --- --- One task by id, whatever state it is in. --- --- Distinct from LockActiveAgentInstanceTask, which finds whichever task currently --- holds the instance's turn — and deliberately no longer counts a parked one, since a --- question awaiting an answer must not stop the next turn starting. The parked-task --- operations still need to reach that exact task to answer it or give it up, so they --- name it instead of asking for the active one. -SELECT * FROM agent_instance_task -WHERE context_id = $1 AND id = $2 -FOR UPDATE; - -- name: LockActiveAgentInstanceTask :one SELECT * FROM agent_instance_task WHERE context_id = $1 diff --git a/go/core/internal/grpcserver/agenttemplate.go b/go/core/internal/grpcserver/agenttemplate.go index 4cedf8706..048a53e86 100644 --- a/go/core/internal/grpcserver/agenttemplate.go +++ b/go/core/internal/grpcserver/agenttemplate.go @@ -29,8 +29,8 @@ func (s *agentTemplateServer) ListAgentTemplates(ctx context.Context, request *a return nil, err } templates := make([]*apiv1alpha1.AgentTemplate, 0, len(items)) - for index := range items { - template, err := s.agentTemplate(&items[index]) + for _, item := range items { + template, err := s.agentTemplate(item) if err != nil { return nil, err } diff --git a/go/core/internal/grpcserver/agenttemplate_harness_test.go b/go/core/internal/grpcserver/agenttemplate_harness_test.go index 71456295b..f926c7850 100644 --- a/go/core/internal/grpcserver/agenttemplate_harness_test.go +++ b/go/core/internal/grpcserver/agenttemplate_harness_test.go @@ -237,17 +237,6 @@ func TestHarnessServiceGeneratedClient(t *testing.T) { }) assertCode(t, err, codes.AlreadyExists) - updated, err := client.UpdateHarness(ctx, &apiv1alpha1.UpdateHarnessRequest{ - Ref: ref, - Resource: structured(t, testHarness("team", "a-created", "pool-b"), harnessKind), - }) - if err != nil { - t.Fatalf("UpdateHarness() error = %v", err) - } - if updated.GetHarness().GetRuntime() != harnessRuntimeCodex { - t.Fatalf("UpdateHarness() runtime = %q", updated.GetHarness().GetRuntime()) - } - listed, err := client.ListHarnesses(ctx, &apiv1alpha1.ListHarnessesRequest{Namespace: "team"}) if err != nil { t.Fatalf("ListHarnesses() error = %v", err) @@ -275,13 +264,6 @@ func TestHarnessServiceGeneratedClient(t *testing.T) { _, err = client.ListHarnesses(ctx, &apiv1alpha1.ListHarnessesRequest{}) assertCode(t, err, codes.InvalidArgument) - _, err = client.GetHarness(ctx, &apiv1alpha1.GetHarnessRequest{}) - assertCode(t, err, codes.InvalidArgument) - _, err = client.GetHarness(ctx, &apiv1alpha1.GetHarnessRequest{ - Ref: &apiv1alpha1.ResourceReference{Namespace: "team", Name: "absent"}, - }) - assertCode(t, err, codes.NotFound) - if _, err := client.DeleteHarness(ctx, &apiv1alpha1.DeleteHarnessRequest{Ref: ref}); err != nil { t.Fatalf("DeleteHarness() error = %v", err) } diff --git a/go/core/internal/grpcserver/harness.go b/go/core/internal/grpcserver/harness.go index 63ca7480c..c8999583b 100644 --- a/go/core/internal/grpcserver/harness.go +++ b/go/core/internal/grpcserver/harness.go @@ -39,8 +39,8 @@ func (s *harnessServer) ListHarnesses(ctx context.Context, request *apiv1alpha1. return nil, err } harnesses := make([]*apiv1alpha1.Harness, 0, len(items)) - for index := range items { - encoded, err := s.harness(&items[index]) + for _, item := range items { + encoded, err := s.harness(item) if err != nil { return nil, err } @@ -49,22 +49,6 @@ func (s *harnessServer) ListHarnesses(ctx context.Context, request *apiv1alpha1. return &apiv1alpha1.ListHarnessesResponse{Harnesses: harnesses}, nil } -func (s *harnessServer) GetHarness(ctx context.Context, request *apiv1alpha1.GetHarnessRequest) (*apiv1alpha1.GetHarnessResponse, error) { - ref, err := requiredHarnessRef(request.GetRef()) - if err != nil { - return nil, err - } - result, err := s.service.Get(ctx, ref) - if err != nil { - return nil, err - } - encoded, err := s.harness(result) - if err != nil { - return nil, err - } - return &apiv1alpha1.GetHarnessResponse{Harness: encoded}, nil -} - func (s *harnessServer) CreateHarness(ctx context.Context, request *apiv1alpha1.CreateHarnessRequest) (*apiv1alpha1.CreateHarnessResponse, error) { incoming := &v1alpha3.Harness{} if err := s.decodeResource(request.GetRef(), request.GetResource(), incoming); err != nil { @@ -81,26 +65,6 @@ func (s *harnessServer) CreateHarness(ctx context.Context, request *apiv1alpha1. return &apiv1alpha1.CreateHarnessResponse{Harness: encoded}, nil } -func (s *harnessServer) UpdateHarness(ctx context.Context, request *apiv1alpha1.UpdateHarnessRequest) (*apiv1alpha1.UpdateHarnessResponse, error) { - ref, err := requiredHarnessRef(request.GetRef()) - if err != nil { - return nil, err - } - incoming := &v1alpha3.Harness{} - if err := s.decodeResource(request.GetRef(), request.GetResource(), incoming); err != nil { - return nil, err - } - result, err := s.service.Update(ctx, ref, incoming) - if err != nil { - return nil, err - } - encoded, err := s.harness(result) - if err != nil { - return nil, err - } - return &apiv1alpha1.UpdateHarnessResponse{Harness: encoded}, nil -} - func (s *harnessServer) DeleteHarness(ctx context.Context, request *apiv1alpha1.DeleteHarnessRequest) (*apiv1alpha1.DeleteHarnessResponse, error) { ref, err := requiredHarnessRef(request.GetRef()) if err != nil { diff --git a/go/core/internal/grpcserver/policy.go b/go/core/internal/grpcserver/policy.go index c69d2a7e2..d77d347f8 100644 --- a/go/core/internal/grpcserver/policy.go +++ b/go/core/internal/grpcserver/policy.go @@ -81,18 +81,13 @@ func DefaultMethodPolicies() MethodPolicies { grpc_health_v1.Health_Watch_FullMethodName: AccessPublic, "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo": AccessPublic, "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo": AccessPublic, - apiv1alpha1.SystemService_GetSubstrateSummary_FullMethodName: AccessRead, - apiv1alpha1.SystemService_ListSubstrateActors_FullMethodName: AccessRead, - apiv1alpha1.SystemService_ListSubstrateWorkers_FullMethodName: AccessRead, apiv1alpha1.AgentTemplateService_ListAgentTemplates_FullMethodName: AccessRead, apiv1alpha1.AgentTemplateService_GetAgentTemplate_FullMethodName: AccessRead, apiv1alpha1.AgentTemplateService_CreateAgentTemplate_FullMethodName: AccessCreate, apiv1alpha1.AgentTemplateService_UpdateAgentTemplate_FullMethodName: AccessUpdate, apiv1alpha1.AgentTemplateService_DeleteAgentTemplate_FullMethodName: AccessDelete, apiv1alpha1.HarnessService_ListHarnesses_FullMethodName: AccessRead, - apiv1alpha1.HarnessService_GetHarness_FullMethodName: AccessRead, apiv1alpha1.HarnessService_CreateHarness_FullMethodName: AccessCreate, - apiv1alpha1.HarnessService_UpdateHarness_FullMethodName: AccessUpdate, apiv1alpha1.HarnessService_DeleteHarness_FullMethodName: AccessDelete, } policies[apiv1alpha1.AgentInstanceService_CreateAgentInstance_FullMethodName] = AccessCreate diff --git a/go/core/internal/grpcserver/protovalidate_test.go b/go/core/internal/grpcserver/protovalidate_test.go index ef8d3927a..f5429bce5 100644 --- a/go/core/internal/grpcserver/protovalidate_test.go +++ b/go/core/internal/grpcserver/protovalidate_test.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) func TestProtovalidateUnaryInterceptor(t *testing.T) { @@ -38,3 +39,29 @@ func TestProtovalidateUnaryInterceptor(t *testing.T) { t.Fatalf("validation details = %d, want 1", len(details)) } } + +func TestAgentInstanceRequestValidation(t *testing.T) { + validator, err := protovalidate.New() + if err != nil { + t.Fatal(err) + } + for _, test := range []struct { + name string + request proto.Message + valid bool + }{ + {"ordinary name", &apiv1alpha1.CreateAgentInstanceRequest{Namespace: "team-a", Harness: "kagent", AgentTemplate: "assistant", RequestId: "request-1", Name: "Deploy 🚀"}, true}, + {"leading whitespace", &apiv1alpha1.CreateAgentInstanceRequest{Namespace: "team-a", Harness: "kagent", AgentTemplate: "assistant", RequestId: "request-1", Name: " title"}, false}, + {"control character", &apiv1alpha1.CreateAgentInstanceRequest{Namespace: "team-a", Harness: "kagent", AgentTemplate: "assistant", RequestId: "request-1", Name: "first\nsecond"}, false}, + {"invalid template filter", &apiv1alpha1.ListAgentInstancesRequest{Namespace: "team-a", AgentTemplate: "NOT A NAME"}, false}, + {"valid rename", &apiv1alpha1.RenameAgentInstanceRequest{Namespace: "team-a", AgentInstanceId: "11111111-1111-4111-8111-111111111111", Name: "New title"}, true}, + {"invalid rename id", &apiv1alpha1.RenameAgentInstanceRequest{Namespace: "team-a", AgentInstanceId: "not-a-uuid", Name: "New title"}, false}, + } { + t.Run(test.name, func(t *testing.T) { + err := validator.Validate(test.request) + if (err == nil) != test.valid { + t.Fatalf("Validate() error = %v, valid = %t", err, test.valid) + } + }) + } +} diff --git a/go/core/internal/grpcserver/system.go b/go/core/internal/grpcserver/system.go index 2667ab909..32c42222a 100644 --- a/go/core/internal/grpcserver/system.go +++ b/go/core/internal/grpcserver/system.go @@ -7,7 +7,6 @@ import ( "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" systemservice "github.com/kagent-dev/kagent/go/core/internal/service/system" "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" ) type systemServer struct { @@ -55,203 +54,6 @@ func (s *systemServer) ListNamespaces(ctx context.Context, _ *apiv1alpha1.ListNa return &apiv1alpha1.ListNamespacesResponse{Namespaces: namespaces}, nil } -func (s *systemServer) GetSubstrateSummary(ctx context.Context, request *apiv1alpha1.GetSubstrateSummaryRequest) (*apiv1alpha1.GetSubstrateSummaryResponse, error) { - result, err := s.service.GetSubstrateSummary(ctx, request.GetNamespace()) - if err != nil { - return nil, err - } - response := &apiv1alpha1.GetSubstrateSummaryResponse{ - Enabled: result.Enabled, - AteApiError: result.ATEAPIError, - WorkerPools: make([]*apiv1alpha1.SubstrateWorkerPool, 0, len(result.WorkerPools)), - ActorTemplates: make([]*apiv1alpha1.SubstrateActorTemplate, 0, len(result.ActorTemplates)), - ActorCount: result.ActorCount, - WorkerCount: result.WorkerCount, - RunningActorCount: result.RunningActorCount, - BusyWorkerCount: result.BusyWorkerCount, - ActorStatusCounts: make([]*apiv1alpha1.SubstrateStatusCount, 0, len(result.ActorStatusCounts)), - ComputedAt: timestamppb.New(result.ComputedAt), - } - for _, workerPool := range result.WorkerPools { - response.WorkerPools = append(response.WorkerPools, workerPoolToProto(workerPool)) - } - for _, actorTemplate := range result.ActorTemplates { - response.ActorTemplates = append(response.ActorTemplates, actorTemplateToProto(actorTemplate)) - } - for _, statusCount := range result.ActorStatusCounts { - response.ActorStatusCounts = append(response.ActorStatusCounts, &apiv1alpha1.SubstrateStatusCount{ - Status: statusCount.Status, - Count: statusCount.Count, - }) - } - return response, nil -} - -/* - * The sort enums, mapped both ways. - * - * Written out rather than derived, and keyed by the generated enum so a member - * added to the proto fails the build here instead of being served as a zero. The - * response reports the order that was *applied*, which is why the outbound - * direction exists at all: a caller should be able to say how its rows are sorted - * rather than assume its request was honoured. - */ -var actorSortFieldFromProto = map[apiv1alpha1.SubstrateActorSortField]systemservice.ActorSortField{ - apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED: systemservice.ActorSortDefault, - apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_STATUS: systemservice.ActorSortStatus, - apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID: systemservice.ActorSortID, - apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE: systemservice.ActorSortTemplate, - apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD: systemservice.ActorSortWorker, -} - -var actorSortFieldToProto = map[systemservice.ActorSortField]apiv1alpha1.SubstrateActorSortField{ - systemservice.ActorSortDefault: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED, - systemservice.ActorSortStatus: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_STATUS, - systemservice.ActorSortID: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID, - systemservice.ActorSortTemplate: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE, - systemservice.ActorSortWorker: apiv1alpha1.SubstrateActorSortField_SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD, -} - -var workerSortFieldFromProto = map[apiv1alpha1.SubstrateWorkerSortField]systemservice.WorkerSortField{ - apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED: systemservice.WorkerSortDefault, - apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POOL: systemservice.WorkerSortPool, - apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POD: systemservice.WorkerSortPod, - apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_ACTOR: systemservice.WorkerSortActor, -} - -var workerSortFieldToProto = map[systemservice.WorkerSortField]apiv1alpha1.SubstrateWorkerSortField{ - systemservice.WorkerSortDefault: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED, - systemservice.WorkerSortPool: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POOL, - systemservice.WorkerSortPod: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_POD, - systemservice.WorkerSortActor: apiv1alpha1.SubstrateWorkerSortField_SUBSTRATE_WORKER_SORT_FIELD_ACTOR, -} - -func sortOrderFromProto(order apiv1alpha1.SubstrateSortOrder) systemservice.SortOrder { - if order == apiv1alpha1.SubstrateSortOrder_SUBSTRATE_SORT_ORDER_DESCENDING { - return systemservice.SortDescending - } - return systemservice.SortAscending -} - -func sortOrderToProto(order systemservice.SortOrder) apiv1alpha1.SubstrateSortOrder { - if order == systemservice.SortDescending { - return apiv1alpha1.SubstrateSortOrder_SUBSTRATE_SORT_ORDER_DESCENDING - } - return apiv1alpha1.SubstrateSortOrder_SUBSTRATE_SORT_ORDER_ASCENDING -} - -func (s *systemServer) ListSubstrateActors(ctx context.Context, request *apiv1alpha1.ListSubstrateActorsRequest) (*apiv1alpha1.ListSubstrateActorsResponse, error) { - result, err := s.service.ListSubstrateActors(ctx, systemservice.ListActorsRequest{ - Namespace: request.GetNamespace(), - Filter: request.GetFilter(), - Limit: request.GetPage().GetLimit(), - PageToken: request.GetPage().GetPageToken(), - // A field this build does not know maps to the default order rather than - // being refused: a newer caller asking for a column added later gets a - // coherent page, and the response says which order it actually got. - SortField: actorSortFieldFromProto[request.GetSortField()], - SortOrder: sortOrderFromProto(request.GetSortOrder()), - }) - if err != nil { - return nil, err - } - response := &apiv1alpha1.ListSubstrateActorsResponse{ - Actors: make([]*apiv1alpha1.SubstrateActor, 0, len(result.Actors)), - Page: &apiv1alpha1.PageResponse{NextPageToken: result.NextPageToken}, - TotalSize: result.TotalSize, - AppliedSortField: actorSortFieldToProto[result.SortField], - AppliedSortOrder: sortOrderToProto(result.SortOrder), - ComputedAt: timestamppb.New(result.ComputedAt), - } - for _, actor := range result.Actors { - response.Actors = append(response.Actors, actorToProto(actor)) - } - return response, nil -} - -func (s *systemServer) ListSubstrateWorkers(ctx context.Context, request *apiv1alpha1.ListSubstrateWorkersRequest) (*apiv1alpha1.ListSubstrateWorkersResponse, error) { - result, err := s.service.ListSubstrateWorkers(ctx, systemservice.ListWorkersRequest{ - Namespace: request.GetNamespace(), - Filter: request.GetFilter(), - Limit: request.GetPage().GetLimit(), - PageToken: request.GetPage().GetPageToken(), - SortField: workerSortFieldFromProto[request.GetSortField()], - SortOrder: sortOrderFromProto(request.GetSortOrder()), - }) - if err != nil { - return nil, err - } - response := &apiv1alpha1.ListSubstrateWorkersResponse{ - Workers: make([]*apiv1alpha1.SubstrateWorker, 0, len(result.Workers)), - Page: &apiv1alpha1.PageResponse{NextPageToken: result.NextPageToken}, - TotalSize: result.TotalSize, - AppliedSortField: workerSortFieldToProto[result.SortField], - AppliedSortOrder: sortOrderToProto(result.SortOrder), - ComputedAt: timestamppb.New(result.ComputedAt), - } - for _, worker := range result.Workers { - response.Workers = append(response.Workers, workerToProto(worker)) - } - return response, nil -} - -// The four row conversions, shared by GetSubstrateStatus and the paged reads -// that replaced it, so the same record cannot arrive shaped differently -// depending on which RPC a caller used. - -func workerPoolToProto(workerPool systemservice.SubstrateWorkerPool) *apiv1alpha1.SubstrateWorkerPool { - return &apiv1alpha1.SubstrateWorkerPool{ - Namespace: workerPool.Namespace, - Name: workerPool.Name, - Replicas: workerPool.Replicas, - AteomImage: workerPool.AteomImage, - } -} - -func actorTemplateToProto(actorTemplate systemservice.SubstrateActorTemplate) *apiv1alpha1.SubstrateActorTemplate { - return &apiv1alpha1.SubstrateActorTemplate{ - Namespace: actorTemplate.Namespace, - Name: actorTemplate.Name, - Phase: actorTemplate.Phase, - GoldenActorId: actorTemplate.GoldenActorID, - GoldenSnapshot: actorTemplate.GoldenSnapshot, - SandboxClass: actorTemplate.SandboxClass, - WorkerSelector: actorTemplate.WorkerSelector, - HarnessName: actorTemplate.HarnessName, - ManagedByKagent: actorTemplate.ManagedByKagent, - } -} - -func actorToProto(actor systemservice.SubstrateActor) *apiv1alpha1.SubstrateActor { - return &apiv1alpha1.SubstrateActor{ - ActorId: actor.ActorID, - Atespace: actor.Atespace, - Status: actor.Status, - ActorTemplateNamespace: actor.ActorTemplateNamespace, - ActorTemplateName: actor.ActorTemplateName, - AteomPodNamespace: actor.AteomPodNamespace, - AteomPodName: actor.AteomPodName, - AteomPodIp: actor.AteomPodIP, - LatestSnapshot: actor.LatestSnapshot, - WorkerPoolName: actor.WorkerPoolName, - InProgressSnapshot: actor.InProgressSnapshot, - Version: actor.Version, - } -} - -func workerToProto(worker systemservice.SubstrateWorker) *apiv1alpha1.SubstrateWorker { - return &apiv1alpha1.SubstrateWorker{ - WorkerNamespace: worker.WorkerNamespace, - WorkerPool: worker.WorkerPool, - WorkerPod: worker.WorkerPod, - ActorNamespace: worker.ActorNamespace, - ActorTemplate: worker.ActorTemplate, - ActorId: worker.ActorID, - Ip: worker.IP, - Version: worker.Version, - } -} - func (s *systemServer) GetSubstrateStatus(ctx context.Context, request *apiv1alpha1.GetSubstrateStatusRequest) (*apiv1alpha1.GetSubstrateStatusResponse, error) { result, err := s.service.GetSubstrateStatus(ctx, request.GetNamespace()) if err != nil { @@ -266,16 +68,53 @@ func (s *systemServer) GetSubstrateStatus(ctx context.Context, request *apiv1alp Workers: make([]*apiv1alpha1.SubstrateWorker, 0, len(result.Workers)), } for _, workerPool := range result.WorkerPools { - response.WorkerPools = append(response.WorkerPools, workerPoolToProto(workerPool)) + response.WorkerPools = append(response.WorkerPools, &apiv1alpha1.SubstrateWorkerPool{ + Namespace: workerPool.Namespace, + Name: workerPool.Name, + Replicas: workerPool.Replicas, + AteomImage: workerPool.AteomImage, + }) } for _, actorTemplate := range result.ActorTemplates { - response.ActorTemplates = append(response.ActorTemplates, actorTemplateToProto(actorTemplate)) + response.ActorTemplates = append(response.ActorTemplates, &apiv1alpha1.SubstrateActorTemplate{ + Namespace: actorTemplate.Namespace, + Name: actorTemplate.Name, + Phase: actorTemplate.Phase, + GoldenActorId: actorTemplate.GoldenActorID, + GoldenSnapshot: actorTemplate.GoldenSnapshot, + SandboxClass: actorTemplate.SandboxClass, + WorkerSelector: actorTemplate.WorkerSelector, + HarnessName: actorTemplate.HarnessName, + ManagedByKagent: actorTemplate.ManagedByKagent, + }) } for _, actor := range result.Actors { - response.Actors = append(response.Actors, actorToProto(actor)) + response.Actors = append(response.Actors, &apiv1alpha1.SubstrateActor{ + ActorId: actor.ActorID, + Atespace: actor.Atespace, + Status: actor.Status, + ActorTemplateNamespace: actor.ActorTemplateNamespace, + ActorTemplateName: actor.ActorTemplateName, + AteomPodNamespace: actor.AteomPodNamespace, + AteomPodName: actor.AteomPodName, + AteomPodIp: actor.AteomPodIP, + LatestSnapshot: actor.LatestSnapshot, + WorkerPoolName: actor.WorkerPoolName, + InProgressSnapshot: actor.InProgressSnapshot, + Version: actor.Version, + }) } for _, worker := range result.Workers { - response.Workers = append(response.Workers, workerToProto(worker)) + response.Workers = append(response.Workers, &apiv1alpha1.SubstrateWorker{ + WorkerNamespace: worker.WorkerNamespace, + WorkerPool: worker.WorkerPool, + WorkerPod: worker.WorkerPod, + ActorNamespace: worker.ActorNamespace, + ActorTemplate: worker.ActorTemplate, + ActorId: worker.ActorID, + Ip: worker.IP, + Version: worker.Version, + }) } return response, nil } diff --git a/go/core/internal/service/agenttemplate/service.go b/go/core/internal/service/agenttemplate/service.go index 864872998..0f0964c7c 100644 --- a/go/core/internal/service/agenttemplate/service.go +++ b/go/core/internal/service/agenttemplate/service.go @@ -1,180 +1,43 @@ -// Package agenttemplate serves CRUD over the kagent.dev/v1alpha3 AgentTemplate -// CRD, the portable-behavior half of the (Harness, AgentTemplate) pair that -// AgentInstanceService.CreateAgentInstance names. package agenttemplate import ( - "cmp" "context" - "fmt" - "slices" "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/internal/service/kubecrud" "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" "github.com/kagent-dev/kagent/go/core/pkg/auth" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - utilvalidation "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" ) -// resourceType is the authorizer's name for this kind. It is deliberately not -// "Agent": an AgentTemplate is authored and shared like a template, and the -// AgentInstances created from it are what carry per-agent authorization. -const resourceType = "AgentTemplate" - type Service struct { - kubeClient client.Client - authorizer auth.Authorizer + *kubecrud.Service[*v1alpha3.AgentTemplate, *v1alpha3.AgentTemplateList] } -func NewService(kubeClient client.Client, authorizer auth.Authorizer) *Service { - return &Service{kubeClient: kubeClient, authorizer: authorizer} +func NewService(client client.Client, authorizer auth.Authorizer) *Service { + return &Service{Service: kubecrud.NewService( + client, authorizer, &v1alpha3.AgentTemplate{}, &v1alpha3.AgentTemplateList{}, "AgentTemplate", + )} } -func (s *Service) List(ctx context.Context, namespace string) ([]v1alpha3.AgentTemplate, error) { - if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType}); err != nil { - return nil, err - } - if namespace == "" { - return nil, serviceerrors.NewInvalidArgument("namespace is required", nil) - } - - list := &v1alpha3.AgentTemplateList{} - if err := s.kubeClient.List(ctx, list, client.InNamespace(namespace)); err != nil { - return nil, serviceerrors.NewInternal("Failed to list AgentTemplates", err) - } - slices.SortFunc(list.Items, func(left, right v1alpha3.AgentTemplate) int { - return cmp.Compare(left.Name, right.Name) - }) - return list.Items, nil -} - -func (s *Service) Get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.AgentTemplate, error) { - if err := validateRef(ref); err != nil { - return nil, err - } - if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return nil, err - } - return s.get(ctx, ref) -} - -func (s *Service) Create(ctx context.Context, template *v1alpha3.AgentTemplate) (*v1alpha3.AgentTemplate, error) { - if template == nil { +func (s *Service) Create(ctx context.Context, incoming *v1alpha3.AgentTemplate) (*v1alpha3.AgentTemplate, error) { + if incoming == nil { return nil, serviceerrors.NewInvalidArgument("AgentTemplate resource is required", nil) } - ref := types.NamespacedName{Namespace: template.Namespace, Name: template.Name} - if err := validateNewRef(ref); err != nil { - return nil, err - } - if err := s.authorize(ctx, auth.VerbCreate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return nil, err - } - - // Status is controller-owned, so a caller that round-trips a Get into a - // Create cannot assert readiness it has not earned. - created := template.DeepCopy() + created := incoming.DeepCopy() created.Status = v1alpha3.AgentTemplateStatus{} - if err := s.kubeClient.Create(ctx, created); err != nil { - if apierrors.IsAlreadyExists(err) { - return nil, serviceerrors.NewAlreadyExists("An AgentTemplate with this name already exists in the namespace", err) - } - if apierrors.IsInvalid(err) { - return nil, serviceerrors.NewInvalidArgument("Invalid AgentTemplate", err) - } - return nil, serviceerrors.NewInternal("Failed to create AgentTemplate", err) - } - return created, nil + return s.Service.Create(ctx, created) } -func (s *Service) Update(ctx context.Context, ref types.NamespacedName, template *v1alpha3.AgentTemplate) (*v1alpha3.AgentTemplate, error) { - if template == nil { +func (s *Service) Update(ctx context.Context, ref types.NamespacedName, incoming *v1alpha3.AgentTemplate) (*v1alpha3.AgentTemplate, error) { + if incoming == nil { return nil, serviceerrors.NewInvalidArgument("AgentTemplate resource is required", nil) } - if err := validateRef(ref); err != nil { - return nil, err - } - if err := s.authorize(ctx, auth.VerbUpdate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return nil, err - } - - // The spec is applied onto the stored object rather than the incoming one - // being written wholesale: that keeps the caller's stale resourceVersion, - // labels and controller-written status from silently overwriting the live - // object, so an update is a spec change and nothing else. - existing, err := s.get(ctx, ref) + existing, err := s.GetForUpdate(ctx, ref) if err != nil { return nil, err } - existing.Spec = *template.Spec.DeepCopy() - if err := s.kubeClient.Update(ctx, existing); err != nil { - if apierrors.IsInvalid(err) { - return nil, serviceerrors.NewInvalidArgument("Invalid AgentTemplate", err) - } - return nil, serviceerrors.NewInternal("Failed to update AgentTemplate", err) - } - return existing, nil -} - -func (s *Service) Delete(ctx context.Context, ref types.NamespacedName) error { - if err := validateRef(ref); err != nil { - return err - } - if err := s.authorize(ctx, auth.VerbDelete, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return err - } - - existing, err := s.get(ctx, ref) - if err != nil { - return err - } - if err := s.kubeClient.Delete(ctx, existing); err != nil { - return serviceerrors.NewInternal("Failed to delete AgentTemplate", err) - } - return nil -} - -func (s *Service) get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.AgentTemplate, error) { - template := &v1alpha3.AgentTemplate{} - if err := s.kubeClient.Get(ctx, ref, template); err != nil { - if apierrors.IsNotFound(err) { - return nil, serviceerrors.NewNotFound("AgentTemplate not found", err) - } - return nil, serviceerrors.NewInternal("Failed to get AgentTemplate", err) - } - return template, nil -} - -func (s *Service) authorize(ctx context.Context, verb auth.Verb, resource auth.Resource) error { - session, ok := auth.AuthSessionFrom(ctx) - if !ok || session == nil { - return serviceerrors.NewUnauthenticated("Failed to get authenticated principal", fmt.Errorf("no session found")) - } - if err := s.authorizer.Check(ctx, session.Principal(), verb, resource); err != nil { - return serviceerrors.NewPermissionDenied("Not authorized", err) - } - return nil -} - -func validateRef(ref types.NamespacedName) error { - if ref.Namespace == "" || ref.Name == "" { - return serviceerrors.NewInvalidArgument("AgentTemplate namespace and name are required", nil) - } - return nil -} - -// validateNewRef additionally rejects names the apiserver would reject, so a -// create fails with an actionable message rather than a wrapped 422. -func validateNewRef(ref types.NamespacedName) error { - if err := validateRef(ref); err != nil { - return err - } - if len(utilvalidation.IsDNS1123Subdomain(ref.Namespace)) > 0 { - return serviceerrors.NewInvalidArgument("namespace must be a valid DNS subdomain", nil) - } - if len(utilvalidation.IsDNS1123Subdomain(ref.Name)) > 0 { - return serviceerrors.NewInvalidArgument("name must be a valid DNS subdomain", nil) - } - return nil + existing.Spec = *incoming.Spec.DeepCopy() + return s.SaveUpdate(ctx, existing) } diff --git a/go/core/internal/service/harness/service.go b/go/core/internal/service/harness/service.go index 7c4a7b6bc..26af59607 100644 --- a/go/core/internal/service/harness/service.go +++ b/go/core/internal/service/harness/service.go @@ -1,186 +1,30 @@ -// Package harness serves CRUD over the kagent.dev/v1alpha3 Harness CRD, the -// runtime and infrastructure half of the (Harness, AgentTemplate) pair that -// AgentInstanceService.CreateAgentInstance names. -// -// Harness is not AgentHarness. The agent service's GetAgentHarness / -// CreateAgentHarness / DeleteAgentHarness operate on the AgentHarness CRD — a -// single agent bound to an external ACP backend — and share nothing with this -// kind beyond a substring. A Harness is a reusable runtime that admits many -// AgentTemplates by label selector; go/core/v2/controller/collections.go pairs -// the two. Keeping them in separate packages is what stops the next reader -// from wiring one service into the other's RPCs. package harness import ( - "cmp" "context" - "fmt" - "slices" "github.com/kagent-dev/kagent/go/api/v1alpha3" + "github.com/kagent-dev/kagent/go/core/internal/service/kubecrud" "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" "github.com/kagent-dev/kagent/go/core/pkg/auth" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - utilvalidation "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" ) -const resourceType = "Harness" - type Service struct { - kubeClient client.Client - authorizer auth.Authorizer -} - -func NewService(kubeClient client.Client, authorizer auth.Authorizer) *Service { - return &Service{kubeClient: kubeClient, authorizer: authorizer} -} - -func (s *Service) List(ctx context.Context, namespace string) ([]v1alpha3.Harness, error) { - if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType}); err != nil { - return nil, err - } - if namespace == "" { - return nil, serviceerrors.NewInvalidArgument("namespace is required", nil) - } - - list := &v1alpha3.HarnessList{} - if err := s.kubeClient.List(ctx, list, client.InNamespace(namespace)); err != nil { - return nil, serviceerrors.NewInternal("Failed to list Harnesses", err) - } - slices.SortFunc(list.Items, func(left, right v1alpha3.Harness) int { - return cmp.Compare(left.Name, right.Name) - }) - return list.Items, nil + *kubecrud.Service[*v1alpha3.Harness, *v1alpha3.HarnessList] } -func (s *Service) Get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.Harness, error) { - if err := validateRef(ref); err != nil { - return nil, err - } - if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return nil, err - } - return s.get(ctx, ref) +func NewService(client client.Client, authorizer auth.Authorizer) *Service { + return &Service{Service: kubecrud.NewService( + client, authorizer, &v1alpha3.Harness{}, &v1alpha3.HarnessList{}, "Harness", + )} } func (s *Service) Create(ctx context.Context, incoming *v1alpha3.Harness) (*v1alpha3.Harness, error) { if incoming == nil { return nil, serviceerrors.NewInvalidArgument("Harness resource is required", nil) } - ref := types.NamespacedName{Namespace: incoming.Namespace, Name: incoming.Name} - if err := validateNewRef(ref); err != nil { - return nil, err - } - if err := s.authorize(ctx, auth.VerbCreate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return nil, err - } - - // Status carries the controller's capability record, which is proven for a - // pinned adapter and image rather than declared. A caller must not be able - // to seed it by round-tripping a Get into a Create. created := incoming.DeepCopy() created.Status = v1alpha3.HarnessStatus{} - if err := s.kubeClient.Create(ctx, created); err != nil { - if apierrors.IsAlreadyExists(err) { - return nil, serviceerrors.NewAlreadyExists("A Harness with this name already exists in the namespace", err) - } - if apierrors.IsInvalid(err) { - return nil, serviceerrors.NewInvalidArgument("Invalid Harness", err) - } - return nil, serviceerrors.NewInternal("Failed to create Harness", err) - } - return created, nil -} - -func (s *Service) Update(ctx context.Context, ref types.NamespacedName, incoming *v1alpha3.Harness) (*v1alpha3.Harness, error) { - if incoming == nil { - return nil, serviceerrors.NewInvalidArgument("Harness resource is required", nil) - } - if err := validateRef(ref); err != nil { - return nil, err - } - if err := s.authorize(ctx, auth.VerbUpdate, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return nil, err - } - - // The spec is applied onto the stored object rather than the incoming one - // being written wholesale: that keeps the caller's stale resourceVersion, - // labels and controller-written status from silently overwriting the live - // object, so an update is a spec change and nothing else. - existing, err := s.get(ctx, ref) - if err != nil { - return nil, err - } - existing.Spec = *incoming.Spec.DeepCopy() - if err := s.kubeClient.Update(ctx, existing); err != nil { - if apierrors.IsInvalid(err) { - return nil, serviceerrors.NewInvalidArgument("Invalid Harness", err) - } - return nil, serviceerrors.NewInternal("Failed to update Harness", err) - } - return existing, nil -} - -func (s *Service) Delete(ctx context.Context, ref types.NamespacedName) error { - if err := validateRef(ref); err != nil { - return err - } - if err := s.authorize(ctx, auth.VerbDelete, auth.Resource{Type: resourceType, Name: ref.String()}); err != nil { - return err - } - - existing, err := s.get(ctx, ref) - if err != nil { - return err - } - if err := s.kubeClient.Delete(ctx, existing); err != nil { - return serviceerrors.NewInternal("Failed to delete Harness", err) - } - return nil -} - -func (s *Service) get(ctx context.Context, ref types.NamespacedName) (*v1alpha3.Harness, error) { - result := &v1alpha3.Harness{} - if err := s.kubeClient.Get(ctx, ref, result); err != nil { - if apierrors.IsNotFound(err) { - return nil, serviceerrors.NewNotFound("Harness not found", err) - } - return nil, serviceerrors.NewInternal("Failed to get Harness", err) - } - return result, nil -} - -func (s *Service) authorize(ctx context.Context, verb auth.Verb, resource auth.Resource) error { - session, ok := auth.AuthSessionFrom(ctx) - if !ok || session == nil { - return serviceerrors.NewUnauthenticated("Failed to get authenticated principal", fmt.Errorf("no session found")) - } - if err := s.authorizer.Check(ctx, session.Principal(), verb, resource); err != nil { - return serviceerrors.NewPermissionDenied("Not authorized", err) - } - return nil -} - -func validateRef(ref types.NamespacedName) error { - if ref.Namespace == "" || ref.Name == "" { - return serviceerrors.NewInvalidArgument("Harness namespace and name are required", nil) - } - return nil -} - -// validateNewRef additionally rejects names the apiserver would reject, so a -// create fails with an actionable message rather than a wrapped 422. -func validateNewRef(ref types.NamespacedName) error { - if err := validateRef(ref); err != nil { - return err - } - if len(utilvalidation.IsDNS1123Subdomain(ref.Namespace)) > 0 { - return serviceerrors.NewInvalidArgument("namespace must be a valid DNS subdomain", nil) - } - if len(utilvalidation.IsDNS1123Subdomain(ref.Name)) > 0 { - return serviceerrors.NewInvalidArgument("name must be a valid DNS subdomain", nil) - } - return nil + return s.Service.Create(ctx, created) } diff --git a/go/core/internal/service/harness/service_test.go b/go/core/internal/service/harness/service_test.go index e8f026c7c..9a0dbc712 100644 --- a/go/core/internal/service/harness/service_test.go +++ b/go/core/internal/service/harness/service_test.go @@ -87,36 +87,15 @@ func TestCRUD(t *testing.T) { require.NoError(t, err) assert.Equal(t, testImage, fetched.Spec.Workload.Image) - updated, err := service.Update(ctx, ref, fixture("team", "shared", "pool-b")) - require.NoError(t, err) - assert.Equal(t, "pool-b", updated.Spec.Substrate.WorkerPoolRef.Name) - require.NoError(t, service.Delete(ctx, ref)) _, err = service.Get(ctx, ref) assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeNotFound), err) } // The capability record in status is proven by the controller for a pinned -// adapter and image, so an edit must not blank it and a create must not seed it. +// adapter and image, so a create must not seed it. func TestStatusIsControllerOwned(t *testing.T) { - existing := fixture("team", "shared", "pool-a") - existing.Status = v1alpha3.HarnessStatus{ - Capabilities: &v1alpha3.HarnessCapabilities{Version: "v1", Streaming: true}, - Conditions: []metav1.Condition{{ - Type: v1alpha3.HarnessConditionTypeReady, - Status: metav1.ConditionTrue, - Reason: "Ready", - }}, - } - service, ctx := newService(t, &authimpl.NoopAuthorizer{}, existing) - - updated, err := service.Update(ctx, - types.NamespacedName{Namespace: "team", Name: "shared"}, - fixture("team", "shared", "pool-b")) - require.NoError(t, err) - assert.Equal(t, "pool-b", updated.Spec.Substrate.WorkerPoolRef.Name) - require.NotNil(t, updated.Status.Capabilities) - assert.Equal(t, "v1", updated.Status.Capabilities.Version) + service, ctx := newService(t, &authimpl.NoopAuthorizer{}) forged := fixture("team", "forged", "pool-a") forged.Status = v1alpha3.HarnessStatus{ @@ -174,14 +153,6 @@ func TestInvalidArgumentsAndNotFound(t *testing.T) { }, code: serviceerrors.CodeInvalidArgument, }, - { - name: "update nil resource", - call: func(s *harness.Service, ctx context.Context) error { - _, err := s.Update(ctx, missing, nil) - return err - }, - code: serviceerrors.CodeInvalidArgument, - }, { name: "get missing harness", call: func(s *harness.Service, ctx context.Context) error { @@ -190,14 +161,6 @@ func TestInvalidArgumentsAndNotFound(t *testing.T) { }, code: serviceerrors.CodeNotFound, }, - { - name: "update missing harness", - call: func(s *harness.Service, ctx context.Context) error { - _, err := s.Update(ctx, missing, fixture("team", "absent", "pool-a")) - return err - }, - code: serviceerrors.CodeNotFound, - }, { name: "delete missing harness", call: func(s *harness.Service, ctx context.Context) error { @@ -234,10 +197,6 @@ func TestAuthorizationDenied(t *testing.T) { _, err := s.Create(ctx, fixture("team", "shared", "pool-a")) return err }}, - {"update", func(s *harness.Service, ctx context.Context) error { - _, err := s.Update(ctx, ref, fixture("team", "shared", "pool-a")) - return err - }}, {"delete", func(s *harness.Service, ctx context.Context) error { return s.Delete(ctx, ref) }}, diff --git a/go/core/internal/service/kubecrud/service.go b/go/core/internal/service/kubecrud/service.go new file mode 100644 index 000000000..c8bdf1879 --- /dev/null +++ b/go/core/internal/service/kubecrud/service.go @@ -0,0 +1,185 @@ +package kubecrud + +import ( + "cmp" + "context" + "fmt" + "slices" + + "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" + "github.com/kagent-dev/kagent/go/core/pkg/auth" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Object interface { + client.Object + comparable +} + +type Service[T Object, L client.ObjectList] struct { + client client.Client + object T + list L + authorizer auth.Authorizer + resource string +} + +func NewService[T Object, L client.ObjectList]( + client client.Client, + authorizer auth.Authorizer, + object T, + list L, + resource string, +) *Service[T, L] { + return &Service[T, L]{ + client: client, object: object, list: list, authorizer: authorizer, resource: resource, + } +} + +func (s *Service[T, L]) List(ctx context.Context, namespace string) ([]T, error) { + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: s.resource}); err != nil { + return nil, err + } + if namespace == "" { + return nil, serviceerrors.NewInvalidArgument("namespace is required", nil) + } + list := s.list.DeepCopyObject().(L) + if err := s.client.List(ctx, list, client.InNamespace(namespace)); err != nil { + return nil, serviceerrors.NewInternal("Failed to list "+s.resource+"s", err) + } + items := make([]T, 0) + if err := meta.EachListItem(list, func(item runtime.Object) error { + items = append(items, item.(T)) + return nil + }); err != nil { + return nil, serviceerrors.NewInternal("Failed to read "+s.resource+" list", err) + } + slices.SortFunc(items, func(left, right T) int { return cmp.Compare(left.GetName(), right.GetName()) }) + return items, nil +} + +func (s *Service[T, L]) Get(ctx context.Context, ref types.NamespacedName) (T, error) { + var zero T + if err := s.validateRef(ref); err != nil { + return zero, err + } + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: s.resource, Name: ref.String()}); err != nil { + return zero, err + } + return s.get(ctx, ref) +} + +// Create persists an object already prepared by the resource-specific service. +func (s *Service[T, L]) Create(ctx context.Context, object T) (T, error) { + var zero T + if object == zero { + return zero, serviceerrors.NewInvalidArgument(s.resource+" resource is required", nil) + } + ref := types.NamespacedName{Namespace: object.GetNamespace(), Name: object.GetName()} + if err := s.validateNewRef(ref); err != nil { + return zero, err + } + if err := s.authorize(ctx, auth.VerbCreate, auth.Resource{Type: s.resource, Name: ref.String()}); err != nil { + return zero, err + } + if err := s.client.Create(ctx, object); err != nil { + switch { + case apierrors.IsAlreadyExists(err): + return zero, serviceerrors.NewAlreadyExists(s.resource+" already exists", err) + case apierrors.IsInvalid(err): + return zero, serviceerrors.NewInvalidArgument("Invalid "+s.resource, err) + default: + return zero, serviceerrors.NewInternal("Failed to create "+s.resource, err) + } + } + return object, nil +} + +// GetForUpdate authorizes an update and loads the live object that owns metadata and status. +func (s *Service[T, L]) GetForUpdate(ctx context.Context, ref types.NamespacedName) (T, error) { + var zero T + if err := s.validateRef(ref); err != nil { + return zero, err + } + if err := s.authorize(ctx, auth.VerbUpdate, auth.Resource{Type: s.resource, Name: ref.String()}); err != nil { + return zero, err + } + return s.get(ctx, ref) +} + +// SaveUpdate persists an object returned by GetForUpdate after its spec is changed. +func (s *Service[T, L]) SaveUpdate(ctx context.Context, object T) (T, error) { + var zero T + if err := s.client.Update(ctx, object); err != nil { + if apierrors.IsInvalid(err) { + return zero, serviceerrors.NewInvalidArgument("Invalid "+s.resource, err) + } + return zero, serviceerrors.NewInternal("Failed to update "+s.resource, err) + } + return object, nil +} + +func (s *Service[T, L]) Delete(ctx context.Context, ref types.NamespacedName) error { + if err := s.validateRef(ref); err != nil { + return err + } + if err := s.authorize(ctx, auth.VerbDelete, auth.Resource{Type: s.resource, Name: ref.String()}); err != nil { + return err + } + object, err := s.get(ctx, ref) + if err != nil { + return err + } + if err := s.client.Delete(ctx, object); err != nil { + return serviceerrors.NewInternal("Failed to delete "+s.resource, err) + } + return nil +} + +func (s *Service[T, L]) get(ctx context.Context, ref types.NamespacedName) (T, error) { + var zero T + object := s.object.DeepCopyObject().(T) + if err := s.client.Get(ctx, ref, object); err != nil { + if apierrors.IsNotFound(err) { + return zero, serviceerrors.NewNotFound(s.resource+" not found", err) + } + return zero, serviceerrors.NewInternal("Failed to get "+s.resource, err) + } + return object, nil +} + +func (s *Service[T, L]) authorize(ctx context.Context, verb auth.Verb, resource auth.Resource) error { + session, ok := auth.AuthSessionFrom(ctx) + if !ok || session == nil { + return serviceerrors.NewUnauthenticated("Failed to get authenticated principal", fmt.Errorf("no session found")) + } + if err := s.authorizer.Check(ctx, session.Principal(), verb, resource); err != nil { + return serviceerrors.NewPermissionDenied("Not authorized", err) + } + return nil +} + +func (s *Service[T, L]) validateRef(ref types.NamespacedName) error { + if ref.Namespace == "" || ref.Name == "" { + return serviceerrors.NewInvalidArgument(s.resource+" namespace and name are required", nil) + } + return nil +} + +func (s *Service[T, L]) validateNewRef(ref types.NamespacedName) error { + if err := s.validateRef(ref); err != nil { + return err + } + if len(utilvalidation.IsDNS1123Subdomain(ref.Namespace)) > 0 { + return serviceerrors.NewInvalidArgument("namespace must be a valid DNS subdomain", nil) + } + if len(utilvalidation.IsDNS1123Subdomain(ref.Name)) > 0 { + return serviceerrors.NewInvalidArgument("name must be a valid DNS subdomain", nil) + } + return nil +} diff --git a/go/core/internal/service/system/service.go b/go/core/internal/service/system/service.go index 1f4d770fe..65e8773ca 100644 --- a/go/core/internal/service/system/service.go +++ b/go/core/internal/service/system/service.go @@ -16,6 +16,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -29,13 +30,6 @@ type Version struct { type ATEClient interface { ListActors(context.Context, string) ([]*ateapipb.Actor, error) ListWorkers(context.Context) ([]*ateapipb.Worker, error) - // EachActorPage walks the actors a page at a time without accumulating them. - // - // Part of the interface rather than an optional cast, because the whole point - // is that the paged reads never hold the whole inventory — and an optional - // interface that silently does not engage would put that back without anything - // failing. See the substrate client's implementation for the numbers. - EachActorPage(ctx context.Context, atespace string, visit func([]*ateapipb.Actor) error) error } type Service struct { @@ -43,9 +37,6 @@ type Service struct { observedNamespaces []string authorizer auth.Authorizer ateClient ATEClient - // cache memoises the substrate reads for a fraction of a second; see - // substratecache.go for what it is for and why its answers carry their age. - cache *substrateCache } type Option func(*Service) @@ -110,7 +101,7 @@ type SubstrateWorker struct { } func NewService(options ...Option) *Service { - service := &Service{cache: newSubstrateCache()} + service := &Service{} for _, option := range options { option(service) } @@ -189,16 +180,21 @@ func (s *Service) ListNamespaces(ctx context.Context) ([]Namespace, error) { return namespaces, nil } -// GetSubstrateStatus returns the whole inventory in one value. -// -// It does not survive a large cluster — see the package comment on substrate.go -// for what replaced it and why. Kept for callers that predate the split. func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace string) (SubstrateStatus, error) { - namespaces, err := s.substrateScope(ctx, requestedNamespace) - if err != nil { + if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: "Agent"}); err != nil { return SubstrateStatus{}, err } + requestedNamespace = strings.TrimSpace(requestedNamespace) + if requestedNamespace != "" { + if validationErrors := utilvalidation.IsDNS1123Label(requestedNamespace); len(validationErrors) > 0 { + return SubstrateStatus{}, serviceerrors.NewInvalidArgument( + fmt.Sprintf("invalid namespace %q: %s", requestedNamespace, strings.Join(validationErrors, ", ")), + nil, + ) + } + } + result := SubstrateStatus{ Enabled: s.ateClient != nil, WorkerPools: []SubstrateWorkerPool{}, @@ -213,6 +209,7 @@ func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace str return SubstrateStatus{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", fmt.Errorf("kubernetes client is not configured")) } + namespaces := s.substrateNamespaces(requestedNamespace) for _, namespace := range namespaces { workerPools, actorTemplates, err := s.listSubstrateCRs(ctx, namespace) if err != nil { @@ -230,8 +227,12 @@ func (s *Service) GetSubstrateStatus(ctx context.Context, requestedNamespace str ctrllog.FromContext(ctx).Error(err, "list ate-api state") } - sortWorkerPools(result.WorkerPools) - sortActorTemplates(result.ActorTemplates) + slices.SortStableFunc(result.WorkerPools, func(left, right SubstrateWorkerPool) int { + return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) + }) + slices.SortStableFunc(result.ActorTemplates, func(left, right SubstrateActorTemplate) int { + return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) + }) slices.SortStableFunc(result.Actors, func(left, right SubstrateActor) int { return strings.Compare(left.ActorID, right.ActorID) }) diff --git a/go/core/internal/service/system/service_test.go b/go/core/internal/service/system/service_test.go index 08bbd51e7..e22c234db 100644 --- a/go/core/internal/service/system/service_test.go +++ b/go/core/internal/service/system/service_test.go @@ -43,25 +43,6 @@ func (client *fakeATEClient) ListActors(context.Context, string) ([]*ateapipb.Ac return client.actors, nil } -// EachActorPage hands the actors over in more than one page on purpose. -// -// The production client pages, and a fake that answered in a single page would let -// a selector that only ever sees one page pass — which is precisely the bug worth -// catching, since the whole reason this method exists is not to hold them all. -func (client *fakeATEClient) EachActorPage(_ context.Context, _ string, visit func([]*ateapipb.Actor) error) error { - if client.err != nil { - return client.err - } - const pageSize = 3 - for start := 0; start < len(client.actors); start += pageSize { - end := min(start+pageSize, len(client.actors)) - if err := visit(client.actors[start:end]); err != nil { - return err - } - } - return nil -} - func (client *fakeATEClient) ListWorkers(context.Context) ([]*ateapipb.Worker, error) { return client.workers, client.err } diff --git a/go/core/internal/service/system/substrate.go b/go/core/internal/service/system/substrate.go deleted file mode 100644 index 7cb3a1bce..000000000 --- a/go/core/internal/service/system/substrate.go +++ /dev/null @@ -1,720 +0,0 @@ -package system - -import ( - "context" - "encoding/base64" - "fmt" - "slices" - "strings" - "time" - - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" - "github.com/kagent-dev/kagent/go/core/pkg/auth" - "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/substrate" - utilvalidation "k8s.io/apimachinery/pkg/util/validation" - ctrllog "sigs.k8s.io/controller-runtime/pkg/log" -) - -// The substrate inventory, in the shape a caller can actually read it. -// -// GetSubstrateStatus answers with every actor and every worker in one message, -// which stopped working: a cluster reporting 103,134 actors produces a response -// gRPC refuses to send at all. What follows splits that one read into the three -// a caller needs — counts, a page of actors, a page of workers — so no single -// response grows with the size of the cluster. -// -// # What this does and does not fix -// -// It bounds what crosses the wire, not what ate-api hands the controller: the -// actor and worker lists still arrive here whole, because ListActors takes no -// page and no filter. So the cost of a read is unchanged on this side and the -// response is bounded on the other, which is the half that was failing. Pushing -// the narrowing all the way down needs ate-api to offer it. - -const ( - // What a caller gets when it does not ask, matching AgentInstanceService. - defaultSubstratePageSize = 50 - // The most one response will carry, matching AgentInstanceService. - maxSubstratePageSize = 100 -) - -/* - * The three public reads, each memoised briefly. - * - * The cache key is the whole question — every field of the request — so a different - * filter, sort, page or scope is a different answer rather than a stale one. See - * substratecache.go for why the answers carry the instant they were computed. - */ - -// GetSubstrateSummary counts the inventory server-side and returns the two small lists whole. -func (s *Service) GetSubstrateSummary(ctx context.Context, requestedNamespace string) (SubstrateSummary, error) { - // Authorized before the cache is consulted, so a cached answer can never be - // served to a caller who would have been refused the read. - if _, err := s.substrateScope(ctx, requestedNamespace); err != nil { - return SubstrateSummary{}, err - } - key := fmt.Sprintf("summary|%s", requestedNamespace) - value, computedAt, err := s.cache.get(ctx, key, func() (any, error) { - return s.computeSubstrateSummary(ctx, requestedNamespace) - }) - if err != nil { - return SubstrateSummary{}, err - } - result := value.(SubstrateSummary) - result.ComputedAt = computedAt - return result, nil -} - -// ListSubstrateActors returns one page of the actors matching filter, in the order asked for. -func (s *Service) ListSubstrateActors(ctx context.Context, request ListActorsRequest) (ListActorsResult, error) { - if _, err := s.substrateScope(ctx, request.Namespace); err != nil { - return ListActorsResult{}, err - } - key := fmt.Sprintf("actors|%s|%s|%d|%s|%s|%s", - request.Namespace, request.Filter, request.Limit, request.PageToken, - request.SortField, request.SortOrder) - value, computedAt, err := s.cache.get(ctx, key, func() (any, error) { - return s.computeSubstrateActors(ctx, request) - }) - if err != nil { - return ListActorsResult{}, err - } - result := value.(ListActorsResult) - result.ComputedAt = computedAt - return result, nil -} - -// ListSubstrateWorkers returns one page of the workers matching filter, in the order asked for. -func (s *Service) ListSubstrateWorkers(ctx context.Context, request ListWorkersRequest) (ListWorkersResult, error) { - if _, err := s.substrateScope(ctx, request.Namespace); err != nil { - return ListWorkersResult{}, err - } - key := fmt.Sprintf("workers|%s|%s|%d|%s|%s|%s", - request.Namespace, request.Filter, request.Limit, request.PageToken, - request.SortField, request.SortOrder) - value, computedAt, err := s.cache.get(ctx, key, func() (any, error) { - return s.computeSubstrateWorkers(ctx, request) - }) - if err != nil { - return ListWorkersResult{}, err - } - result := value.(ListWorkersResult) - result.ComputedAt = computedAt - return result, nil -} - -// SubstrateStatusCount is how many rows carry one status. -type SubstrateStatusCount struct { - Status string - Count int32 -} - -// SubstrateSummary is the whole inventory as counts, plus the two lists that are -// small enough to travel inline. -// -// The counts are over everything in scope and take no filter: they are what a -// caller reports as a total, and a total narrowed by a search is not one. The -// paged reads carry their own filtered total for the other half of "20 of 4,312". -type SubstrateSummary struct { - // ComputedAt is when this answer was produced, which is not necessarily now: - // the reads are memoised briefly (see substratecache.go). Reported so a caller - // can say how old the numbers are instead of implying they are live. - ComputedAt time.Time - Enabled bool - ATEAPIError string - WorkerPools []SubstrateWorkerPool - ActorTemplates []SubstrateActorTemplate - ActorCount int32 - WorkerCount int32 - RunningActorCount int32 - BusyWorkerCount int32 - ActorStatusCounts []SubstrateStatusCount -} - -// SortOrder is the direction a paged read is sorted in. -type SortOrder string - -const ( - SortAscending SortOrder = "asc" - SortDescending SortOrder = "desc" -) - -// ActorSortField names the column ListSubstrateActors orders by. -// -// Every order below ends in the actor id, which is unique. That is not tidiness: -// a page token is the sort key of the last row already sent, so a key that could -// tie would skip or repeat rows at a page boundary. -type ActorSortField string - -const ( - // ActorSortDefault groups by status and orders by id within each group. - ActorSortDefault ActorSortField = "status_then_id" - ActorSortStatus ActorSortField = "status" - ActorSortID ActorSortField = "id" - ActorSortTemplate ActorSortField = "template" - ActorSortWorker ActorSortField = "worker" -) - -// WorkerSortField names the column ListSubstrateWorkers orders by. -type WorkerSortField string - -const ( - // WorkerSortDefault groups by pool and orders by pod within each group. - WorkerSortDefault WorkerSortField = "pool_then_pod" - WorkerSortPool WorkerSortField = "pool" - WorkerSortPod WorkerSortField = "pod" - WorkerSortActor WorkerSortField = "actor" -) - -// ListActorsRequest is what one page of actors is asked for by. -type ListActorsRequest struct { - Namespace string - Filter string - Limit int32 - PageToken string - SortField ActorSortField - SortOrder SortOrder -} - -// ListWorkersRequest is the mirror of ListActorsRequest. -type ListWorkersRequest struct { - Namespace string - Filter string - Limit int32 - PageToken string - SortField WorkerSortField - SortOrder SortOrder -} - -// ListActorsResult is one page of actors and how many matched in total. -type ListActorsResult struct { - // ComputedAt is when this page was produced. See SubstrateSummary.ComputedAt. - ComputedAt time.Time - Actors []SubstrateActor - NextPageToken string - TotalSize int32 - // The order actually applied. Reported rather than assumed, so a caller can - // say how the rows are sorted instead of trusting that its request was - // honoured — an unspecified field resolves to a concrete one here. - SortField ActorSortField - SortOrder SortOrder -} - -// ListWorkersResult is one page of workers and how many matched in total. -type ListWorkersResult struct { - ComputedAt time.Time - Workers []SubstrateWorker - NextPageToken string - TotalSize int32 - SortField WorkerSortField - SortOrder SortOrder -} - -// GetSubstrateSummary counts the inventory server-side and returns the two small -// lists whole. -// -// The ate-api halves can be absent (no endpoint configured) or partial (an error -// on an otherwise successful read). Both are reported rather than raised: the -// Kubernetes-derived halves are complete either way, and failing the whole call -// would hide them. -func (s *Service) computeSubstrateSummary(ctx context.Context, requestedNamespace string) (SubstrateSummary, error) { - namespaces, err := s.substrateScope(ctx, requestedNamespace) - if err != nil { - return SubstrateSummary{}, err - } - - result := SubstrateSummary{ - Enabled: s.ateClient != nil, - WorkerPools: []SubstrateWorkerPool{}, - ActorTemplates: []SubstrateActorTemplate{}, - ActorStatusCounts: []SubstrateStatusCount{}, - } - if s.ateClient == nil { - return result, nil - } - if s.kubeClient == nil { - return SubstrateSummary{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", fmt.Errorf("kubernetes client is not configured")) - } - - for _, namespace := range namespaces { - workerPools, actorTemplates, err := s.listSubstrateCRs(ctx, namespace) - if err != nil { - return SubstrateSummary{}, serviceerrors.NewInternal("Failed to list substrate resources from Kubernetes", err) - } - result.WorkerPools = append(result.WorkerPools, workerPools...) - result.ActorTemplates = append(result.ActorTemplates, actorTemplates...) - } - sortWorkerPools(result.WorkerPools) - sortActorTemplates(result.ActorTemplates) - - // Counted straight off the protos ate-api returned, without converting them. - // - // A count needs no struct, and building 410,110 of them to take their length is - // how the paged read next door OOM-killed the controller. The whole distribution - // is kept rather than only the running tally: knowing 12 of 4,312 are running - // says nothing about the other 4,300. - statusCounts := map[string]int32{} - inScope := namespaceFilter(namespaces) - - // Walked a page at a time and never accumulated: counting 410,110 actors costs - // a few integers this way, where holding them cost the controller its memory - // limit. - if err := s.ateClient.EachActorPage(ctx, "", func(page []*ateapipb.Actor) error { - for _, actor := range page { - if actor == nil || !inScope(actor.GetActorTemplateNamespace()) { - continue - } - result.ActorCount++ - status := substrate.ActorStatusLabel(actor.GetStatus().GetState()) - statusCounts[status]++ - if strings.EqualFold(status, "Running") { - result.RunningActorCount++ - } - } - return nil - }); err != nil { - // Partial rather than failed: the Kubernetes halves above are complete, and - // the caller is told the counts may be short instead of losing everything. - result.ATEAPIError = err.Error() - ctrllog.FromContext(ctx).Error(err, "list ate-api actors") - } - - if workersFromAPI, err := s.ateClient.ListWorkers(ctx); err != nil { - if result.ATEAPIError == "" { - result.ATEAPIError = err.Error() - } - ctrllog.FromContext(ctx).Error(err, "list ate-api workers") - } else { - for _, worker := range workersFromAPI { - if worker == nil || !inScope(worker.GetWorkerNamespace()) { - continue - } - result.WorkerCount++ - // A worker holding an actor is busy; one holding none is available. - if worker.GetStatus().GetAssignment().GetActor().GetName() != "" { - result.BusyWorkerCount++ - } - } - } - - for status, count := range statusCounts { - result.ActorStatusCounts = append(result.ActorStatusCounts, SubstrateStatusCount{Status: status, Count: count}) - } - slices.SortStableFunc(result.ActorStatusCounts, func(left, right SubstrateStatusCount) int { - return strings.Compare(left.Status, right.Status) - }) - - return result, nil -} - -// ListSubstrateActors returns one page of the actors matching filter, in the -// order asked for. -// -// Sorting is server-side for the same reason the filter is: the rows are paged, -// so ordering a page that has already been fetched reorders a hundred rows out -// of hundreds of thousands. It looks like sorting and it is not — the first row -// of the sorted cluster is almost certainly not among the hundred on screen. -func (s *Service) computeSubstrateActors(ctx context.Context, request ListActorsRequest) (ListActorsResult, error) { - namespaces, err := s.substrateScope(ctx, request.Namespace) - if err != nil { - return ListActorsResult{}, err - } - pageSize, err := substratePageSize(request.Limit) - if err != nil { - return ListActorsResult{}, err - } - after, err := decodeSubstratePageToken(request.PageToken) - if err != nil { - return ListActorsResult{}, err - } - field, order := actorSort(request.SortField, request.SortOrder) - - result := ListActorsResult{Actors: []SubstrateActor{}, SortField: field, SortOrder: order} - if s.ateClient == nil { - return result, nil - } - - /* - * Selected while streaming, so the cost of this call does not grow with the - * cluster. - * - * The obvious implementation — collect every actor, sort, take a slice — is what - * OOM-killed the controller at 410,110 actors: ate-api pages its own list, and - * accumulating those pages is hundreds of megabytes of protos before any of this - * code runs. Instead the actors are walked a page at a time and a bounded buffer - * keeps only the `pageSize` rows that belong on the page being asked for, which - * is `pageSize` rows of memory whatever the cluster is running. - * - * The filtered total is counted in the same pass, because it is the other half of - * "20 of 4,312" and counting it afterwards would mean a second walk. - */ - inScope := namespaceFilter(namespaces) - key := actorKey(field) - selector := newPageSelector(pageSize, after, order, key) - - if err := s.ateClient.EachActorPage(ctx, "", func(page []*ateapipb.Actor) error { - for _, actor := range page { - if actor == nil || !inScope(actor.GetActorTemplateNamespace()) { - continue - } - converted := actorFromProto(actor) - if !matchesFilter(request.Filter, converted.ActorID, converted.Status, converted.ActorTemplateNamespace, converted.ActorTemplateName, converted.AteomPodNamespace, converted.AteomPodName, converted.AteomPodIP) { - continue - } - selector.offer(converted) - } - return nil - }); err != nil { - // Unlike the summary, there is no complete half to salvage here: this call - // answers with actors or it answers with nothing. - return ListActorsResult{}, serviceerrors.NewInternal("Failed to list actors from ate-api", err) - } - - rows, nextToken, total := selector.page() - result.Actors = rows - result.NextPageToken = nextToken - result.TotalSize = total - return result, nil -} - -// ListSubstrateWorkers returns one page of the workers matching filter, in the -// order asked for. The mirror of ListSubstrateActors. -func (s *Service) computeSubstrateWorkers(ctx context.Context, request ListWorkersRequest) (ListWorkersResult, error) { - namespaces, err := s.substrateScope(ctx, request.Namespace) - if err != nil { - return ListWorkersResult{}, err - } - pageSize, err := substratePageSize(request.Limit) - if err != nil { - return ListWorkersResult{}, err - } - after, err := decodeSubstratePageToken(request.PageToken) - if err != nil { - return ListWorkersResult{}, err - } - field, order := workerSort(request.SortField, request.SortOrder) - - result := ListWorkersResult{Workers: []SubstrateWorker{}, SortField: field, SortOrder: order} - if s.ateClient == nil { - return result, nil - } - - // The actors are not read at all — see ListSubstrateActors for why that matters. - matching, err := s.matchingWorkers(ctx, namespaces, request.Filter) - if err != nil { - return ListWorkersResult{}, serviceerrors.NewInternal("Failed to list workers from ate-api", err) - } - - // Not streamed, unlike the actors: ListWorkers answers in one response and a - // worker count is bounded by the size of the pools, so the same selector is used - // only to keep the paging and ordering rules identical between the two. - selector := newPageSelector(pageSize, after, order, workerKey(field)) - for _, worker := range matching { - selector.offer(worker) - } - - rows, nextToken, total := selector.page() - result.Workers = rows - result.NextPageToken = nextToken - result.TotalSize = total - return result, nil -} - -/* - * The sort keys. - * - * Each is the chosen column followed by a unique tiebreaker, so that ordering is - * total: a page token is the key of the last row sent, and a key two rows could - * share would make the boundary between pages ambiguous — skipping one row or - * repeating it. - */ -func actorSort(field ActorSortField, order SortOrder) (ActorSortField, SortOrder) { - switch field { - case ActorSortStatus, ActorSortID, ActorSortTemplate, ActorSortWorker: - default: - field = ActorSortDefault - } - if order != SortDescending { - order = SortAscending - } - return field, order -} - -func actorKey(field ActorSortField) func(SubstrateActor) string { - switch field { - case ActorSortID: - return func(a SubstrateActor) string { return a.ActorID } - case ActorSortStatus: - return func(a SubstrateActor) string { return a.Status + "\x00" + a.ActorID } - case ActorSortTemplate: - return func(a SubstrateActor) string { - return a.ActorTemplateNamespace + "/" + a.ActorTemplateName + "\x00" + a.ActorID - } - case ActorSortWorker: - return func(a SubstrateActor) string { - return a.AteomPodNamespace + "/" + a.AteomPodName + "\x00" + a.ActorID - } - default: - return func(a SubstrateActor) string { return a.Status + "\x00" + a.ActorID } - } -} - -func workerSort(field WorkerSortField, order SortOrder) (WorkerSortField, SortOrder) { - switch field { - case WorkerSortPool, WorkerSortPod, WorkerSortActor: - default: - field = WorkerSortDefault - } - if order != SortDescending { - order = SortAscending - } - return field, order -} - -func workerKey(field WorkerSortField) func(SubstrateWorker) string { - pod := func(w SubstrateWorker) string { return w.WorkerNamespace + "/" + w.WorkerPod } - switch field { - case WorkerSortPod: - return pod - case WorkerSortActor: - // Idle workers sort together, and after the busy ones ascending: an empty - // string would put every available worker first, which buries the placements - // this column exists to show. - return func(w SubstrateWorker) string { - actor := w.ActorID - if actor == "" { - actor = "\uffff" - } - return actor + "\x00" + pod(w) - } - default: - return func(w SubstrateWorker) string { return w.WorkerPool + "\x00" + pod(w) } - } -} - -// matchingWorkers reads the workers and keeps only those in scope and matching -// the filter, converting as it goes. -// -// Not streamed, unlike the actors: ate-api answers ListWorkers in one response and -// a worker count is bounded by the size of the pools — eight on the cluster this -// was measured against — so there is nothing here to page around. -func (s *Service) matchingWorkers(ctx context.Context, namespaces []string, filter string) ([]SubstrateWorker, error) { - workersFromAPI, err := s.ateClient.ListWorkers(ctx) - if err != nil { - return nil, err - } - inScope := namespaceFilter(namespaces) - - var matching []SubstrateWorker - for _, worker := range workersFromAPI { - if worker == nil || !inScope(worker.GetWorkerNamespace()) { - continue - } - converted := workerFromProto(worker) - if !matchesFilter(filter, converted.WorkerNamespace, converted.WorkerPool, converted.WorkerPod, converted.ActorNamespace, converted.ActorTemplate, converted.ActorID, converted.IP) { - continue - } - matching = append(matching, converted) - } - return matching, nil -} - -// namespaceFilter reports whether a row's namespace is in scope. -// -// A row with no namespace is always in scope: ate-api leaves it empty on records -// it cannot attribute, and dropping those would quietly shorten the inventory. -// This is the same rule listATEState applies, extracted so the paged reads cannot -// drift from the unpaged one. -func namespaceFilter(namespaces []string) func(string) bool { - if len(namespaces) == 1 && namespaces[0] == "" { - return func(string) bool { return true } - } - allowed := make(map[string]struct{}, len(namespaces)) - for _, namespace := range namespaces { - if namespace != "" { - allowed[namespace] = struct{}{} - } - } - return func(namespace string) bool { - namespace = strings.TrimSpace(namespace) - if namespace == "" { - return true - } - _, ok := allowed[namespace] - return ok - } -} - -// substrateScope authorizes the caller and resolves which namespaces to read. -// -// Shared by all three substrate reads so that one of them cannot quietly become -// more permissive than the others — the authorization and the namespace -// validation are the same check on every path. -func (s *Service) substrateScope(ctx context.Context, requestedNamespace string) ([]string, error) { - if err := s.authorize(ctx, auth.VerbGet, auth.Resource{Type: "Agent"}); err != nil { - return nil, err - } - requestedNamespace = strings.TrimSpace(requestedNamespace) - if requestedNamespace != "" { - if problems := utilvalidation.IsDNS1123Label(requestedNamespace); len(problems) > 0 { - return nil, serviceerrors.NewInvalidArgument( - fmt.Sprintf("invalid namespace %q: %s", requestedNamespace, strings.Join(problems, ", ")), - nil, - ) - } - } - return s.substrateNamespaces(requestedNamespace), nil -} - -// substratePageSize applies the same bounds AgentInstanceService uses. -// -// Zero means "no preference" and takes the default; anything outside the range -// is refused rather than clamped, so a caller asking for 5,000 is told its -// request was not honoured instead of quietly receiving 100. -func substratePageSize(limit int32) (int, error) { - if limit == 0 { - return defaultSubstratePageSize, nil - } - if limit < 0 || limit > maxSubstratePageSize { - return 0, serviceerrors.NewInvalidArgument(fmt.Sprintf("page limit must be between 1 and %d", maxSubstratePageSize), nil) - } - return int(limit), nil -} - -// The page token is the sort key of the last row already sent. -// -// A key rather than an offset, so that rows appearing or disappearing between -// two reads shift the page's contents instead of causing a row to be skipped or -// repeated — which on an inventory that changes every second it is polled is the -// difference between a stable list and one that flickers. -func encodeSubstratePageToken(key string) string { - return base64.RawURLEncoding.EncodeToString([]byte(key)) -} - -func decodeSubstratePageToken(token string) (string, error) { - if token == "" { - return "", nil - } - value, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return "", serviceerrors.NewInvalidArgument("page token is invalid", err) - } - return string(value), nil -} - -/* - * The rows belonging on one page, chosen without holding the rest. - * - * Paging is by sort key — the key of the last row already sent — so the page being - * asked for is "the `limit` rows that come after `after` in the chosen order". That - * can be decided from a stream: keep the `limit` rows nearest the front and discard - * anything that cannot make the page. - * - * Compaction rather than a heap. The buffer is allowed to grow to twice the limit - * and is then sorted and truncated, which is the same amortised work as a heap for - * these sizes and considerably easier to be sure is right — and being sure matters, - * because the failure mode of a wrong selector is a page that silently skips rows. - */ -type pageSelector[T any] struct { - limit int - after string - order SortOrder - key func(T) string - rows []T - // Every row that matched, page or not. The caller reports it as the total, and - // it is the reason this is counted here rather than by a second walk. - total int32 -} - -func newPageSelector[T any](limit int, after string, order SortOrder, key func(T) string) *pageSelector[T] { - return &pageSelector[T]{limit: limit, after: after, order: order, key: key} -} - -// before reports whether left comes before right in the selected order. -func (s *pageSelector[T]) before(left, right string) bool { - if s.order == SortDescending { - return left > right - } - return left < right -} - -func (s *pageSelector[T]) offer(row T) { - // Every matching row counts towards the total, page or not — it is a total, not - // a remainder. - s.total++ - // Rows at or before the token have already been sent. - if s.after != "" && !s.before(s.after, s.key(row)) { - return - } - s.rows = append(s.rows, row) - if len(s.rows) >= 2*s.limit { - s.compact() - } -} - -func (s *pageSelector[T]) compact() { - slices.SortStableFunc(s.rows, func(left, right T) int { - leftKey, rightKey := s.key(left), s.key(right) - if leftKey == rightKey { - return 0 - } - if s.before(leftKey, rightKey) { - return -1 - } - return 1 - }) - if len(s.rows) > s.limit { - s.rows = s.rows[:s.limit] - } -} - -// page returns the page, the token to ask for the next one, and the filtered total. -// -// The token is empty on the last page rather than on a page that merely happens to -// be full, so a caller never fetches an empty page to discover it has finished. -func (s *pageSelector[T]) page() ([]T, string, int32) { - s.compact() - if len(s.rows) == 0 { - return []T{}, "", s.total - } - // A full page means there may be more: the selector discarded everything past - // the limit, so it cannot know whether anything was there. Asking again is the - // only way to find out, and an empty answer is what ends the walk. - token := "" - if len(s.rows) == s.limit { - token = encodeSubstratePageToken(s.key(s.rows[len(s.rows)-1])) - } - return s.rows, token, s.total -} - -// matchesFilter reports whether any of the row's displayed fields contains the -// term, case-insensitively. An empty term matches every row. -// -// Matched against the fields the caller displays rather than against every field -// on the record: a search that hits on something not on screen reads as a list -// filtering itself at random. -func matchesFilter(filter string, fields ...string) bool { - needle := strings.ToLower(strings.TrimSpace(filter)) - if needle == "" { - return true - } - for _, field := range fields { - if strings.Contains(strings.ToLower(field), needle) { - return true - } - } - return false -} - -func sortWorkerPools(pools []SubstrateWorkerPool) { - slices.SortStableFunc(pools, func(left, right SubstrateWorkerPool) int { - return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) - }) -} - -func sortActorTemplates(templates []SubstrateActorTemplate) { - slices.SortStableFunc(templates, func(left, right SubstrateActorTemplate) int { - return strings.Compare(left.Namespace+"/"+left.Name, right.Namespace+"/"+right.Name) - }) -} diff --git a/go/core/internal/service/system/substrate_test.go b/go/core/internal/service/system/substrate_test.go deleted file mode 100644 index 925bc58f0..000000000 --- a/go/core/internal/service/system/substrate_test.go +++ /dev/null @@ -1,491 +0,0 @@ -package system_test - -import ( - "context" - "fmt" - "slices" - "testing" - - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - authimpl "github.com/kagent-dev/kagent/go/core/internal/httpserver/auth" - "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" - "github.com/kagent-dev/kagent/go/core/internal/service/system" - pkgAuth "github.com/kagent-dev/kagent/go/core/pkg/auth" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -// substrateScheme is the scheme the substrate CRs are registered against, shared -// by every test below. -func substrateScheme(t *testing.T) *runtime.Scheme { - t.Helper() - scheme := runtime.NewScheme() - require.NoError(t, corev1.AddToScheme(scheme)) - require.NoError(t, atev1alpha1.AddToScheme(scheme)) - return scheme -} - -func substrateContext(t *testing.T) context.Context { - t.Helper() - return pkgAuth.AuthSessionTo(t.Context(), &authimpl.SimpleSession{P: pkgAuth.Principal{User: pkgAuth.User{ID: "user"}}}) -} - -// actorsNamed builds n actors in one namespace, alternating status so that the -// grouping the service sorts by is actually exercised rather than assumed. -func actorsNamed(namespace string, n int) []*ateapipb.Actor { - actors := make([]*ateapipb.Actor, 0, n) - for i := range n { - state := ateapipb.ActorState_ACTOR_STATE_RUNNING - if i%2 == 1 { - state = ateapipb.ActorState_ACTOR_STATE_SUSPENDED - } - actors = append(actors, &ateapipb.Actor{ - Metadata: &ateapipb.ResourceMetadata{Name: fmt.Sprintf("actor-%03d", i)}, - Status: &ateapipb.ActorStatus{State: state}, - ActorTemplateNamespace: namespace, - ActorTemplateName: "template", - }) - } - return actors -} - -func workersNamed(namespace string, n int, busy int) []*ateapipb.Worker { - workers := make([]*ateapipb.Worker, 0, n) - for i := range n { - worker := &ateapipb.Worker{ - Metadata: &ateapipb.ResourceMetadata{Version: int64(i)}, - WorkerNamespace: namespace, - WorkerPool: "pool", - WorkerPod: fmt.Sprintf("worker-%03d", i), - Status: &ateapipb.WorkerStatus{}, - } - if i < busy { - worker.Status.Assignment = &ateapipb.ActorAssignment{ - ActorTemplate: &ateapipb.KubeNamespacedObjectRef{Namespace: namespace, Name: "template"}, - Actor: &ateapipb.ObjectRef{Name: fmt.Sprintf("actor-%03d", i)}, - } - } - workers = append(workers, worker) - } - return workers -} - -// TestGetSubstrateSummary is the guard on the tiles: these counts are the only -// honest total a caller has, because every other read is now a page. -func TestGetSubstrateSummary(t *testing.T) { - ctx := substrateContext(t) - - t.Run("counts everything in scope and carries the small lists inline", func(t *testing.T) { - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).WithObjects( - &atev1alpha1.WorkerPool{ - ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "pool"}, - Spec: atev1alpha1.WorkerPoolSpec{Replicas: 8, AteomImage: "ateom:test"}, - }, - &atev1alpha1.ActorTemplate{ - ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "template"}, - Status: atev1alpha1.ActorTemplateStatus{Phase: atev1alpha1.PhaseReady}, - }, - ).Build() - ateClient := &fakeATEClient{ - actors: actorsNamed("team", 10), - workers: workersNamed("team", 8, 3), - } - service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, ateClient)) - - result, err := service.GetSubstrateSummary(ctx, "team") - require.NoError(t, err) - - assert.True(t, result.Enabled) - assert.Empty(t, result.ATEAPIError) - require.Len(t, result.WorkerPools, 1) - assert.Equal(t, int32(8), result.WorkerPools[0].Replicas) - require.Len(t, result.ActorTemplates, 1) - - assert.Equal(t, int32(10), result.ActorCount) - assert.Equal(t, int32(5), result.RunningActorCount, "half the actors are Running") - assert.Equal(t, int32(8), result.WorkerCount) - assert.Equal(t, int32(3), result.BusyWorkerCount, "a worker is busy when an actor is placed on it") - - // The whole distribution, not only the running tally: a caller that knows 5 - // of 10 are running still cannot say what the other 5 are doing. - assert.Equal(t, []system.SubstrateStatusCount{ - {Status: "Running", Count: 5}, - {Status: "Suspended", Count: 5}, - }, result.ActorStatusCounts) - }) - - t.Run("reports a partial ate-api read rather than failing", func(t *testing.T) { - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).WithObjects( - &atev1alpha1.WorkerPool{ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "pool"}}, - ).Build() - ateClient := &fakeATEClient{err: assert.AnError} - service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, ateClient)) - - result, err := service.GetSubstrateSummary(ctx, "team") - - // The Kubernetes half is complete, so failing the call would hide data that - // arrived intact. - require.NoError(t, err) - assert.NotEmpty(t, result.ATEAPIError) - assert.Len(t, result.WorkerPools, 1) - assert.Equal(t, int32(0), result.ActorCount) - }) - - t.Run("disabled does not read Kubernetes", func(t *testing.T) { - service := system.NewService(system.WithInventory(nil, nil, &authimpl.NoopAuthorizer{}, nil)) - result, err := service.GetSubstrateSummary(ctx, "team") - require.NoError(t, err) - assert.False(t, result.Enabled) - assert.Empty(t, result.WorkerPools) - }) - - t.Run("validates and authorizes exactly as GetSubstrateStatus does", func(t *testing.T) { - service := system.NewService(system.WithInventory(nil, nil, &authimpl.NoopAuthorizer{}, nil)) - _, err := service.GetSubstrateSummary(ctx, "INVALID_NAMESPACE") - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) - - service = system.NewService(system.WithInventory(nil, nil, systemDenyAuthorizer{}, nil)) - _, err = service.GetSubstrateSummary(ctx, "") - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied), err) - }) -} - -func TestListSubstrateActors(t *testing.T) { - ctx := substrateContext(t) - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() - - newService := func(actors []*ateapipb.Actor) *system.Service { - return system.NewService(system.WithInventory( - kubeClient, nil, &authimpl.NoopAuthorizer{}, - &fakeATEClient{actors: actors}, - )) - } - - t.Run("pages through every actor exactly once", func(t *testing.T) { - service := newService(actorsNamed("team", 25)) - - seen := map[string]int{} - pageToken := "" - pages := 0 - for { - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: pageToken}) - require.NoError(t, err) - // The total is of everything matching, not of this page — which is what - // lets a caller say "10 of 25" instead of implying the page is the lot. - assert.Equal(t, int32(25), result.TotalSize) - for _, actor := range result.Actors { - seen[actor.ActorID]++ - } - pages++ - require.Less(t, pages, 10, "paging did not terminate") - if result.NextPageToken == "" { - // An empty token is the last page, so a caller never fetches an empty - // one to discover it has finished. - assert.LessOrEqual(t, len(result.Actors), 10) - break - } - pageToken = result.NextPageToken - } - - assert.Equal(t, 3, pages, "25 actors at 10 a page") - assert.Len(t, seen, 25, "every actor appeared") - for id, count := range seen { - assert.Equal(t, 1, count, "%s appeared more than once", id) - } - }) - - t.Run("filters server-side across the whole list, not one page", func(t *testing.T) { - service := newService(actorsNamed("team", 25)) - - // actor-019 sorts well past the first page, so a client-side filter over a - // fetched page would report no matches for it. That is the failure this - // exists to prevent. - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "actor-019", Limit: 10, PageToken: ""}) - require.NoError(t, err) - require.Len(t, result.Actors, 1) - assert.Equal(t, "actor-019", result.Actors[0].ActorID) - assert.Equal(t, int32(1), result.TotalSize) - assert.Empty(t, result.NextPageToken) - }) - - t.Run("matches case-insensitively on status too", func(t *testing.T) { - service := newService(actorsNamed("team", 10)) - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "SUSPEND", Limit: 100, PageToken: ""}) - require.NoError(t, err) - assert.Equal(t, int32(5), result.TotalSize) - }) - - t.Run("groups by status so a page is stable between reads", func(t *testing.T) { - service := newService(actorsNamed("team", 10)) - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 100, PageToken: ""}) - require.NoError(t, err) - require.Len(t, result.Actors, 10) - - // Sorted by status then id: every Running actor precedes every Suspended one. - for i := range 5 { - assert.Equal(t, "Running", result.Actors[i].Status) - } - for i := 5; i < 10; i++ { - assert.Equal(t, "Suspended", result.Actors[i].Status) - } - }) - - t.Run("refuses a page size it cannot honour rather than clamping", func(t *testing.T) { - service := newService(actorsNamed("team", 5)) - - _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 5000, PageToken: ""}) - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) - - _, err = service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: -1, PageToken: ""}) - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) - - // Zero is "no preference" and takes the default. - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 0, PageToken: ""}) - require.NoError(t, err) - assert.Len(t, result.Actors, 5) - }) - - t.Run("rejects a page token that is not one", func(t *testing.T) { - service := newService(actorsNamed("team", 5)) - _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: "not base64!!"}) - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) - }) - - t.Run("answers empty when ate-api is not configured", func(t *testing.T) { - service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, nil)) - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) - require.NoError(t, err) - assert.Empty(t, result.Actors) - assert.Equal(t, int32(0), result.TotalSize) - }) - - t.Run("fails the call when ate-api does", func(t *testing.T) { - // Unlike the summary there is no complete half to salvage: this call answers - // with actors or it answers with nothing. - service := system.NewService(system.WithInventory( - kubeClient, nil, &authimpl.NoopAuthorizer{}, &fakeATEClient{err: assert.AnError}, - )) - _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) - require.Error(t, err) - }) - - t.Run("validates and authorizes", func(t *testing.T) { - service := newService(nil) - _, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "INVALID_NAMESPACE", Filter: "", Limit: 10, PageToken: ""}) - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument), err) - - denied := system.NewService(system.WithInventory(kubeClient, nil, systemDenyAuthorizer{}, &fakeATEClient{})) - _, err = denied.ListSubstrateActors(ctx, system.ListActorsRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) - assert.True(t, serviceerrors.IsCode(err, serviceerrors.CodePermissionDenied), err) - }) -} - -func TestListSubstrateWorkers(t *testing.T) { - ctx := substrateContext(t) - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() - - newService := func(workers []*ateapipb.Worker) *system.Service { - return system.NewService(system.WithInventory( - kubeClient, nil, &authimpl.NoopAuthorizer{}, - &fakeATEClient{workers: workers}, - )) - } - - t.Run("pages through every worker exactly once", func(t *testing.T) { - service := newService(workersNamed("team", 12, 4)) - - seen := map[string]int{} - pageToken := "" - for pages := 0; ; pages++ { - require.Less(t, pages, 10, "paging did not terminate") - result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{Namespace: "team", Filter: "", Limit: 5, PageToken: pageToken}) - require.NoError(t, err) - assert.Equal(t, int32(12), result.TotalSize) - for _, worker := range result.Workers { - seen[worker.WorkerPod]++ - } - if result.NextPageToken == "" { - break - } - pageToken = result.NextPageToken - } - - assert.Len(t, seen, 12) - for pod, count := range seen { - assert.Equal(t, 1, count, "%s appeared more than once", pod) - } - }) - - t.Run("filters on the placed actor", func(t *testing.T) { - service := newService(workersNamed("team", 12, 4)) - result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{Namespace: "team", Filter: "actor-002", Limit: 100, PageToken: ""}) - require.NoError(t, err) - require.Len(t, result.Workers, 1) - assert.Equal(t, "worker-002", result.Workers[0].WorkerPod) - }) - - t.Run("answers empty when ate-api is not configured", func(t *testing.T) { - service := system.NewService(system.WithInventory(kubeClient, nil, &authimpl.NoopAuthorizer{}, nil)) - result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{Namespace: "team", Filter: "", Limit: 10, PageToken: ""}) - require.NoError(t, err) - assert.Empty(t, result.Workers) - }) -} - -// TestListSubstrateActorsSorting is the guard on server-side ordering. -// -// The property that matters is not "the rows came back sorted" — it is that -// **paging through a sorted result yields every row exactly once**. A selector -// whose direction and whose page token disagree drops rows at a page boundary, -// and it does so silently: each page looks correctly ordered on its own. -func TestListSubstrateActorsSorting(t *testing.T) { - ctx := substrateContext(t) - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() - service := system.NewService(system.WithInventory( - kubeClient, nil, &authimpl.NoopAuthorizer{}, - &fakeATEClient{actors: actorsNamed("team", 25)}, - )) - - // Every order the API offers, in both directions. - fields := []system.ActorSortField{ - system.ActorSortDefault, - system.ActorSortStatus, - system.ActorSortID, - system.ActorSortTemplate, - system.ActorSortWorker, - } - orders := []system.SortOrder{system.SortAscending, system.SortDescending} - - for _, field := range fields { - for _, order := range orders { - t.Run(string(field)+"/"+string(order), func(t *testing.T) { - seen := map[string]int{} - var ordered []string - pageToken := "" - - for pages := 0; ; pages++ { - require.Less(t, pages, 12, "paging did not terminate") - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{ - Namespace: "team", - Limit: 7, - PageToken: pageToken, - SortField: field, - SortOrder: order, - }) - require.NoError(t, err) - - // The order applied is reported, not assumed — a caller says how its - // rows are sorted rather than trusting the request was honoured. - assert.Equal(t, field, result.SortField) - assert.Equal(t, order, result.SortOrder) - assert.Equal(t, int32(25), result.TotalSize) - - for _, actor := range result.Actors { - seen[actor.ActorID]++ - ordered = append(ordered, actor.ActorID) - } - if result.NextPageToken == "" { - break - } - pageToken = result.NextPageToken - } - - // Every row, exactly once, across the whole walk. - assert.Len(t, seen, 25, "paging lost or repeated rows") - for id, count := range seen { - assert.Equal(t, 1, count, "%s appeared more than once", id) - } - - // And the concatenated pages are themselves in order: a page that - // sorted only within itself would satisfy the count above. - sorted := append([]string(nil), ordered...) - slices.Sort(sorted) - if order == system.SortDescending { - slices.Reverse(sorted) - } - if field == system.ActorSortID { - // Only the id sort is a total order on the id alone; the others tie - // on their column and break it with the id, so the ids themselves - // are not monotonic. - assert.Equal(t, sorted, ordered, "pages were not in the requested order") - } - }) - } - } -} - -// TestListSubstrateActorsSortDirectionIsHonoured checks the two directions -// actually differ — a selector that ignored the order would pass every -// completeness assertion above. -func TestListSubstrateActorsSortDirectionIsHonoured(t *testing.T) { - ctx := substrateContext(t) - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() - service := system.NewService(system.WithInventory( - kubeClient, nil, &authimpl.NoopAuthorizer{}, - &fakeATEClient{actors: actorsNamed("team", 25)}, - )) - - first := func(order system.SortOrder) string { - result, err := service.ListSubstrateActors(ctx, system.ListActorsRequest{ - Namespace: "team", - Limit: 1, - SortField: system.ActorSortID, - SortOrder: order, - }) - require.NoError(t, err) - require.Len(t, result.Actors, 1) - return result.Actors[0].ActorID - } - - assert.Equal(t, "actor-000", first(system.SortAscending)) - assert.Equal(t, "actor-024", first(system.SortDescending)) -} - -// TestListSubstrateWorkersSorting is the same property for the worker list, -// which pages through the same selector. -func TestListSubstrateWorkersSorting(t *testing.T) { - ctx := substrateContext(t) - kubeClient := fake.NewClientBuilder().WithScheme(substrateScheme(t)).Build() - service := system.NewService(system.WithInventory( - kubeClient, nil, &authimpl.NoopAuthorizer{}, - &fakeATEClient{workers: workersNamed("team", 12, 4)}, - )) - - for _, field := range []system.WorkerSortField{ - system.WorkerSortDefault, - system.WorkerSortPool, - system.WorkerSortPod, - system.WorkerSortActor, - } { - for _, order := range []system.SortOrder{system.SortAscending, system.SortDescending} { - seen := map[string]int{} - pageToken := "" - for pages := 0; ; pages++ { - require.Less(t, pages, 12, "paging did not terminate") - result, err := service.ListSubstrateWorkers(ctx, system.ListWorkersRequest{ - Namespace: "team", - Limit: 5, - PageToken: pageToken, - SortField: field, - SortOrder: order, - }) - require.NoError(t, err) - assert.Equal(t, field, result.SortField) - assert.Equal(t, int32(12), result.TotalSize) - for _, worker := range result.Workers { - seen[worker.WorkerPod]++ - } - if result.NextPageToken == "" { - break - } - pageToken = result.NextPageToken - } - assert.Len(t, seen, 12, "%s/%s lost or repeated rows", field, order) - } - } -} diff --git a/go/core/internal/service/system/substratecache.go b/go/core/internal/service/system/substratecache.go deleted file mode 100644 index ed5f95f36..000000000 --- a/go/core/internal/service/system/substratecache.go +++ /dev/null @@ -1,149 +0,0 @@ -package system - -import ( - "context" - "sync" - "time" - - "golang.org/x/sync/singleflight" -) - -/* - * A short-lived cache in front of the substrate reads, and why it reports its own - * age. - * - * # The cost this exists for - * - * Every one of the three substrate reads walks ate-api's whole actor list, because - * ate-api offers no filter, no ordering and no server-side count — only pagination. - * On a deployment holding 410,110 actors that measured at ~1.6s per call, and the - * substrate page makes three of them on load and again on every poll tick. - * - * Asking ate-api for larger pages does not help: the page size was raised to its - * maximum of 1000 and the timing did not move, so the cost is ate-api's own scan - * rather than the number of round trips. There is nothing to optimise on this side - * of that call; the only lever left is to make the same call less often. - * - * # Why the age is part of the answer - * - * Because the page above this offers polling, and a cache is exactly how polling - * becomes a lie: the reader turns it on, the requests go out, the responses come - * back instantly, and the numbers never change. This codebase has already shipped - * that once — a poll control that reported it was re-reading and was not — and the - * fix then was to measure rather than to trust. - * - * So every cached answer carries the instant it was computed. A caller can say "as - * of 0.4s ago" instead of implying "now", and a reader watching a stalled cluster - * can tell the difference between nothing changing and nothing being read. The TTL - * is deliberately shorter than the page's default poll interval, so an ordinary - * poll misses the cache and genuinely re-reads; what the cache absorbs is the burst - * of identical requests a single page load makes. - */ - -// substrateCacheTTL is how long a computed answer may be reused. -// -// Below the substrate page's default one-second poll and at its half-second floor, -// so polling at any offered rate still reaches ate-api. What this collapses is the -// three-or-more identical requests one page load makes — including React rendering -// a component twice in development. -const substrateCacheTTL = 400 * time.Millisecond - -// substrateCacheEntries caps how many distinct answers are held. -// -// Each entry is one page of rows or one set of counts, so the cap bounds memory at -// something small and fixed. Distinct entries come from distinct questions — a -// different filter, sort or page — and a reader cannot generate many of those -// quickly. Oldest-out when full, which for a TTL this short is nearly always an -// entry that had expired anyway. -const substrateCacheEntries = 64 - -// cachedAnswer is a computed result and the instant it was computed at. -type cachedAnswer struct { - value any - computedAt time.Time -} - -// substrateCache memoises the substrate reads for substrateCacheTTL. -// -// The singleflight group is the other half of the point: without it, the three -// reads a page load fires concurrently would each start their own walk before any -// of them had a result to cache. -type substrateCache struct { - mutex sync.Mutex - entries map[string]cachedAnswer - group singleflight.Group - // now is injectable so the tests can move time without sleeping. - now func() time.Time -} - -func newSubstrateCache() *substrateCache { - return &substrateCache{entries: map[string]cachedAnswer{}, now: time.Now} -} - -// get returns the answer for key, computing it only when there is no fresh one. -// -// Returns the value and the instant it was computed, which is not necessarily now — -// that difference is the whole reason this returns two things. -func (c *substrateCache) get(ctx context.Context, key string, compute func() (any, error)) (any, time.Time, error) { - if c == nil { - value, err := compute() - return value, time.Now(), err - } - - if answer, ok := c.fresh(key); ok { - return answer.value, answer.computedAt, nil - } - - // Shared: concurrent callers asking the same question wait for one walk rather - // than starting one each. - result, err, _ := c.group.Do(key, func() (any, error) { - // Re-checked inside the flight: a caller that queued behind another one may - // find the answer already stored by the time it runs. - if answer, ok := c.fresh(key); ok { - return answer, nil - } - value, err := compute() - if err != nil { - return cachedAnswer{}, err - } - answer := cachedAnswer{value: value, computedAt: c.now()} - c.store(key, answer) - return answer, nil - }) - if err != nil { - return nil, time.Time{}, err - } - if ctx.Err() != nil { - return nil, time.Time{}, ctx.Err() - } - answer := result.(cachedAnswer) - return answer.value, answer.computedAt, nil -} - -func (c *substrateCache) fresh(key string) (cachedAnswer, bool) { - c.mutex.Lock() - defer c.mutex.Unlock() - answer, ok := c.entries[key] - if !ok || c.now().Sub(answer.computedAt) > substrateCacheTTL { - return cachedAnswer{}, false - } - return answer, true -} - -func (c *substrateCache) store(key string, answer cachedAnswer) { - c.mutex.Lock() - defer c.mutex.Unlock() - if len(c.entries) >= substrateCacheEntries { - // Drop the oldest rather than clearing everything: clearing would throw away - // the entry the current burst of requests is about to ask for again. - var oldestKey string - var oldest time.Time - for candidate, entry := range c.entries { - if oldestKey == "" || entry.computedAt.Before(oldest) { - oldestKey, oldest = candidate, entry.computedAt - } - } - delete(c.entries, oldestKey) - } - c.entries[key] = answer -} diff --git a/go/core/internal/service/system/substratecache_test.go b/go/core/internal/service/system/substratecache_test.go deleted file mode 100644 index 604f53a47..000000000 --- a/go/core/internal/service/system/substratecache_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package system - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// The cache exists to make a ~1.6s walk happen less often. These pin the two -// properties that make it safe to do that: a stale answer is never presented as a -// fresh one, and concurrent identical questions cost one walk rather than many. -func TestSubstrateCache(t *testing.T) { - t.Run("recomputes once the entry has aged out", func(t *testing.T) { - cache := newSubstrateCache() - clock := time.Unix(1_800_000_000, 0) - cache.now = func() time.Time { return clock } - - calls := 0 - compute := func() (any, error) { - calls++ - return calls, nil - } - - first, firstAt, err := cache.get(t.Context(), "k", compute) - require.NoError(t, err) - assert.Equal(t, 1, first) - assert.Equal(t, clock, firstAt) - - // Inside the window: the same answer, and the same age — which is the point. - // A cache that reported `now` here would make a stale number look live. - clock = clock.Add(substrateCacheTTL / 2) - second, secondAt, err := cache.get(t.Context(), "k", compute) - require.NoError(t, err) - assert.Equal(t, 1, second, "should have been served from the cache") - assert.Equal(t, firstAt, secondAt, "a cached answer must report when it was computed") - assert.Equal(t, 1, calls) - - // Past the window: computed again, and the age moves with it. - clock = clock.Add(substrateCacheTTL) - third, thirdAt, err := cache.get(t.Context(), "k", compute) - require.NoError(t, err) - assert.Equal(t, 2, third) - assert.True(t, thirdAt.After(firstAt)) - assert.Equal(t, 2, calls) - }) - - t.Run("a different question is a different answer, not a stale one", func(t *testing.T) { - cache := newSubstrateCache() - calls := 0 - compute := func() (any, error) { - calls++ - return calls, nil - } - - _, _, err := cache.get(t.Context(), "actors|team||100||status|asc", compute) - require.NoError(t, err) - // Same read, different sort — a cache keyed too loosely would answer this - // with the previous order's rows. - _, _, err = cache.get(t.Context(), "actors|team||100||status|desc", compute) - require.NoError(t, err) - assert.Equal(t, 2, calls) - }) - - t.Run("concurrent identical requests share one walk", func(t *testing.T) { - cache := newSubstrateCache() - var mutex sync.Mutex - calls := 0 - release := make(chan struct{}) - - compute := func() (any, error) { - mutex.Lock() - calls++ - mutex.Unlock() - <-release - return "value", nil - } - - const callers = 8 - var waiting sync.WaitGroup - waiting.Add(callers) - for range callers { - go func() { - defer waiting.Done() - value, _, err := cache.get(context.Background(), "k", compute) - assert.NoError(t, err) - assert.Equal(t, "value", value) - }() - } - - // Let them all queue behind the one in flight, then finish it. - time.Sleep(50 * time.Millisecond) - close(release) - waiting.Wait() - - mutex.Lock() - defer mutex.Unlock() - assert.Equal(t, 1, calls, "the walk should have happened once for all callers") - }) - - t.Run("a failure is not cached", func(t *testing.T) { - cache := newSubstrateCache() - calls := 0 - compute := func() (any, error) { - calls++ - return nil, errors.New("ate-api is down") - } - - _, _, err := cache.get(t.Context(), "k", compute) - require.Error(t, err) - _, _, err = cache.get(t.Context(), "k", compute) - require.Error(t, err) - // Caching the failure would keep a recovered backend looking broken for as - // long as the entry lived. - assert.Equal(t, 2, calls) - }) - - t.Run("holds a bounded number of entries", func(t *testing.T) { - cache := newSubstrateCache() - for index := range substrateCacheEntries * 3 { - key := string(rune('a'+index%26)) + string(rune('0'+index/26)) - _, _, err := cache.get(t.Context(), key, func() (any, error) { return index, nil }) - require.NoError(t, err) - } - cache.mutex.Lock() - defer cache.mutex.Unlock() - assert.LessOrEqual(t, len(cache.entries), substrateCacheEntries) - }) -} diff --git a/go/core/pkg/sandboxbackend/substrate/list.go b/go/core/pkg/sandboxbackend/substrate/list.go index ddbbec31a..b01265752 100644 --- a/go/core/pkg/sandboxbackend/substrate/list.go +++ b/go/core/pkg/sandboxbackend/substrate/list.go @@ -6,13 +6,6 @@ import ( "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) -// actorPageSize is what ate-api is asked for per page. -// -// Its maximum: "values above 1000 are coerced to 1000". Left unset the server picks a -// much smaller default, and on a deployment holding 410,110 actors that is thousands of -// round trips for one walk — which measured at ~1.6s per read of the inventory. -const actorPageSize = 1000 - // ListActors returns all actors in the given atespace (empty atespace = all atespaces, // including substrate's reserved golden atespace). The list API is paginated — pages are // followed until the token drains, since a single page may miss actors. @@ -27,7 +20,6 @@ func (c *Client) ListActors(ctx context.Context, atespace string) ([]*ateapipb.A for { resp, err := c.ControlClient.ListActors(ctx, &ateapipb.ListActorsRequest{ Atespace: atespace, - PageSize: actorPageSize, PageToken: pageToken, }) if err != nil { @@ -41,44 +33,6 @@ func (c *Client) ListActors(ctx context.Context, atespace string) ([]*ateapipb.A } } -// EachActorPage calls visit with each page of actors as it arrives, instead of -// accumulating them. -// -// ListActors above collects every page into one slice, which is fine for a small -// cluster and fatal for a large one: a deployment reporting 410,110 actors put -// several hundred megabytes of protos in the controller and OOM-killed it. A -// caller that only needs to count, filter or take one page never has to hold them -// all, and this is how it avoids doing so. -// -// visit must not retain the slice it is given — the next page reuses nothing, but -// the actors themselves are only guaranteed to live as long as the call. Returning -// an error from visit stops the walk and is returned as-is. -func (c *Client) EachActorPage(ctx context.Context, atespace string, visit func([]*ateapipb.Actor) error) error { - if c == nil { - return nil - } - ctx, cancel := c.callCtx(ctx) - defer cancel() - pageToken := "" - for { - resp, err := c.ControlClient.ListActors(ctx, &ateapipb.ListActorsRequest{ - Atespace: atespace, - PageSize: actorPageSize, - PageToken: pageToken, - }) - if err != nil { - return err - } - if err := visit(resp.GetActors()); err != nil { - return err - } - pageToken = resp.GetNextPageToken() - if pageToken == "" { - return nil - } - } -} - // ListWorkers returns all workers reflected in ate-api. func (c *Client) ListWorkers(ctx context.Context) ([]*ateapipb.Worker, error) { if c == nil { diff --git a/go/core/v2/a2agateway/gateway.go b/go/core/v2/a2agateway/gateway.go index f98697f16..5796715a8 100644 --- a/go/core/v2/a2agateway/gateway.go +++ b/go/core/v2/a2agateway/gateway.go @@ -48,7 +48,6 @@ type instanceStore interface { CreateAgentInstanceTask(context.Context, string, []byte, *a2atype.Task) (*a2atype.Task, bool, error) GetActiveAgentInstanceTask(context.Context, string) (*a2atype.Task, error) InterruptActiveAgentInstanceTask(context.Context, string, string) (bool, error) - AbandonActiveAgentInstanceTask(context.Context, string, string) (bool, error) StoreAgentInstanceTaskEvent(context.Context, string, *a2atype.Task, a2atype.Event, *dbpkg.AgentInstanceTaskSnapshot) error GetAgentInstanceTask(context.Context, string, string) (*a2atype.Task, error) ListAgentInstanceTasks(context.Context, string, string, a2atype.TaskState, *time.Time, int) ([]*a2atype.Task, int, error) @@ -292,17 +291,7 @@ func (g *Gateway) CancelTask(ctx context.Context, req *a2atype.CancelTaskRequest defer client.Destroy() canceled, err := client.CancelTask(ctx, req) if err != nil { - // A cancel the reader asked for has to free the conversation even when the - // runtime cannot help — it may have no record of the task, or be - // unreachable. Without this an instance whose turn is parked or stranded - // stays unable to answer with no way out, which is the defect this path - // exists to escape. The store only acts while the task is still the active - // one, so a turn that has already finished is untouched. - local, localErr := g.cancelTaskLocally(ctx, instance.GetId(), req.ID) - if localErr != nil || local == nil { - return nil, err - } - return local, nil + return nil, err } if err := validateTaskInfo(canceled, task); err != nil { return nil, a2atype.NewError(a2atype.ErrInternalError, err.Error()) @@ -542,13 +531,6 @@ func (g *Gateway) reconcileActiveTask(ctx context.Context, instance *apiv1alpha1 if err != nil { return err } - // A parked turn needs no runtime round trip: its state already says the - // runtime stopped and is waiting on a human. Nothing here may clear it — the - // question is still answerable, and only the reader can decide to give it up, - // which they do with CancelTask. - if dbpkg.TaskParkedAwaitingUser(active.Status.State) { - return errParkedTaskHoldsSlot - } client, err := g.dialer.Dial(ctx, instance) if err != nil { ctrllog.FromContext(ctx).Error(err, "failed to reconcile active AgentInstance task", "task", active.ID) @@ -561,16 +543,6 @@ func (g *Gateway) reconcileActiveTask(ctx context.Context, instance *apiv1alpha1 for event, eventErr := range client.SubscribeToTask(ctx, &a2atype.SubscribeToTaskRequest{ID: active.ID}) { if errors.Is(eventErr, a2atype.ErrTaskNotFound) { latest, err := client.GetTask(ctx, &a2atype.GetTaskRequest{ID: active.ID}) - // A runtime that has no record of the task at all is ambiguous: the task - // may never have been dispatched, in which case the dispatch is still - // coming and interrupting it would race. Age is the only discriminator — - // past the dispatch grace period no dispatch can still be in flight, so - // the slot is stale rather than contended, and without this an instance - // stranded by a lost runtime record could never answer again. A task with - // no status timestamp has an unknown age and stays untouched. - if errors.Is(err, a2atype.ErrTaskNotFound) && staleBeyondDispatch(active) { - return g.interruptTask(ctx, instance.GetId(), active.ID) - } if err != nil || latest == nil { return dbpkg.ErrAgentInstanceTaskConflict } @@ -744,46 +716,10 @@ func (g *Gateway) failAttempt(ctx context.Context, attempt *preparedSend) { } } -// errParkedTaskHoldsSlot means the instance's active task is waiting on a human, -// not executing. It is reported rather than cleared: the pending question is -// valid, and the reader may still want to answer it. Discarding it silently to -// make room for an unrelated message would throw away the thing the agent is -// waiting for. -var errParkedTaskHoldsSlot = errors.New("AgentInstance task is waiting for a reply") - -// dispatchGracePeriod bounds how long a task the runtime has never heard of may -// still be mid-dispatch. Well above any real dispatch, so a live one is never -// interrupted, and short enough that a reader is not locked out of their own -// conversation for long. -const dispatchGracePeriod = 5 * time.Minute - -func staleBeyondDispatch(task *a2atype.Task) bool { - if task.Status.Timestamp == nil { - return false - } - return time.Since(*task.Status.Timestamp) > dispatchGracePeriod -} - -func (g *Gateway) cancelTaskLocally(ctx context.Context, instanceID string, taskID a2atype.TaskID) (*a2atype.Task, error) { - canceled, err := g.store.AbandonActiveAgentInstanceTask(ctx, instanceID, string(taskID)) - if err != nil || !canceled { - return nil, err - } - ctrllog.FromContext(ctx).Info("recorded AgentInstance task cancellation without the runtime", "instance", instanceID, "task", taskID) - return g.store.GetAgentInstanceTask(ctx, instanceID, string(taskID)) -} - func (g *Gateway) storeError(ctx context.Context, err error) error { if errors.Is(err, dbpkg.ErrIdempotencyConflict) { return a2atype.NewError(a2atype.ErrInvalidRequest, "message ID was already used with a different request") } - if errors.Is(err, errParkedTaskHoldsSlot) { - // Naming the way out matters: saying only that a task was active is why a - // conversation waiting on an unanswered question read as a broken agent - // rather than as one waiting for the reader. - return a2atype.NewError(a2atype.ErrUnsupportedOperation, - "the agent is waiting for a reply to its last message; answer it, or cancel that task to start a new one") - } if errors.Is(err, dbpkg.ErrAgentInstanceTaskConflict) { return a2atype.NewError(a2atype.ErrUnsupportedOperation, "AgentInstance already has an active task") } diff --git a/go/core/v2/a2agateway/gateway_test.go b/go/core/v2/a2agateway/gateway_test.go index 8464fcfb1..049b91d15 100644 --- a/go/core/v2/a2agateway/gateway_test.go +++ b/go/core/v2/a2agateway/gateway_test.go @@ -48,10 +48,6 @@ type gatewayTestStore struct { active *a2atype.Task interruptResult bool interrupted bool - abandonResult bool - abandoned bool - claimed *a2atype.Task - restored *a2atype.Task createdTasks int stored []a2atype.Event snapshot *dbpkg.AgentInstanceTaskSnapshot @@ -117,40 +113,6 @@ func (s *gatewayTestStore) InterruptActiveAgentInstanceTask(_ context.Context, _ return true, nil } -func (s *gatewayTestStore) AbandonActiveAgentInstanceTask(_ context.Context, _ string, taskID string) (bool, error) { - if !s.abandonResult || s.active == nil || string(s.active.ID) != taskID { - return false, nil - } - canceled := *s.active - canceled.Status = a2atype.TaskStatus{State: a2atype.TaskStateCanceled} - s.task = &canceled - s.active = nil - s.abandoned = true - return true, nil -} - -func (s *gatewayTestStore) ClaimParkedAgentInstanceTask(_ context.Context, _ string, taskID string) (*a2atype.Task, bool, error) { - if s.active == nil { - return nil, false, dbpkg.ErrNotFound - } - if string(s.active.ID) != taskID || !dbpkg.TaskParkedAwaitingUser(s.active.Status.State) { - return nil, false, nil - } - parked := *s.active - working := *s.active - working.Status = a2atype.TaskStatus{State: a2atype.TaskStateWorking} - s.active = &working - s.claimed = &parked - return &parked, true, nil -} - -func (s *gatewayTestStore) RestoreParkedAgentInstanceTask(_ context.Context, _ string, task *a2atype.Task) error { - restored := *task - s.active = &restored - s.restored = &restored - return nil -} - func (s *gatewayTestStore) GetAgentInstanceTask(_ context.Context, _ string, taskID string) (*a2atype.Task, error) { if s.taskErr != nil { return nil, s.taskErr @@ -1065,236 +1027,3 @@ func TestGatewayIgnoresASessionShare(t *testing.T) { // question* (`ask_user` is a long-running call), so the send must be refused with // a reason the reader can act on, and the question must survive: only the reader // may give it up. -func TestGatewayRefusesButPreservesAParkedTurn(t *testing.T) { - for _, test := range []struct { - name string - state a2atype.TaskState - wantSent bool - wantQueries int - }{ - {name: "input required", state: a2atype.TaskStateInputRequired, wantSent: false, wantQueries: 0}, - {name: "auth required", state: a2atype.TaskStateAuthRequired, wantSent: false, wantQueries: 0}, - // A turn the runtime is still executing keeps the slot too, but for the - // other reason: an execution really is in flight. - {name: "working is still live", state: a2atype.TaskStateWorking, wantSent: false, wantQueries: 1}, - {name: "submitted is still live", state: a2atype.TaskStateSubmitted, wantSent: false, wantQueries: 1}, - } { - t.Run(test.name, func(t *testing.T) { - active := &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: test.state}} - runtime := &gatewayTestRuntime{subscribeEvent: active} - store := &gatewayTestStore{instance: gatewayTestInstance(), active: active, abandonResult: true, interruptResult: true} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) - - _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()) - if (err == nil) != test.wantSent { - t.Fatalf("SendMessage() error = %v, want sent %t", err, test.wantSent) - } - // The pending question must still be there afterwards. - if store.abandoned || store.interrupted || store.active != active { - t.Fatalf("the parked turn was discarded: abandoned=%v interrupted=%v active=%#v", store.abandoned, store.interrupted, store.active) - } - // A parked turn is diagnosed without asking the runtime anything: its - // state already says no execution is in flight. Counting dials cannot - // show this, so count the reconcile's own round trip. - if runtime.subscribeCalls != test.wantQueries || runtime.getTaskCalls != 0 { - t.Fatalf("runtime queries: subscribe = %d (want %d), GetTask = %d (want 0)", runtime.subscribeCalls, test.wantQueries, runtime.getTaskCalls) - } - if dbpkg.TaskParkedAwaitingUser(test.state) && !strings.Contains(err.Error(), "waiting for a reply") { - // The old wording named only the symptom, so a conversation waiting on - // the reader was indistinguishable from a wedged one. - t.Fatalf("refusal for a parked turn = %q, want it to say what the agent is waiting for", err) - } - }) - } -} - -// TestGatewayCancelTaskFreesAConversationTheRuntimeCannotHelpWith pins the -// deliberate recovery. Cancel is the reader choosing to give up a pending -// question, and it has to work even when the runtime has no record of the task — -// otherwise a parked or stranded turn leaves the conversation unable to answer -// with no way out. -func TestGatewayCancelTaskFreesAConversationTheRuntimeCannotHelpWith(t *testing.T) { - for _, test := range []struct { - name string - abandonResult bool - wantErr bool - wantAbandoned bool - }{ - {name: "the active turn is canceled locally", abandonResult: true, wantErr: false, wantAbandoned: true}, - // Nothing to cancel means the runtime's own error is the honest answer. - {name: "a turn that already finished is left to the runtime error", abandonResult: false, wantErr: true, wantAbandoned: false}, - } { - t.Run(test.name, func(t *testing.T) { - parked := &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired}} - runtime := &gatewayTestRuntime{cancelErr: a2atype.ErrTaskNotFound} - store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked, abandonResult: test.abandonResult} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) - - _, err := gateway.CancelTask(gatewayTestContext(), &a2atype.CancelTaskRequest{ID: parked.ID}) - if (err != nil) != test.wantErr { - t.Fatalf("CancelTask() error = %v, want error %t", err, test.wantErr) - } - if store.abandoned != test.wantAbandoned { - t.Fatalf("abandoned = %v, want %t", store.abandoned, test.wantAbandoned) - } - }) - } -} - -// TestGatewaySendAfterCancellingAParkedTurnSucceeds is the whole recovery, end to -// end: refused while the question stands, accepted once the reader cancels it. -func TestGatewaySendAfterCancellingAParkedTurnSucceeds(t *testing.T) { - parked := &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired}} - runtime := &gatewayTestRuntime{cancelErr: a2atype.ErrTaskNotFound} - store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked, abandonResult: true} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) - - if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()); err == nil { - t.Fatal("SendMessage() was accepted while a question was pending") - } - if _, err := gateway.CancelTask(gatewayTestContext(), &a2atype.CancelTaskRequest{ID: parked.ID}); err != nil { - t.Fatal(err) - } - if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()); err != nil { - t.Fatalf("SendMessage() after cancelling the parked turn = %v", err) - } -} - -// TestGatewayReapsAStaleSlotOnlyOnceDispatchCannotBeInFlight pins both halves of -// the age gate. A task the runtime has never heard of may simply not have been -// dispatched yet, so interrupting a fresh one races the dispatch; an old one -// cannot still be arriving, and leaving it would make the instance permanently -// unable to answer. -func TestGatewayReapsAStaleSlotOnlyOnceDispatchCannotBeInFlight(t *testing.T) { - stale := time.Now().Add(-dispatchGracePeriod - time.Minute) - fresh := time.Now() - for _, test := range []struct { - name string - timestamp *time.Time - wantInterrupted bool - }{ - {name: "older than the grace period is reaped", timestamp: &stale, wantInterrupted: true}, - {name: "within the grace period is left alone", timestamp: &fresh, wantInterrupted: false}, - {name: "an unknown age is left alone", timestamp: nil, wantInterrupted: false}, - } { - t.Run(test.name, func(t *testing.T) { - active := &a2atype.Task{ - ID: "active", ContextID: gatewayTestID, - Status: a2atype.TaskStatus{State: a2atype.TaskStateWorking, Timestamp: test.timestamp}, - } - runtime := &gatewayTestRuntime{taskErr: a2atype.ErrTaskNotFound, subscribeErr: a2atype.ErrTaskNotFound} - store := &gatewayTestStore{instance: gatewayTestInstance(), active: active, interruptResult: true} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) - - _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestRequest()) - if (err == nil) != test.wantInterrupted { - t.Fatalf("SendMessage() error = %v, want reaped %t", err, test.wantInterrupted) - } - if store.interrupted != test.wantInterrupted { - t.Fatalf("interrupted = %v, want %t", store.interrupted, test.wantInterrupted) - } - }) - } -} - -func gatewayTestParkedTask() *a2atype.Task { - return &a2atype.Task{ - ID: "parked", ContextID: gatewayTestID, - Status: a2atype.TaskStatus{State: a2atype.TaskStateInputRequired}, - } -} - -func gatewayTestReply(taskID a2atype.TaskID) *a2atype.SendMessageRequest { - message := a2atype.NewMessage(a2atype.MessageRoleUser, a2atype.NewTextPart("Medium")) - message.TaskID = taskID - return &a2atype.SendMessageRequest{Message: message} -} - -func TestGatewayRefusesAReplyThatCannotBeDelivered(t *testing.T) { - for _, test := range []struct { - name string - // known is the task the store can find by id, which is what separates an - // unknown task from one that exists and is simply past answering. - known *a2atype.Task - active *a2atype.Task - taskID a2atype.TaskID - wantNotFound bool - }{ - { - // The replay guard: a duplicate reply finds the turn already moved on. - // Reporting that as "task not found" — which it used to — is a lie about a - // task sitting in the reader's own transcript. - name: "a turn already working is no longer waiting", - known: &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateWorking}}, - active: &a2atype.Task{ID: "parked", ContextID: gatewayTestID, Status: a2atype.TaskStatus{State: a2atype.TaskStateWorking}}, - taskID: "parked", - wantNotFound: false, - }, - { - name: "a reply naming a task that does not exist", - active: gatewayTestParkedTask(), - taskID: "no-such-task", - wantNotFound: true, - }, - { - name: "a reply with no turn to answer at all", - active: nil, - taskID: "parked", - wantNotFound: true, - }, - } { - t.Run(test.name, func(t *testing.T) { - runtime := &gatewayTestRuntime{} - store := &gatewayTestStore{instance: gatewayTestInstance(), active: test.active, task: test.known} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) - - _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(test.taskID)) - if err == nil { - t.Fatal("SendMessage() accepted a reply it could not deliver") - } - if errors.Is(err, a2atype.ErrTaskNotFound) != test.wantNotFound { - t.Fatalf("refusal = %v, want task-not-found %t", err, test.wantNotFound) - } - if runtime.sent || store.createdTasks != 0 { - t.Fatalf("undeliverable reply: reached runtime = %v, tasks reserved = %d", runtime.sent, store.createdTasks) - } - }) - } -} - -// TestGatewayRepliedTwiceDeliversOnce is the replay guard measured rather than -// reasoned about: the same answer sent twice must reach the runtime once. -func TestGatewayRepliedTwiceDeliversOnce(t *testing.T) { - parked := gatewayTestParkedTask() - runtime := &gatewayTestRuntime{} - store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{client: gatewayTestClient(t, runtime)}, &gatewayTestWorkflow{}, gatewayTestURL) - - if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(parked.ID)); err != nil { - t.Fatal(err) - } - if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(parked.ID)); err == nil { - t.Fatal("the same reply was accepted twice") - } - if runtime.sendCalls != 1 { - t.Fatalf("runtime received %d sends, want exactly 1", runtime.sendCalls) - } -} - -// TestGatewayRestoresTheQuestionWhenAReplyCannotBeDelivered keeps a transport -// failure from turning an answerable question into a dead turn. -func TestGatewayRestoresTheQuestionWhenAReplyCannotBeDelivered(t *testing.T) { - parked := gatewayTestParkedTask() - store := &gatewayTestStore{instance: gatewayTestInstance(), task: parked, active: parked} - gateway := New(store, &gatewayTestAuthorizer{}, &gatewayTestDialer{err: errors.New("runtime unavailable")}, &gatewayTestWorkflow{}, gatewayTestURL) - - if _, err := gateway.SendMessage(gatewayTestContext(), gatewayTestReply(parked.ID)); err == nil { - t.Fatal("SendMessage() reported success with no runtime") - } - // The last thing written must put the question back where a reader can answer - // it. Failing to deliver an answer does not make the question unanswerable, and - // leaving the claimed task behind would stop the conversation for good. - if store.task == nil || !dbpkg.TaskParkedAwaitingUser(store.task.Status.State) { - t.Fatalf("task left behind = %#v, want the question back awaiting the reader", store.task) - } -} diff --git a/go/core/v2/agentinstance/service.go b/go/core/v2/agentinstance/service.go index 7c43c63c9..af22ebab0 100644 --- a/go/core/v2/agentinstance/service.go +++ b/go/core/v2/agentinstance/service.go @@ -8,27 +8,18 @@ import ( "errors" "fmt" "strings" - "unicode" - "unicode/utf8" - a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/google/uuid" dbpkg "github.com/kagent-dev/kagent/go/api/database" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" "github.com/kagent-dev/kagent/go/core/internal/service/serviceerrors" "github.com/kagent-dev/kagent/go/core/pkg/auth" utilvalidation "k8s.io/apimachinery/pkg/util/validation" - ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) const ( defaultPageSize = 50 maxPageSize = 100 - // maxNameLength bounds the conversation's display name. It is counted in - // runes rather than bytes so a non-ASCII title is not cut to a third of the - // length an ASCII one gets, and it is generous enough to hold a title derived - // from a first message while still fitting a list column. - maxNameLength = 200 ) type store interface { @@ -39,8 +30,6 @@ type store interface { CreateAgentInstanceShare(context.Context, dbpkg.AgentInstanceShare) (*dbpkg.AgentInstanceShare, error) ListAgentInstanceShares(context.Context, string, string, string, string, int) ([]dbpkg.AgentInstanceShare, error) DeleteAgentInstanceShare(context.Context, string, string, string) error - GetActiveAgentInstanceTask(context.Context, string) (*a2a.Task, error) - InterruptActiveAgentInstanceTask(context.Context, string, string) (bool, error) } type instanceWorkflow interface { @@ -89,9 +78,6 @@ func (s *Service) Create(ctx context.Context, namespace, harness, template, requ if err := validateCreate(namespace, harness, template, requestID); err != nil { return nil, err } - if err := validateName(name); err != nil { - return nil, err - } creator, err := s.authorize(ctx, auth.VerbCreate, namespace+"/"+template) if err != nil { return nil, err @@ -139,12 +125,6 @@ func (s *Service) Get(ctx context.Context, namespace, id string) (*apiv1alpha1.A // service this is a write, and it authorizes as one: a reader who may list and // open a conversation must not be able to retitle it. func (s *Service) Rename(ctx context.Context, namespace, id, name string) (*apiv1alpha1.AgentInstance, error) { - if err := validateIdentity(namespace, id); err != nil { - return nil, err - } - if err := validateName(name); err != nil { - return nil, err - } creator, err := s.authorize(ctx, auth.VerbUpdate, namespace+"/"+id) if err != nil { return nil, err @@ -163,12 +143,6 @@ func (s *Service) List(ctx context.Context, request ListRequest) (ListResult, er if err := validateNamespace(request.Namespace); err != nil { return ListResult{}, err } - if err := validateOptionalName("agent_template", request.AgentTemplate); err != nil { - return ListResult{}, err - } - if err := validateOptionalName("harness", request.Harness); err != nil { - return ListResult{}, err - } userID, err := s.authorize(ctx, auth.VerbGet, request.Namespace) if err != nil { return ListResult{}, err @@ -253,41 +227,9 @@ func (s *Service) Suspend(ctx context.Context, namespace, id string) (*apiv1alph if err != nil { return nil, serviceerrors.NewUnavailable("Failed to suspend AgentInstance", err) } - s.reapActiveTask(ctx, instance.GetId()) return instance, nil } -// reapActiveTask records that the instance's in-flight turn ended, because -// suspending stops the runtime executing it. Without this the turn stays -// non-terminal and holds the instance's single active-task slot, and the -// instance is left unable to answer until something else notices. -// -// A turn parked awaiting the reader is deliberately left alone. Suspending is a -// pause, not an abandonment: the agent's question is still valid and still -// answerable after a resume, so failing it here would destroy the very thing the -// conversation is waiting for — and would do so invisibly, since a suspend says -// nothing about tasks. -// -// A failure here is logged rather than returned: the suspend itself succeeded, -// and reporting it as failed would invite a retry of an operation that already -// happened. -func (s *Service) reapActiveTask(ctx context.Context, instanceID string) { - active, err := s.store.GetActiveAgentInstanceTask(ctx, instanceID) - if errors.Is(err, dbpkg.ErrNotFound) { - return - } - if err != nil { - ctrllog.FromContext(ctx).Error(err, "failed to read active task while suspending AgentInstance", "instance", instanceID) - return - } - if dbpkg.TaskParkedAwaitingUser(active.Status.State) { - return - } - if _, err := s.store.InterruptActiveAgentInstanceTask(ctx, instanceID, string(active.ID)); err != nil { - ctrllog.FromContext(ctx).Error(err, "failed to interrupt active task while suspending AgentInstance", "instance", instanceID, "task", active.ID) - } -} - func (s *Service) Resume(ctx context.Context, namespace, id string) (*apiv1alpha1.AgentInstance, error) { if err := validateIdentity(namespace, id); err != nil { return nil, err @@ -450,44 +392,6 @@ func validateCreate(namespace, harness, template, requestID string) error { return nil } -// validateName bounds a conversation's display name. An empty name is valid and -// means unnamed. Control characters are refused because they render as an -// invisible break in a table cell or silently truncate a header, and surrounding -// whitespace is refused rather than trimmed: quietly rewriting what someone -// typed reads on screen as a rename that did not take. -func validateName(name string) error { - if name == "" { - return nil - } - if strings.TrimSpace(name) != name { - return serviceerrors.NewInvalidArgument("name must not have leading or trailing whitespace", nil) - } - if utf8.RuneCountInString(name) > maxNameLength { - return serviceerrors.NewInvalidArgument(fmt.Sprintf("name must be at most %d characters", maxNameLength), nil) - } - if !utf8.ValidString(name) { - return serviceerrors.NewInvalidArgument("name must be valid UTF-8", nil) - } - for _, character := range name { - if unicode.IsControl(character) { - return serviceerrors.NewInvalidArgument("name must not contain control characters", nil) - } - } - return nil -} - -// validateOptionalName checks a filter that names a Kubernetes object, where -// absent means "do not filter". -func validateOptionalName(field, value string) error { - if value == "" { - return nil - } - if problems := utilvalidation.IsDNS1123Subdomain(value); len(problems) > 0 { - return serviceerrors.NewInvalidArgument(field+" is invalid: "+strings.Join(problems, "; "), nil) - } - return nil -} - func validateIdentity(namespace, id string) error { if err := validateNamespace(namespace); err != nil { return err diff --git a/go/core/v2/agentinstance/service_test.go b/go/core/v2/agentinstance/service_test.go index cfc480a06..ee8d9a285 100644 --- a/go/core/v2/agentinstance/service_test.go +++ b/go/core/v2/agentinstance/service_test.go @@ -5,10 +5,8 @@ import ( "context" "crypto/sha256" "errors" - "strings" "testing" - a2a "github.com/a2aproject/a2a-go/v2/a2a" "github.com/google/uuid" dbpkg "github.com/kagent-dev/kagent/go/api/database" apiv1alpha1 "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1" @@ -43,8 +41,6 @@ type serviceTestStore struct { renameUserID string renameErr error getCreator string - activeTask *a2a.Task - interrupted string } func (s *serviceTestStore) CreateAgentInstance(_ context.Context, instance *apiv1alpha1.AgentInstance, requestID string) (*apiv1alpha1.AgentInstance, bool, error) { @@ -76,18 +72,6 @@ func (s *serviceTestStore) RenameAgentInstance(_ context.Context, _, id, userID, return s.renamed, nil } -func (s *serviceTestStore) GetActiveAgentInstanceTask(context.Context, string) (*a2a.Task, error) { - if s.activeTask == nil { - return nil, dbpkg.ErrNotFound - } - return s.activeTask, nil -} - -func (s *serviceTestStore) InterruptActiveAgentInstanceTask(_ context.Context, _, taskID string) (bool, error) { - s.interrupted = taskID - return true, nil -} - func (s *serviceTestStore) CreateAgentInstanceShare(_ context.Context, share dbpkg.AgentInstanceShare) (*dbpkg.AgentInstanceShare, error) { s.share = share return &s.share, nil @@ -298,52 +282,6 @@ func TestServiceCreateCarriesTheNameAndLeavesAnOmittedOneEmpty(t *testing.T) { } } -func TestServiceRejectsInvalidNames(t *testing.T) { - for _, test := range []struct { - name string - given string - wantErr bool - }{ - {name: "empty is unnamed", given: "", wantErr: false}, - {name: "ordinary title", given: "Why is the pod pending?", wantErr: false}, - {name: "punctuation and emoji", given: "deploy 🚀 v2 — take 3", wantErr: false}, - {name: "at the length limit", given: strings.Repeat("a", maxNameLength), wantErr: false}, - {name: "runes not bytes at the limit", given: strings.Repeat("é", maxNameLength), wantErr: false}, - {name: "over the length limit", given: strings.Repeat("a", maxNameLength+1), wantErr: true}, - {name: "newline", given: "first line\nsecond line", wantErr: true}, - {name: "carriage return", given: "title\r", wantErr: true}, - {name: "tab", given: "a\tb", wantErr: true}, - {name: "leading whitespace", given: " title", wantErr: true}, - {name: "trailing whitespace", given: "title ", wantErr: true}, - } { - t.Run(test.name, func(t *testing.T) { - service := NewService(&serviceTestStore{}, serviceTestAuthorizer{}, serviceTestWorkflow{}) - ctx := serviceTestContext("alice") - createErr := service.createError(ctx, test.given) - renameErr := service.renameError(ctx, test.given) - if (createErr != nil) != test.wantErr || (renameErr != nil) != test.wantErr { - t.Fatalf("create error = %v, rename error = %v, want error %t", createErr, renameErr, test.wantErr) - } - if test.wantErr && !serviceerrors.IsCode(createErr, serviceerrors.CodeInvalidArgument) { - t.Fatalf("create error = %v, want code %s", createErr, serviceerrors.CodeInvalidArgument) - } - }) - } -} - -// createError and renameError keep the validation table above honest: both entry -// points must apply the same rules, or a name refused on create is accepted on -// rename and reaches the database anyway. -func (s *Service) createError(ctx context.Context, name string) error { - _, err := s.Create(ctx, "team-a", "kagent", "assistant", "request-1", name) - return err -} - -func (s *Service) renameError(ctx context.Context, name string) error { - _, err := s.Rename(ctx, "team-a", "11111111-1111-4111-8111-111111111111", name) - return err -} - func TestServiceRenameRequiresWriteAuthorizationAndScopesToTheOwner(t *testing.T) { instanceID := "11111111-1111-4111-8111-111111111111" @@ -471,7 +409,6 @@ func TestServiceListPassesTheAgentPairThroughToTheStore(t *testing.T) { for _, test := range []struct { name string request ListRequest - wantErr bool wantPair [2]string }{ { @@ -489,27 +426,11 @@ func TestServiceListPassesTheAgentPairThroughToTheStore(t *testing.T) { request: ListRequest{Namespace: "team-a"}, wantPair: [2]string{"", ""}, }, - { - name: "an invalid template name is refused rather than matching nothing", - request: ListRequest{Namespace: "team-a", AgentTemplate: "NOT A NAME"}, - wantErr: true, - }, - { - name: "an invalid harness name is refused", - request: ListRequest{Namespace: "team-a", Harness: "NOT A NAME"}, - wantErr: true, - }, } { t.Run(test.name, func(t *testing.T) { store := &serviceTestStore{} service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{}) _, err := service.List(serviceTestContext("alice"), test.request) - if test.wantErr { - if !serviceerrors.IsCode(err, serviceerrors.CodeInvalidArgument) { - t.Fatalf("List() error = %v, want code %s", err, serviceerrors.CodeInvalidArgument) - } - return - } if err != nil { t.Fatal(err) } @@ -520,60 +441,3 @@ func TestServiceListPassesTheAgentPairThroughToTheStore(t *testing.T) { }) } } - -// TestServiceSuspendReapsTheActiveTurn pins the half of the stranded-task fix that -// stops the strand forming. Suspending stops the runtime, so an in-flight turn is -// over; leaving it non-terminal holds the instance's one active-task slot and -// every later send is refused with "AgentInstance already has an active task". -func TestServiceSuspendReapsTheActiveTurn(t *testing.T) { - for _, test := range []struct { - name string - active *a2a.Task - workflowErr error - wantInterrupted string - }{ - { - name: "an in-flight turn is interrupted", - active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateWorking}}, - wantInterrupted: "task-1", - }, - { - name: "no active turn is left alone", - active: nil, - wantInterrupted: "", - }, - { - // A suspend that did not happen must not close a turn that is still running. - name: "a failed suspend interrupts nothing", - active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateWorking}}, - workflowErr: errors.New("substrate unavailable"), - wantInterrupted: "", - }, - { - // Suspending is a pause, not an abandonment. A question the agent asked is - // still valid and still answerable after a resume, so failing it here would - // destroy the thing the conversation is waiting for — invisibly, since a - // suspend says nothing about tasks. - name: "a turn waiting on the reader survives a suspend", - active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateInputRequired}}, - wantInterrupted: "", - }, - { - name: "a turn waiting on authorization survives a suspend", - active: &a2a.Task{ID: "task-1", Status: a2a.TaskStatus{State: a2a.TaskStateAuthRequired}}, - wantInterrupted: "", - }, - } { - t.Run(test.name, func(t *testing.T) { - store := &serviceTestStore{activeTask: test.active} - service := NewService(store, serviceTestAuthorizer{}, serviceTestWorkflow{err: test.workflowErr}) - _, err := service.Suspend(serviceTestContext("alice"), "team-a", "8bd650a8-9775-488f-8bc1-0d52bf7bdcab") - if (err != nil) != (test.workflowErr != nil) { - t.Fatalf("Suspend() error = %v", err) - } - if store.interrupted != test.wantInterrupted { - t.Fatalf("interrupted task = %q, want %q", store.interrupted, test.wantInterrupted) - } - }) - } -} diff --git a/proto/kagent/api/v1alpha1/agent_instances.proto b/proto/kagent/api/v1alpha1/agent_instances.proto index a83fab0e6..fb5c02a9a 100644 --- a/proto/kagent/api/v1alpha1/agent_instances.proto +++ b/proto/kagent/api/v1alpha1/agent_instances.proto @@ -79,10 +79,11 @@ message CreateAgentInstanceRequest { min_len: 1 max_len: 128 }]; - // Optional display name. Omit it to create an unnamed conversation. Unvalidated - // because empty is the ordinary case: a conversation is usually named later, or - // never. - string name = 5; + // Optional display name. Empty means unnamed. + string name = 5 [(buf.validate.field).string = { + max_len: 200 + pattern: "^(?:$|[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}]*[^\\p{Z}\\p{Cc}])?)$" + }]; } message CreateAgentInstanceResponse { @@ -108,8 +109,14 @@ message ListAgentInstancesRequest { // (AgentTemplate, Harness) pair. Either may be given alone. Both are matched // against the pair the instance's prepared revision was built from, so they // also select instances created before these fields existed. - string agent_template = 5; - string harness = 6; + string agent_template = 5 [(buf.validate.field).string = { + max_len: 253 + pattern: "^(?:$|[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*)$" + }]; + string harness = 6 [(buf.validate.field).string = { + max_len: 253 + pattern: "^(?:$|[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*)$" + }]; } message ListAgentInstancesResponse { @@ -118,11 +125,18 @@ message ListAgentInstancesResponse { } message RenameAgentInstanceRequest { - string namespace = 1; - string agent_instance_id = 2; + string namespace = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 63 + pattern: "^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$" + }]; + string agent_instance_id = 2 [(buf.validate.field).string.uuid = true]; // The new display name. Empty clears the name, returning the conversation to // being identified by its id. - string name = 3; + string name = 3 [(buf.validate.field).string = { + max_len: 200 + pattern: "^(?:$|[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}]*[^\\p{Z}\\p{Cc}])?)$" + }]; } message RenameAgentInstanceResponse { diff --git a/proto/kagent/api/v1alpha1/harnesses.proto b/proto/kagent/api/v1alpha1/harnesses.proto index 6e8f658e1..01b149871 100644 --- a/proto/kagent/api/v1alpha1/harnesses.proto +++ b/proto/kagent/api/v1alpha1/harnesses.proto @@ -20,9 +20,7 @@ option go_package = "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1 // so this is a separate service rather than more RPCs on AgentService. service HarnessService { rpc ListHarnesses(ListHarnessesRequest) returns (ListHarnessesResponse); - rpc GetHarness(GetHarnessRequest) returns (GetHarnessResponse); rpc CreateHarness(CreateHarnessRequest) returns (CreateHarnessResponse); - rpc UpdateHarness(UpdateHarnessRequest) returns (UpdateHarnessResponse); rpc DeleteHarness(DeleteHarnessRequest) returns (DeleteHarnessResponse); } @@ -54,14 +52,6 @@ message ListHarnessesResponse { repeated Harness harnesses = 1; } -message GetHarnessRequest { - ResourceReference ref = 1; -} - -message GetHarnessResponse { - Harness harness = 1; -} - message CreateHarnessRequest { ResourceReference ref = 1; StructuredObject resource = 2; @@ -71,15 +61,6 @@ message CreateHarnessResponse { Harness harness = 1; } -message UpdateHarnessRequest { - ResourceReference ref = 1; - StructuredObject resource = 2; -} - -message UpdateHarnessResponse { - Harness harness = 1; -} - message DeleteHarnessRequest { ResourceReference ref = 1; } diff --git a/proto/kagent/api/v1alpha1/system.proto b/proto/kagent/api/v1alpha1/system.proto index 04362094b..5bc3ef780 100644 --- a/proto/kagent/api/v1alpha1/system.proto +++ b/proto/kagent/api/v1alpha1/system.proto @@ -3,8 +3,6 @@ syntax = "proto3"; package kagent.api.v1alpha1; import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "kagent/api/v1alpha1/common.proto"; option go_package = "github.com/kagent-dev/kagent/go/api/gen/kagent/api/v1alpha1;apiv1alpha1"; @@ -12,42 +10,7 @@ service SystemService { rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse); rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse); - - // GetSubstrateStatus returns the entire inventory in one message: every - // worker pool, actor template, actor and worker, unpaginated and unfiltered. - // - // It does not survive a real cluster. A deployment reporting 103,134 actors - // answers with a message the gRPC client refuses outright — "trying to send - // message larger than max (43016460 vs. 16777216)" — so the caller gets no - // inventory at all rather than a large one. Raising the ceiling moves the - // number without changing the shape. - // - // Prefer GetSubstrateSummary with ListSubstrateActors and - // ListSubstrateWorkers, which bound what any single response can carry. This - // RPC is kept for callers that predate them and for the small clusters where - // it still works. rpc GetSubstrateStatus(GetSubstrateStatusRequest) returns (GetSubstrateStatusResponse); - - // GetSubstrateSummary returns counts computed server-side, plus the two lists - // that are inherently small. - // - // This is the only honest source of a total. A caller that counts a page and - // presents the result as a total reports "3 actors" for a cluster running a - // hundred thousand, which is the specific failure the paged RPCs below would - // otherwise introduce. - rpc GetSubstrateSummary(GetSubstrateSummaryRequest) returns (GetSubstrateSummaryResponse); - - // ListSubstrateActors pages the actors, narrowing them server-side. - // - // Paged because this is one of the two lists whose length is set by the - // cluster rather than by configuration, and filtered server-side for the same - // reason: narrowing a page that has already been fetched searches only what - // was fetched, so a match on page nine reads on screen as "no matches". - rpc ListSubstrateActors(ListSubstrateActorsRequest) returns (ListSubstrateActorsResponse); - - // ListSubstrateWorkers pages the worker assignments. The mirror of - // ListSubstrateActors. - rpc ListSubstrateWorkers(ListSubstrateWorkersRequest) returns (ListSubstrateWorkersResponse); } message GetVersionRequest {} @@ -88,175 +51,6 @@ message GetSubstrateStatusResponse { repeated SubstrateWorker workers = 6; } -message GetSubstrateSummaryRequest { - // Namespace narrows the inventory. Empty means every namespace the - // controller observes, as it does on GetSubstrateStatusRequest. - string namespace = 1; -} - -// SubstrateStatusCount is how many rows carry one status. -// -// Status is a plain string on the wire rather than an enum: ate-api and the -// ActorTemplate controller each fill it in their own vocabulary, so a closed -// set here would drop a status a newer substrate reports. Counting whatever -// arrives keeps the tally complete even when a value is one this build has -// never seen. -message SubstrateStatusCount { - string status = 1; - int32 count = 2; -} - -message GetSubstrateSummaryResponse { - // Enabled is false when the controller has no ate-api endpoint configured, - // which is an ordinary deployment rather than a failure. - bool enabled = 1; - - // AteApiError is set when ate-api answered with an error on an otherwise - // successful read: the Kubernetes-derived halves below are complete while the - // runtime counts may be short. Distinct from the RPC failing, and worth - // reporting differently. - string ate_api_error = 2; - - // Worker pools and actor templates are bounded by how the cluster is - // configured rather than by how much work it is doing — a handful either way - // — so they ride inline instead of costing two more round trips. - repeated SubstrateWorkerPool worker_pools = 3; - repeated SubstrateActorTemplate actor_templates = 4; - - // Totals over everything in scope, before any filter. - int32 actor_count = 5; - int32 worker_count = 6; - - // RunningActorCount and BusyWorkerCount are the numerators the inventory is - // actually read by: how much of what exists is doing something. A worker is - // busy when an actor is placed on it. - int32 running_actor_count = 7; - int32 busy_worker_count = 8; - - // ActorStatusCounts is every status present, with how many actors hold it, - // ordered by status. The whole distribution rather than the running count - // alone, so a caller can say what the rest are without reading them. - repeated SubstrateStatusCount actor_status_counts = 9; - - // ComputedAt is when this answer was produced, which is not necessarily now. - // - // The substrate reads are memoised for a fraction of a second, because each one - // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - // them. A cache is also exactly how a polling control becomes a lie, so the age - // travels with the answer: a caller can say "as of 0.4s ago" rather than - // implying "now", and a reader can tell a stalled cluster from a stalled read. - google.protobuf.Timestamp computed_at = 10; -} - -// SubstrateSortOrder is the direction a paged substrate read is sorted in. -enum SubstrateSortOrder { - // Unspecified sorts ascending, which is what every default order below reads - // naturally in. - SUBSTRATE_SORT_ORDER_UNSPECIFIED = 0; - SUBSTRATE_SORT_ORDER_ASCENDING = 1; - SUBSTRATE_SORT_ORDER_DESCENDING = 2; -} - -// SubstrateActorSortField is the column ListSubstrateActors orders by. -// -// Every order ends in the actor id, which is unique — so a page token, which is -// the sort key of the last row already sent, always identifies exactly one row. -// A key that could tie would skip or repeat rows at a page boundary. -enum SubstrateActorSortField { - // Unspecified groups by status and orders by id within each group. That is the - // order the inventory is most usefully read in, and it is stable: ate-api - // returns actors in whatever order it holds them, so an unsorted list puts a - // different actor on every page each time it is asked. - SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED = 0; - SUBSTRATE_ACTOR_SORT_FIELD_STATUS = 1; - SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID = 2; - SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE = 3; - SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD = 4; -} - -// SubstrateWorkerSortField is the column ListSubstrateWorkers orders by. -// Every order ends in the worker pod, which is unique within its namespace. -enum SubstrateWorkerSortField { - // Unspecified groups by pool and orders by pod within each group. - SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED = 0; - SUBSTRATE_WORKER_SORT_FIELD_POOL = 1; - SUBSTRATE_WORKER_SORT_FIELD_POD = 2; - SUBSTRATE_WORKER_SORT_FIELD_ACTOR = 3; -} - -message ListSubstrateActorsRequest { - string namespace = 1; - - // Filter is matched case-insensitively as a substring against the actor's id, - // status, actor template and worker pod — the fields a row displays. Empty - // matches everything. - string filter = 2; - - PageRequest page = 3; - - // Sorting is server-side because the rows are paged: ordering a page that has - // already been fetched reorders a hundred rows out of hundreds of thousands, - // which looks like sorting and is not. - SubstrateActorSortField sort_field = 4; - SubstrateSortOrder sort_order = 5; -} - -message ListSubstrateActorsResponse { - repeated SubstrateActor actors = 1; - PageResponse page = 2; - - // TotalSize is how many actors match the filter across every page, so a - // caller can say "20 of 4,312" rather than implying the page is the whole - // result. - int32 total_size = 3; - - // The order actually applied, so a caller can say how the rows are sorted - // rather than assuming its request was honoured. An unspecified field and an - // unspecified order both resolve to a concrete value here. - SubstrateActorSortField applied_sort_field = 4; - SubstrateSortOrder applied_sort_order = 5; - - // ComputedAt is when this answer was produced, which is not necessarily now. - // - // The substrate reads are memoised for a fraction of a second, because each one - // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - // them. A cache is also exactly how a polling control becomes a lie, so the age - // travels with the answer: a caller can say "as of 0.4s ago" rather than - // implying "now", and a reader can tell a stalled cluster from a stalled read. - google.protobuf.Timestamp computed_at = 6; -} - -message ListSubstrateWorkersRequest { - string namespace = 1; - - // Filter is matched case-insensitively as a substring against the worker's - // namespace, pod, pool and placed actor. - string filter = 2; - - PageRequest page = 3; - - SubstrateWorkerSortField sort_field = 4; - SubstrateSortOrder sort_order = 5; -} - -message ListSubstrateWorkersResponse { - repeated SubstrateWorker workers = 1; - PageResponse page = 2; - int32 total_size = 3; - - SubstrateWorkerSortField applied_sort_field = 4; - SubstrateSortOrder applied_sort_order = 5; - - // ComputedAt is when this answer was produced, which is not necessarily now. - // - // The substrate reads are memoised for a fraction of a second, because each one - // walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - // them. A cache is also exactly how a polling control becomes a lie, so the age - // travels with the answer: a caller can say "as of 0.4s ago" rather than - // implying "now", and a reader can tell a stalled cluster from a stalled read. - google.protobuf.Timestamp computed_at = 6; -} - message SubstrateWorkerPool { string namespace = 1; string name = 2; diff --git a/ui/playwright/helpers/mockCalls.ts b/ui/playwright/helpers/mockCalls.ts index df15a60c2..074692c74 100644 --- a/ui/playwright/helpers/mockCalls.ts +++ b/ui/playwright/helpers/mockCalls.ts @@ -44,9 +44,6 @@ export const rpc = { listPromptTemplates: "kagent.api.v1alpha1.PromptTemplateService/ListPromptTemplates", listNamespaces: "kagent.api.v1alpha1.SystemService/ListNamespaces", substrateStatus: "kagent.api.v1alpha1.SystemService/GetSubstrateStatus", - substrateSummary: "kagent.api.v1alpha1.SystemService/GetSubstrateSummary", - substrateActors: "kagent.api.v1alpha1.SystemService/ListSubstrateActors", - substrateWorkers: "kagent.api.v1alpha1.SystemService/ListSubstrateWorkers", listAgentTemplates: "kagent.api.v1alpha1.AgentTemplateService/ListAgentTemplates", listAgentInstances: "kagent.api.v1alpha1.AgentInstanceService/ListAgentInstances", getAgentInstance: "kagent.api.v1alpha1.AgentInstanceService/GetAgentInstance", diff --git a/ui/playwright/tests/substrate/substrate-polling.spec.ts b/ui/playwright/tests/substrate/substrate-polling.spec.ts index 040770c2b..67fb33f7e 100644 --- a/ui/playwright/tests/substrate/substrate-polling.spec.ts +++ b/ui/playwright/tests/substrate/substrate-polling.spec.ts @@ -39,22 +39,12 @@ import { operationCallCounts, rpc } from "../../helpers/mockCalls"; const SUBSTRATE = "/substrate"; -/** - * The inventory, which is three reads now rather than one. - * - * `GetSubstrateStatus` returned everything in a single message and stopped working — - * a cluster of 410,110 actors produces a response gRPC refuses to send. The page - * reads a summary for the counts and a page each of actors and workers, and polling - * drives all three: a timer that re-read the tiles while leaving the tables stale - * would show a moving count over rows that never change. - */ -const POLLED = rpc.substrateSummary; -const ALSO_POLLED = [rpc.substrateActors, rpc.substrateWorkers] as const; +const POLLED = rpc.substrateStatus; /** The scope control's own read, which must stay still while the inventory moves. */ const NOT_POLLED = rpc.listNamespaces; -const READS = [POLLED, ...ALSO_POLLED, NOT_POLLED] as const; +const READS = [POLLED, NOT_POLLED] as const; const readCounts = (page: import("@playwright/test").Page) => operationCallCounts(page, READS); diff --git a/ui/src/api/grpc/operations.ts b/ui/src/api/grpc/operations.ts index d2072d610..cbe17b1f7 100644 --- a/ui/src/api/grpc/operations.ts +++ b/ui/src/api/grpc/operations.ts @@ -60,12 +60,7 @@ import { SessionService, TaskStoreService, } from "@/generated/kagent/api/v1alpha1/sessions_pb"; -import { - SubstrateActorSortField as PbActorSortField, - SubstrateSortOrder as PbSortOrder, - SubstrateWorkerSortField as PbWorkerSortField, - SystemService, -} from "@/generated/kagent/api/v1alpha1/system_pb"; +import { SystemService } from "@/generated/kagent/api/v1alpha1/system_pb"; import { HarnessService } from "@/generated/kagent/api/v1alpha1/harnesses_pb"; import type { Harness as PbHarness } from "@/generated/kagent/api/v1alpha1/harnesses_pb"; import { AgentTemplateService } from "@/generated/kagent/api/v1alpha1/agent_templates_pb"; @@ -141,9 +136,7 @@ import type { ApiOperations, AgentRef, OperationCallOptions, - SubstrateActorSortField, - SubstrateSortOrder, - SubstrateWorkerSortField, + SubstratePageInput, } from "../operations"; import { createContextValues } from "@connectrpc/connect"; @@ -1510,51 +1503,47 @@ function toWorkerEntry(worker: PbSubstrateWorker): SubstrateWorkerEntry { }; } -/* - * The sort enums, mapped both ways. - * - * Written out rather than derived, and keyed by the generated enum so a member - * added to the proto fails `yarn typecheck` here rather than being sent as a zero. - * The inbound direction exists because the response reports the order the server - * *applied*: a table should say how its rows are sorted rather than showing the - * control's own state, which would still read "sorted by status" if the request had - * been ignored. - */ -const ACTOR_SORT_TO_PB: Record = { - default: PbActorSortField.UNSPECIFIED, - status: PbActorSortField.STATUS, - actorId: PbActorSortField.ACTOR_ID, - template: PbActorSortField.ACTOR_TEMPLATE, - workerPod: PbActorSortField.WORKER_POD, -}; - -const ACTOR_SORT_FROM_PB: Partial> = { - [PbActorSortField.UNSPECIFIED]: "default", - [PbActorSortField.STATUS]: "status", - [PbActorSortField.ACTOR_ID]: "actorId", - [PbActorSortField.ACTOR_TEMPLATE]: "template", - [PbActorSortField.WORKER_POD]: "workerPod", -}; - -const WORKER_SORT_TO_PB: Record = { - default: PbWorkerSortField.UNSPECIFIED, - pool: PbWorkerSortField.POOL, - pod: PbWorkerSortField.POD, - actor: PbWorkerSortField.ACTOR, -}; - -const WORKER_SORT_FROM_PB: Partial> = { - [PbWorkerSortField.UNSPECIFIED]: "default", - [PbWorkerSortField.POOL]: "pool", - [PbWorkerSortField.POD]: "pod", - [PbWorkerSortField.ACTOR]: "actor", -}; - -const sortOrderToPb = (order: SubstrateSortOrder | undefined): PbSortOrder => - order === "desc" ? PbSortOrder.DESCENDING : PbSortOrder.ASCENDING; +async function substrateStatus( + namespace: string | undefined, + operation: + | "substrate.status" + | "substrate.summary" + | "substrate.actors" + | "substrate.workers", + options: OperationCallOptions, +): Promise { + const response = await rpc("SystemService/GetSubstrateStatus", options.signal, () => + serviceClient(SystemService).getSubstrateStatus( + { namespace: namespace ?? "" }, + call(operation, options), + ), + ); + return toSubstrateStatus(response); +} -const sortOrderFromPb = (order: PbSortOrder): SubstrateSortOrder => - order === PbSortOrder.DESCENDING ? "desc" : "asc"; +function localPage( + rows: T[], + input: SubstratePageInput, + key: (row: T) => string, + text: (row: T) => string, +) { + const needle = input.filter?.trim().toLowerCase(); + const matching = needle + ? rows.filter((row) => text(row).toLowerCase().includes(needle)) + : rows; + matching.sort((left, right) => { + const compared = key(left).localeCompare(key(right)); + return input.sortOrder === "desc" ? -compared : compared; + }); + const start = Number.parseInt(input.pageToken ?? "0", 10) || 0; + const limit = input.limit || 50; + const end = Math.min(start + limit, matching.length); + return { + rows: matching.slice(start, end), + nextPageToken: end < matching.length ? String(end) : undefined, + totalSize: matching.length, + }; +} const cluster: Pick< ApiOperations, @@ -1575,84 +1564,95 @@ const cluster: Pick< }, "substrate.status": async (input, options) => { - const response = await rpc("SystemService/GetSubstrateStatus", options.signal, () => - serviceClient(SystemService).getSubstrateStatus( - { namespace: input.namespace ?? "" }, - call("substrate.status", options), - ), - ); - return toSubstrateStatus(response); + return substrateStatus(input.namespace, "substrate.status", options); }, "substrate.summary": async (input, options) => { - const response = await rpc("SystemService/GetSubstrateSummary", options.signal, () => - serviceClient(SystemService).getSubstrateSummary( - { namespace: input.namespace ?? "" }, - call("substrate.summary", options), - ), - ); + const response = await substrateStatus(input.namespace, "substrate.summary", options); + const actorStatusCounts = new Map(); + for (const actor of response.actors) { + actorStatusCounts.set(actor.status, (actorStatusCounts.get(actor.status) ?? 0) + 1); + } return { enabled: response.enabled, - ateApiError: orUndefined(response.ateApiError), - workerPools: list(response.workerPools).map(toWorkerPoolEntry), - actorTemplates: list(response.actorTemplates).map(toActorTemplateEntry), - actorCount: response.actorCount, - workerCount: response.workerCount, - runningActorCount: response.runningActorCount, - busyWorkerCount: response.busyWorkerCount, - actorStatusCounts: list(response.actorStatusCounts).map((entry) => ({ - status: entry.status, - count: entry.count, - })), - computedAt: orUndefined(isoFrom(response.computedAt)), + ateApiError: response.ateApiError, + workerPools: response.workerPools, + actorTemplates: response.actorTemplates, + actorCount: response.actors.length, + workerCount: response.workers.length, + runningActorCount: response.actors.filter( + (actor) => actor.status.toLowerCase() === "running", + ).length, + busyWorkerCount: response.workers.filter((worker) => Boolean(worker.actorId)).length, + actorStatusCounts: [...actorStatusCounts].map(([status, count]) => ({ status, count })), }; }, "substrate.actors": async (input, options) => { - const response = await rpc("SystemService/ListSubstrateActors", options.signal, () => - serviceClient(SystemService).listSubstrateActors( - { - namespace: input.namespace ?? "", - filter: input.filter ?? "", - page: { limit: input.limit ?? 0, pageToken: input.pageToken ?? "" }, - sortField: ACTOR_SORT_TO_PB[input.sortField ?? "default"], - sortOrder: sortOrderToPb(input.sortOrder), - }, - call("substrate.actors", options), - ), + const response = await substrateStatus(input.namespace, "substrate.actors", options); + const sortField = input.sortField ?? "default"; + const page = localPage( + response.actors, + input, + (actor) => { + if (sortField === "actorId") return actor.actorId; + if (sortField === "template") { + return `${actor.actorTemplateNamespace ?? ""}/${actor.actorTemplateName ?? ""}\0${actor.actorId}`; + } + if (sortField === "workerPod") { + return `${actor.ateomPodNamespace ?? ""}/${actor.ateomPodName ?? ""}\0${actor.actorId}`; + } + return `${actor.status}\0${actor.actorId}`; + }, + (actor) => + [ + actor.actorId, + actor.status, + actor.actorTemplateNamespace, + actor.actorTemplateName, + actor.ateomPodNamespace, + actor.ateomPodName, + actor.ateomPodIp, + ].join(" "), ); return { - actors: list(response.actors).map(toActorEntry), - // Absent rather than empty, so "is there more" is a question about presence - // and a caller cannot accidentally re-request page one with `""`. - nextPageToken: orUndefined(response.page?.nextPageToken), - totalSize: response.totalSize, - appliedSortField: ACTOR_SORT_FROM_PB[response.appliedSortField] ?? "default", - appliedSortOrder: sortOrderFromPb(response.appliedSortOrder), - computedAt: orUndefined(isoFrom(response.computedAt)), + actors: page.rows, + nextPageToken: page.nextPageToken, + totalSize: page.totalSize, + appliedSortField: sortField, + appliedSortOrder: input.sortOrder ?? "asc", }; }, "substrate.workers": async (input, options) => { - const response = await rpc("SystemService/ListSubstrateWorkers", options.signal, () => - serviceClient(SystemService).listSubstrateWorkers( - { - namespace: input.namespace ?? "", - filter: input.filter ?? "", - page: { limit: input.limit ?? 0, pageToken: input.pageToken ?? "" }, - sortField: WORKER_SORT_TO_PB[input.sortField ?? "default"], - sortOrder: sortOrderToPb(input.sortOrder), - }, - call("substrate.workers", options), - ), + const response = await substrateStatus(input.namespace, "substrate.workers", options); + const sortField = input.sortField ?? "default"; + const page = localPage( + response.workers, + input, + (worker) => { + const pod = `${worker.workerNamespace}/${worker.workerPod}`; + if (sortField === "pod") return pod; + if (sortField === "actor") return `${worker.actorId || "\uffff"}\0${pod}`; + return `${worker.workerPool}\0${pod}`; + }, + (worker) => + [ + worker.workerNamespace, + worker.workerPool, + worker.workerPod, + worker.actorNamespace, + worker.actorTemplate, + worker.actorId, + worker.ip, + ].join(" "), ); return { - workers: list(response.workers).map(toWorkerEntry), - nextPageToken: orUndefined(response.page?.nextPageToken), - totalSize: response.totalSize, - appliedSortField: WORKER_SORT_FROM_PB[response.appliedSortField] ?? "default", - appliedSortOrder: sortOrderFromPb(response.appliedSortOrder), - computedAt: orUndefined(isoFrom(response.computedAt)), + workers: page.rows, + nextPageToken: page.nextPageToken, + totalSize: page.totalSize, + appliedSortField: sortField, + appliedSortOrder: input.sortOrder ?? "asc", }; }, }; diff --git a/ui/src/generated/kagent/api/v1alpha1/agent_instances_pb.ts b/ui/src/generated/kagent/api/v1alpha1/agent_instances_pb.ts index 554788de0..5148b2770 100644 --- a/ui/src/generated/kagent/api/v1alpha1/agent_instances_pb.ts +++ b/ui/src/generated/kagent/api/v1alpha1/agent_instances_pb.ts @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file kagent/api/v1alpha1/agent_instances.proto. */ export const file_kagent_api_v1alpha1_agent_instances: GenFile = /*@__PURE__*/ - fileDesc("CilrYWdlbnQvYXBpL3YxYWxwaGExL2FnZW50X2luc3RhbmNlcy5wcm90bxITa2FnZW50LmFwaS52MWFscGhhMSIqCgdGYWlsdXJlEg4KBnJlYXNvbhgBIAEoCRIPCgdtZXNzYWdlGAIgASgJIu4ECg1BZ2VudEluc3RhbmNlEgoKAmlkGAEgASgJEhEKCW5hbWVzcGFjZRgCIAEoCRIPCgdjcmVhdG9yGAMgASgJEjcKB2hhcm5lc3MYBCABKAsyJi5rYWdlbnQuYXBpLnYxYWxwaGExLlJlc291cmNlUmVmZXJlbmNlEj4KDmFnZW50X3RlbXBsYXRlGAUgASgLMiYua2FnZW50LmFwaS52MWFscGhhMS5SZXNvdXJjZVJlZmVyZW5jZRIZChFwcmVwYXJlZF9yZXZpc2lvbhgGIAEoCRIVCg1hMmFfYXV0aG9yaXR5GAcgASgJEjYKBXN0YXRlGAggASgOMicua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlU3RhdGUSPgoJb3BlcmF0aW9uGAkgASgOMisua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlT3BlcmF0aW9uEi0KB2ZhaWx1cmUYCiABKAsyHC5rYWdlbnQuYXBpLnYxYWxwaGExLkZhaWx1cmUSLgoKY3JlYXRlZF9hdBgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKdXBkYXRlZF9hdBgMIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASPgoGbGFiZWxzGA0gAygLMi4ua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlLkxhYmVsc0VudHJ5EgwKBG5hbWUYDiABKAkaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKhAQoaQ3JlYXRlQWdlbnRJbnN0YW5jZVJlcXVlc3QSGgoJbmFtZXNwYWNlGAEgASgJQge6SARyAhABEhgKB2hhcm5lc3MYAiABKAlCB7pIBHICEAESHwoOYWdlbnRfdGVtcGxhdGUYAyABKAlCB7pIBHICEAESHgoKcmVxdWVzdF9pZBgEIAEoCUIKukgHcgUQARiAARIMCgRuYW1lGAUgASgJIlkKG0NyZWF0ZUFnZW50SW5zdGFuY2VSZXNwb25zZRI6Cg5hZ2VudF9pbnN0YW5jZRgBIAEoCzIiLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZSJZChdHZXRBZ2VudEluc3RhbmNlUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAEiVgoYR2V0QWdlbnRJbnN0YW5jZVJlc3BvbnNlEjoKDmFnZW50X2luc3RhbmNlGAEgASgLMiIua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlIrECChlMaXN0QWdlbnRJbnN0YW5jZXNSZXF1ZXN0EhoKCW5hbWVzcGFjZRgBIAEoCUIHukgEcgIQARJVCgxtYXRjaF9sYWJlbHMYAiADKAsyPy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RBZ2VudEluc3RhbmNlc1JlcXVlc3QuTWF0Y2hMYWJlbHNFbnRyeRIUCgxhbGxfY3JlYXRvcnMYAyABKAgSLgoEcGFnZRgEIAEoCzIgLmthZ2VudC5hcGkudjFhbHBoYTEuUGFnZVJlcXVlc3QSFgoOYWdlbnRfdGVtcGxhdGUYBSABKAkSDwoHaGFybmVzcxgGIAEoCRoyChBNYXRjaExhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiigEKGkxpc3RBZ2VudEluc3RhbmNlc1Jlc3BvbnNlEjsKD2FnZW50X2luc3RhbmNlcxgBIAMoCzIiLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZRIvCgRwYWdlGAIgASgLMiEua2FnZW50LmFwaS52MWFscGhhMS5QYWdlUmVzcG9uc2UiWAoaUmVuYW1lQWdlbnRJbnN0YW5jZVJlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJEhkKEWFnZW50X2luc3RhbmNlX2lkGAIgASgJEgwKBG5hbWUYAyABKAkiWQobUmVuYW1lQWdlbnRJbnN0YW5jZVJlc3BvbnNlEjoKDmFnZW50X2luc3RhbmNlGAEgASgLMiIua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlIl0KG1N1c3BlbmRBZ2VudEluc3RhbmNlUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAEiWgocU3VzcGVuZEFnZW50SW5zdGFuY2VSZXNwb25zZRI6Cg5hZ2VudF9pbnN0YW5jZRgBIAEoCzIiLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZSJcChpSZXN1bWVBZ2VudEluc3RhbmNlUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAEiWQobUmVzdW1lQWdlbnRJbnN0YW5jZVJlc3BvbnNlEjoKDmFnZW50X2luc3RhbmNlGAEgASgLMiIua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlIlwKGkRlbGV0ZUFnZW50SW5zdGFuY2VSZXF1ZXN0EhoKCW5hbWVzcGFjZRgBIAEoCUIHukgEcgIQARIiChFhZ2VudF9pbnN0YW5jZV9pZBgCIAEoCUIHukgEcgIQASJZChtEZWxldGVBZ2VudEluc3RhbmNlUmVzcG9uc2USOgoOYWdlbnRfaW5zdGFuY2UYASABKAsyIi5rYWdlbnQuYXBpLnYxYWxwaGExLkFnZW50SW5zdGFuY2Ui1gEKEkFnZW50SW5zdGFuY2VTaGFyZRIKCgJpZBgBIAEoCRIRCgluYW1lc3BhY2UYAiABKAkSGQoRYWdlbnRfaW5zdGFuY2VfaWQYAyABKAkSDwoHY3JlYXRvchgEIAEoCRJFCgpwZXJtaXNzaW9uGAUgASgOMjEua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlU2hhcmVQZXJtaXNzaW9uEi4KCmNyZWF0ZWRfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIrQBCh9DcmVhdGVBZ2VudEluc3RhbmNlU2hhcmVSZXF1ZXN0EhoKCW5hbWVzcGFjZRgBIAEoCUIHukgEcgIQARIiChFhZ2VudF9pbnN0YW5jZV9pZBgCIAEoCUIHukgEcgIQARJRCgpwZXJtaXNzaW9uGAMgASgOMjEua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlU2hhcmVQZXJtaXNzaW9uQgq6SAeCAQQQASAAImkKIENyZWF0ZUFnZW50SW5zdGFuY2VTaGFyZVJlc3BvbnNlEjYKBXNoYXJlGAEgASgLMicua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlU2hhcmUSDQoFdG9rZW4YAiABKAkikAEKHkxpc3RBZ2VudEluc3RhbmNlU2hhcmVzUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAESLgoEcGFnZRgDIAEoCzIgLmthZ2VudC5hcGkudjFhbHBoYTEuUGFnZVJlcXVlc3QiiwEKH0xpc3RBZ2VudEluc3RhbmNlU2hhcmVzUmVzcG9uc2USNwoGc2hhcmVzGAEgAygLMicua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlU2hhcmUSLwoEcGFnZRgCIAEoCzIhLmthZ2VudC5hcGkudjFhbHBoYTEuUGFnZVJlc3BvbnNlIlgKH1Jldm9rZUFnZW50SW5zdGFuY2VTaGFyZVJlcXVlc3QSGgoJbmFtZXNwYWNlGAEgASgJQge6SARyAhABEhkKCHNoYXJlX2lkGAIgASgJQge6SARyAhABIiIKIFJldm9rZUFnZW50SW5zdGFuY2VTaGFyZVJlc3BvbnNlKocCChJBZ2VudEluc3RhbmNlU3RhdGUSJAogQUdFTlRfSU5TVEFOQ0VfU1RBVEVfVU5TUEVDSUZJRUQQABIhCh1BR0VOVF9JTlNUQU5DRV9TVEFURV9DUkVBVElORxABEh4KGkFHRU5UX0lOU1RBTkNFX1NUQVRFX1JFQURZEAISIgoeQUdFTlRfSU5TVEFOQ0VfU1RBVEVfU1VTUEVOREVEEAMSHwobQUdFTlRfSU5TVEFOQ0VfU1RBVEVfRkFJTEVEEAQSIQodQUdFTlRfSU5TVEFOQ0VfU1RBVEVfREVMRVRJTkcQBRIgChxBR0VOVF9JTlNUQU5DRV9TVEFURV9ERUxFVEVEEAYq1wEKFkFnZW50SW5zdGFuY2VPcGVyYXRpb24SKAokQUdFTlRfSU5TVEFOQ0VfT1BFUkFUSU9OX1VOU1BFQ0lGSUVEEAASIwofQUdFTlRfSU5TVEFOQ0VfT1BFUkFUSU9OX0NSRUFURRABEiQKIEFHRU5UX0lOU1RBTkNFX09QRVJBVElPTl9TVVNQRU5EEAISIwofQUdFTlRfSU5TVEFOQ0VfT1BFUkFUSU9OX1JFU1VNRRADEiMKH0FHRU5UX0lOU1RBTkNFX09QRVJBVElPTl9ERUxFVEUQBCquAQocQWdlbnRJbnN0YW5jZVNoYXJlUGVybWlzc2lvbhIvCitBR0VOVF9JTlNUQU5DRV9TSEFSRV9QRVJNSVNTSU9OX1VOU1BFQ0lGSUVEEAASLQopQUdFTlRfSU5TVEFOQ0VfU0hBUkVfUEVSTUlTU0lPTl9SRUFEX09OTFkQARIuCipBR0VOVF9JTlNUQU5DRV9TSEFSRV9QRVJNSVNTSU9OX1JFQURfV1JJVEUQAjL+CQoUQWdlbnRJbnN0YW5jZVNlcnZpY2USeAoTQ3JlYXRlQWdlbnRJbnN0YW5jZRIvLmthZ2VudC5hcGkudjFhbHBoYTEuQ3JlYXRlQWdlbnRJbnN0YW5jZVJlcXVlc3QaMC5rYWdlbnQuYXBpLnYxYWxwaGExLkNyZWF0ZUFnZW50SW5zdGFuY2VSZXNwb25zZRJvChBHZXRBZ2VudEluc3RhbmNlEiwua2FnZW50LmFwaS52MWFscGhhMS5HZXRBZ2VudEluc3RhbmNlUmVxdWVzdBotLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0QWdlbnRJbnN0YW5jZVJlc3BvbnNlEnUKEkxpc3RBZ2VudEluc3RhbmNlcxIuLmthZ2VudC5hcGkudjFhbHBoYTEuTGlzdEFnZW50SW5zdGFuY2VzUmVxdWVzdBovLmthZ2VudC5hcGkudjFhbHBoYTEuTGlzdEFnZW50SW5zdGFuY2VzUmVzcG9uc2USeAoTUmVuYW1lQWdlbnRJbnN0YW5jZRIvLmthZ2VudC5hcGkudjFhbHBoYTEuUmVuYW1lQWdlbnRJbnN0YW5jZVJlcXVlc3QaMC5rYWdlbnQuYXBpLnYxYWxwaGExLlJlbmFtZUFnZW50SW5zdGFuY2VSZXNwb25zZRJ7ChRTdXNwZW5kQWdlbnRJbnN0YW5jZRIwLmthZ2VudC5hcGkudjFhbHBoYTEuU3VzcGVuZEFnZW50SW5zdGFuY2VSZXF1ZXN0GjEua2FnZW50LmFwaS52MWFscGhhMS5TdXNwZW5kQWdlbnRJbnN0YW5jZVJlc3BvbnNlEngKE1Jlc3VtZUFnZW50SW5zdGFuY2USLy5rYWdlbnQuYXBpLnYxYWxwaGExLlJlc3VtZUFnZW50SW5zdGFuY2VSZXF1ZXN0GjAua2FnZW50LmFwaS52MWFscGhhMS5SZXN1bWVBZ2VudEluc3RhbmNlUmVzcG9uc2USeAoTRGVsZXRlQWdlbnRJbnN0YW5jZRIvLmthZ2VudC5hcGkudjFhbHBoYTEuRGVsZXRlQWdlbnRJbnN0YW5jZVJlcXVlc3QaMC5rYWdlbnQuYXBpLnYxYWxwaGExLkRlbGV0ZUFnZW50SW5zdGFuY2VSZXNwb25zZRKHAQoYQ3JlYXRlQWdlbnRJbnN0YW5jZVNoYXJlEjQua2FnZW50LmFwaS52MWFscGhhMS5DcmVhdGVBZ2VudEluc3RhbmNlU2hhcmVSZXF1ZXN0GjUua2FnZW50LmFwaS52MWFscGhhMS5DcmVhdGVBZ2VudEluc3RhbmNlU2hhcmVSZXNwb25zZRKEAQoXTGlzdEFnZW50SW5zdGFuY2VTaGFyZXMSMy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RBZ2VudEluc3RhbmNlU2hhcmVzUmVxdWVzdBo0LmthZ2VudC5hcGkudjFhbHBoYTEuTGlzdEFnZW50SW5zdGFuY2VTaGFyZXNSZXNwb25zZRKHAQoYUmV2b2tlQWdlbnRJbnN0YW5jZVNoYXJlEjQua2FnZW50LmFwaS52MWFscGhhMS5SZXZva2VBZ2VudEluc3RhbmNlU2hhcmVSZXF1ZXN0GjUua2FnZW50LmFwaS52MWFscGhhMS5SZXZva2VBZ2VudEluc3RhbmNlU2hhcmVSZXNwb25zZUJJWkdnaXRodWIuY29tL2thZ2VudC1kZXYva2FnZW50L2dvL2FwaS9nZW4va2FnZW50L2FwaS92MWFscGhhMTthcGl2MWFscGhhMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_timestamp, file_kagent_api_v1alpha1_common]); + fileDesc("CilrYWdlbnQvYXBpL3YxYWxwaGExL2FnZW50X2luc3RhbmNlcy5wcm90bxITa2FnZW50LmFwaS52MWFscGhhMSIqCgdGYWlsdXJlEg4KBnJlYXNvbhgBIAEoCRIPCgdtZXNzYWdlGAIgASgJIu4ECg1BZ2VudEluc3RhbmNlEgoKAmlkGAEgASgJEhEKCW5hbWVzcGFjZRgCIAEoCRIPCgdjcmVhdG9yGAMgASgJEjcKB2hhcm5lc3MYBCABKAsyJi5rYWdlbnQuYXBpLnYxYWxwaGExLlJlc291cmNlUmVmZXJlbmNlEj4KDmFnZW50X3RlbXBsYXRlGAUgASgLMiYua2FnZW50LmFwaS52MWFscGhhMS5SZXNvdXJjZVJlZmVyZW5jZRIZChFwcmVwYXJlZF9yZXZpc2lvbhgGIAEoCRIVCg1hMmFfYXV0aG9yaXR5GAcgASgJEjYKBXN0YXRlGAggASgOMicua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlU3RhdGUSPgoJb3BlcmF0aW9uGAkgASgOMisua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlT3BlcmF0aW9uEi0KB2ZhaWx1cmUYCiABKAsyHC5rYWdlbnQuYXBpLnYxYWxwaGExLkZhaWx1cmUSLgoKY3JlYXRlZF9hdBgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLgoKdXBkYXRlZF9hdBgMIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASPgoGbGFiZWxzGA0gAygLMi4ua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlLkxhYmVsc0VudHJ5EgwKBG5hbWUYDiABKAkaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLgAQoaQ3JlYXRlQWdlbnRJbnN0YW5jZVJlcXVlc3QSGgoJbmFtZXNwYWNlGAEgASgJQge6SARyAhABEhgKB2hhcm5lc3MYAiABKAlCB7pIBHICEAESHwoOYWdlbnRfdGVtcGxhdGUYAyABKAlCB7pIBHICEAESHgoKcmVxdWVzdF9pZBgEIAEoCUIKukgHcgUQARiAARJLCgRuYW1lGAUgASgJQj26SDpyOBjIATIzXig/OiR8W15ccHtafVxwe0NjfV0oPzpbXlxwe0NjfV0qW15ccHtafVxwe0NjfV0pPykkIlkKG0NyZWF0ZUFnZW50SW5zdGFuY2VSZXNwb25zZRI6Cg5hZ2VudF9pbnN0YW5jZRgBIAEoCzIiLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZSJZChdHZXRBZ2VudEluc3RhbmNlUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAEiVgoYR2V0QWdlbnRJbnN0YW5jZVJlc3BvbnNlEjoKDmFnZW50X2luc3RhbmNlGAEgASgLMiIua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlIuMDChlMaXN0QWdlbnRJbnN0YW5jZXNSZXF1ZXN0EhoKCW5hbWVzcGFjZRgBIAEoCUIHukgEcgIQARJVCgxtYXRjaF9sYWJlbHMYAiADKAsyPy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RBZ2VudEluc3RhbmNlc1JlcXVlc3QuTWF0Y2hMYWJlbHNFbnRyeRIUCgxhbGxfY3JlYXRvcnMYAyABKAgSLgoEcGFnZRgEIAEoCzIgLmthZ2VudC5hcGkudjFhbHBoYTEuUGFnZVJlcXVlc3QSbwoOYWdlbnRfdGVtcGxhdGUYBSABKAlCV7pIVHJSGP0BMk1eKD86JHxbYS16MC05XSg/OlstYS16MC05XSpbYS16MC05XSk/KD86XC5bYS16MC05XSg/OlstYS16MC05XSpbYS16MC05XSk/KSopJBJoCgdoYXJuZXNzGAYgASgJQle6SFRyUhj9ATJNXig/OiR8W2EtejAtOV0oPzpbLWEtejAtOV0qW2EtejAtOV0pPyg/OlwuW2EtejAtOV0oPzpbLWEtejAtOV0qW2EtejAtOV0pPykqKSQaMgoQTWF0Y2hMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIooBChpMaXN0QWdlbnRJbnN0YW5jZXNSZXNwb25zZRI7Cg9hZ2VudF9pbnN0YW5jZXMYASADKAsyIi5rYWdlbnQuYXBpLnYxYWxwaGExLkFnZW50SW5zdGFuY2USLwoEcGFnZRgCIAEoCzIhLmthZ2VudC5hcGkudjFhbHBoYTEuUGFnZVJlc3BvbnNlIs8BChpSZW5hbWVBZ2VudEluc3RhbmNlUmVxdWVzdBI/CgluYW1lc3BhY2UYASABKAlCLLpIKXInEAEYPzIhXlthLXowLTldKD86Wy1hLXowLTldKlthLXowLTldKT8kEiMKEWFnZW50X2luc3RhbmNlX2lkGAIgASgJQgi6SAVyA7ABARJLCgRuYW1lGAMgASgJQj26SDpyOBjIATIzXig/OiR8W15ccHtafVxwe0NjfV0oPzpbXlxwe0NjfV0qW15ccHtafVxwe0NjfV0pPykkIlkKG1JlbmFtZUFnZW50SW5zdGFuY2VSZXNwb25zZRI6Cg5hZ2VudF9pbnN0YW5jZRgBIAEoCzIiLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZSJdChtTdXNwZW5kQWdlbnRJbnN0YW5jZVJlcXVlc3QSGgoJbmFtZXNwYWNlGAEgASgJQge6SARyAhABEiIKEWFnZW50X2luc3RhbmNlX2lkGAIgASgJQge6SARyAhABIloKHFN1c3BlbmRBZ2VudEluc3RhbmNlUmVzcG9uc2USOgoOYWdlbnRfaW5zdGFuY2UYASABKAsyIi5rYWdlbnQuYXBpLnYxYWxwaGExLkFnZW50SW5zdGFuY2UiXAoaUmVzdW1lQWdlbnRJbnN0YW5jZVJlcXVlc3QSGgoJbmFtZXNwYWNlGAEgASgJQge6SARyAhABEiIKEWFnZW50X2luc3RhbmNlX2lkGAIgASgJQge6SARyAhABIlkKG1Jlc3VtZUFnZW50SW5zdGFuY2VSZXNwb25zZRI6Cg5hZ2VudF9pbnN0YW5jZRgBIAEoCzIiLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZSJcChpEZWxldGVBZ2VudEluc3RhbmNlUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAEiWQobRGVsZXRlQWdlbnRJbnN0YW5jZVJlc3BvbnNlEjoKDmFnZW50X2luc3RhbmNlGAEgASgLMiIua2FnZW50LmFwaS52MWFscGhhMS5BZ2VudEluc3RhbmNlItYBChJBZ2VudEluc3RhbmNlU2hhcmUSCgoCaWQYASABKAkSEQoJbmFtZXNwYWNlGAIgASgJEhkKEWFnZW50X2luc3RhbmNlX2lkGAMgASgJEg8KB2NyZWF0b3IYBCABKAkSRQoKcGVybWlzc2lvbhgFIAEoDjIxLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZVNoYXJlUGVybWlzc2lvbhIuCgpjcmVhdGVkX2F0GAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCK0AQofQ3JlYXRlQWdlbnRJbnN0YW5jZVNoYXJlUmVxdWVzdBIaCgluYW1lc3BhY2UYASABKAlCB7pIBHICEAESIgoRYWdlbnRfaW5zdGFuY2VfaWQYAiABKAlCB7pIBHICEAESUQoKcGVybWlzc2lvbhgDIAEoDjIxLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZVNoYXJlUGVybWlzc2lvbkIKukgHggEEEAEgACJpCiBDcmVhdGVBZ2VudEluc3RhbmNlU2hhcmVSZXNwb25zZRI2CgVzaGFyZRgBIAEoCzInLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZVNoYXJlEg0KBXRva2VuGAIgASgJIpABCh5MaXN0QWdlbnRJbnN0YW5jZVNoYXJlc1JlcXVlc3QSGgoJbmFtZXNwYWNlGAEgASgJQge6SARyAhABEiIKEWFnZW50X2luc3RhbmNlX2lkGAIgASgJQge6SARyAhABEi4KBHBhZ2UYAyABKAsyIC5rYWdlbnQuYXBpLnYxYWxwaGExLlBhZ2VSZXF1ZXN0IosBCh9MaXN0QWdlbnRJbnN0YW5jZVNoYXJlc1Jlc3BvbnNlEjcKBnNoYXJlcxgBIAMoCzInLmthZ2VudC5hcGkudjFhbHBoYTEuQWdlbnRJbnN0YW5jZVNoYXJlEi8KBHBhZ2UYAiABKAsyIS5rYWdlbnQuYXBpLnYxYWxwaGExLlBhZ2VSZXNwb25zZSJYCh9SZXZva2VBZ2VudEluc3RhbmNlU2hhcmVSZXF1ZXN0EhoKCW5hbWVzcGFjZRgBIAEoCUIHukgEcgIQARIZCghzaGFyZV9pZBgCIAEoCUIHukgEcgIQASIiCiBSZXZva2VBZ2VudEluc3RhbmNlU2hhcmVSZXNwb25zZSqHAgoSQWdlbnRJbnN0YW5jZVN0YXRlEiQKIEFHRU5UX0lOU1RBTkNFX1NUQVRFX1VOU1BFQ0lGSUVEEAASIQodQUdFTlRfSU5TVEFOQ0VfU1RBVEVfQ1JFQVRJTkcQARIeChpBR0VOVF9JTlNUQU5DRV9TVEFURV9SRUFEWRACEiIKHkFHRU5UX0lOU1RBTkNFX1NUQVRFX1NVU1BFTkRFRBADEh8KG0FHRU5UX0lOU1RBTkNFX1NUQVRFX0ZBSUxFRBAEEiEKHUFHRU5UX0lOU1RBTkNFX1NUQVRFX0RFTEVUSU5HEAUSIAocQUdFTlRfSU5TVEFOQ0VfU1RBVEVfREVMRVRFRBAGKtcBChZBZ2VudEluc3RhbmNlT3BlcmF0aW9uEigKJEFHRU5UX0lOU1RBTkNFX09QRVJBVElPTl9VTlNQRUNJRklFRBAAEiMKH0FHRU5UX0lOU1RBTkNFX09QRVJBVElPTl9DUkVBVEUQARIkCiBBR0VOVF9JTlNUQU5DRV9PUEVSQVRJT05fU1VTUEVORBACEiMKH0FHRU5UX0lOU1RBTkNFX09QRVJBVElPTl9SRVNVTUUQAxIjCh9BR0VOVF9JTlNUQU5DRV9PUEVSQVRJT05fREVMRVRFEAQqrgEKHEFnZW50SW5zdGFuY2VTaGFyZVBlcm1pc3Npb24SLworQUdFTlRfSU5TVEFOQ0VfU0hBUkVfUEVSTUlTU0lPTl9VTlNQRUNJRklFRBAAEi0KKUFHRU5UX0lOU1RBTkNFX1NIQVJFX1BFUk1JU1NJT05fUkVBRF9PTkxZEAESLgoqQUdFTlRfSU5TVEFOQ0VfU0hBUkVfUEVSTUlTU0lPTl9SRUFEX1dSSVRFEAIy/gkKFEFnZW50SW5zdGFuY2VTZXJ2aWNlEngKE0NyZWF0ZUFnZW50SW5zdGFuY2USLy5rYWdlbnQuYXBpLnYxYWxwaGExLkNyZWF0ZUFnZW50SW5zdGFuY2VSZXF1ZXN0GjAua2FnZW50LmFwaS52MWFscGhhMS5DcmVhdGVBZ2VudEluc3RhbmNlUmVzcG9uc2USbwoQR2V0QWdlbnRJbnN0YW5jZRIsLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0QWdlbnRJbnN0YW5jZVJlcXVlc3QaLS5rYWdlbnQuYXBpLnYxYWxwaGExLkdldEFnZW50SW5zdGFuY2VSZXNwb25zZRJ1ChJMaXN0QWdlbnRJbnN0YW5jZXMSLi5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RBZ2VudEluc3RhbmNlc1JlcXVlc3QaLy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RBZ2VudEluc3RhbmNlc1Jlc3BvbnNlEngKE1JlbmFtZUFnZW50SW5zdGFuY2USLy5rYWdlbnQuYXBpLnYxYWxwaGExLlJlbmFtZUFnZW50SW5zdGFuY2VSZXF1ZXN0GjAua2FnZW50LmFwaS52MWFscGhhMS5SZW5hbWVBZ2VudEluc3RhbmNlUmVzcG9uc2USewoUU3VzcGVuZEFnZW50SW5zdGFuY2USMC5rYWdlbnQuYXBpLnYxYWxwaGExLlN1c3BlbmRBZ2VudEluc3RhbmNlUmVxdWVzdBoxLmthZ2VudC5hcGkudjFhbHBoYTEuU3VzcGVuZEFnZW50SW5zdGFuY2VSZXNwb25zZRJ4ChNSZXN1bWVBZ2VudEluc3RhbmNlEi8ua2FnZW50LmFwaS52MWFscGhhMS5SZXN1bWVBZ2VudEluc3RhbmNlUmVxdWVzdBowLmthZ2VudC5hcGkudjFhbHBoYTEuUmVzdW1lQWdlbnRJbnN0YW5jZVJlc3BvbnNlEngKE0RlbGV0ZUFnZW50SW5zdGFuY2USLy5rYWdlbnQuYXBpLnYxYWxwaGExLkRlbGV0ZUFnZW50SW5zdGFuY2VSZXF1ZXN0GjAua2FnZW50LmFwaS52MWFscGhhMS5EZWxldGVBZ2VudEluc3RhbmNlUmVzcG9uc2UShwEKGENyZWF0ZUFnZW50SW5zdGFuY2VTaGFyZRI0LmthZ2VudC5hcGkudjFhbHBoYTEuQ3JlYXRlQWdlbnRJbnN0YW5jZVNoYXJlUmVxdWVzdBo1LmthZ2VudC5hcGkudjFhbHBoYTEuQ3JlYXRlQWdlbnRJbnN0YW5jZVNoYXJlUmVzcG9uc2UShAEKF0xpc3RBZ2VudEluc3RhbmNlU2hhcmVzEjMua2FnZW50LmFwaS52MWFscGhhMS5MaXN0QWdlbnRJbnN0YW5jZVNoYXJlc1JlcXVlc3QaNC5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RBZ2VudEluc3RhbmNlU2hhcmVzUmVzcG9uc2UShwEKGFJldm9rZUFnZW50SW5zdGFuY2VTaGFyZRI0LmthZ2VudC5hcGkudjFhbHBoYTEuUmV2b2tlQWdlbnRJbnN0YW5jZVNoYXJlUmVxdWVzdBo1LmthZ2VudC5hcGkudjFhbHBoYTEuUmV2b2tlQWdlbnRJbnN0YW5jZVNoYXJlUmVzcG9uc2VCSVpHZ2l0aHViLmNvbS9rYWdlbnQtZGV2L2thZ2VudC9nby9hcGkvZ2VuL2thZ2VudC9hcGkvdjFhbHBoYTE7YXBpdjFhbHBoYTFiBnByb3RvMw", [file_buf_validate_validate, file_google_protobuf_timestamp, file_kagent_api_v1alpha1_common]); /** * @generated from message kagent.api.v1alpha1.Failure @@ -149,9 +149,7 @@ export type CreateAgentInstanceRequest = Message<"kagent.api.v1alpha1.CreateAgen requestId: string; /** - * Optional display name. Omit it to create an unnamed conversation. Unvalidated - * because empty is the ordinary case: a conversation is usually named later, or - * never. + * Optional display name. Empty means unnamed. * * @generated from field: string name = 5; */ diff --git a/ui/src/generated/kagent/api/v1alpha1/harnesses_pb.ts b/ui/src/generated/kagent/api/v1alpha1/harnesses_pb.ts index 0c9e5e98a..96e449c4b 100644 --- a/ui/src/generated/kagent/api/v1alpha1/harnesses_pb.ts +++ b/ui/src/generated/kagent/api/v1alpha1/harnesses_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file kagent/api/v1alpha1/harnesses.proto. */ export const file_kagent_api_v1alpha1_harnesses: GenFile = /*@__PURE__*/ - fileDesc("CiNrYWdlbnQvYXBpL3YxYWxwaGExL2hhcm5lc3Nlcy5wcm90bxITa2FnZW50LmFwaS52MWFscGhhMSKvAQoHSGFybmVzcxIzCgNyZWYYASABKAsyJi5rYWdlbnQuYXBpLnYxYWxwaGExLlJlc291cmNlUmVmZXJlbmNlEjcKCHJlc291cmNlGAIgASgLMiUua2FnZW50LmFwaS52MWFscGhhMS5TdHJ1Y3R1cmVkT2JqZWN0Eg8KB3J1bnRpbWUYAyABKAkSFgoOd29ya2xvYWRfaW1hZ2UYBCABKAkSDQoFcmVhZHkYBSABKAgiKQoUTGlzdEhhcm5lc3Nlc1JlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJIkgKFUxpc3RIYXJuZXNzZXNSZXNwb25zZRIvCgloYXJuZXNzZXMYASADKAsyHC5rYWdlbnQuYXBpLnYxYWxwaGExLkhhcm5lc3MiSAoRR2V0SGFybmVzc1JlcXVlc3QSMwoDcmVmGAEgASgLMiYua2FnZW50LmFwaS52MWFscGhhMS5SZXNvdXJjZVJlZmVyZW5jZSJDChJHZXRIYXJuZXNzUmVzcG9uc2USLQoHaGFybmVzcxgBIAEoCzIcLmthZ2VudC5hcGkudjFhbHBoYTEuSGFybmVzcyKEAQoUQ3JlYXRlSGFybmVzc1JlcXVlc3QSMwoDcmVmGAEgASgLMiYua2FnZW50LmFwaS52MWFscGhhMS5SZXNvdXJjZVJlZmVyZW5jZRI3CghyZXNvdXJjZRgCIAEoCzIlLmthZ2VudC5hcGkudjFhbHBoYTEuU3RydWN0dXJlZE9iamVjdCJGChVDcmVhdGVIYXJuZXNzUmVzcG9uc2USLQoHaGFybmVzcxgBIAEoCzIcLmthZ2VudC5hcGkudjFhbHBoYTEuSGFybmVzcyKEAQoUVXBkYXRlSGFybmVzc1JlcXVlc3QSMwoDcmVmGAEgASgLMiYua2FnZW50LmFwaS52MWFscGhhMS5SZXNvdXJjZVJlZmVyZW5jZRI3CghyZXNvdXJjZRgCIAEoCzIlLmthZ2VudC5hcGkudjFhbHBoYTEuU3RydWN0dXJlZE9iamVjdCJGChVVcGRhdGVIYXJuZXNzUmVzcG9uc2USLQoHaGFybmVzcxgBIAEoCzIcLmthZ2VudC5hcGkudjFhbHBoYTEuSGFybmVzcyJLChREZWxldGVIYXJuZXNzUmVxdWVzdBIzCgNyZWYYASABKAsyJi5rYWdlbnQuYXBpLnYxYWxwaGExLlJlc291cmNlUmVmZXJlbmNlIhcKFURlbGV0ZUhhcm5lc3NSZXNwb25zZTKPBAoOSGFybmVzc1NlcnZpY2USZgoNTGlzdEhhcm5lc3NlcxIpLmthZ2VudC5hcGkudjFhbHBoYTEuTGlzdEhhcm5lc3Nlc1JlcXVlc3QaKi5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RIYXJuZXNzZXNSZXNwb25zZRJdCgpHZXRIYXJuZXNzEiYua2FnZW50LmFwaS52MWFscGhhMS5HZXRIYXJuZXNzUmVxdWVzdBonLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0SGFybmVzc1Jlc3BvbnNlEmYKDUNyZWF0ZUhhcm5lc3MSKS5rYWdlbnQuYXBpLnYxYWxwaGExLkNyZWF0ZUhhcm5lc3NSZXF1ZXN0Gioua2FnZW50LmFwaS52MWFscGhhMS5DcmVhdGVIYXJuZXNzUmVzcG9uc2USZgoNVXBkYXRlSGFybmVzcxIpLmthZ2VudC5hcGkudjFhbHBoYTEuVXBkYXRlSGFybmVzc1JlcXVlc3QaKi5rYWdlbnQuYXBpLnYxYWxwaGExLlVwZGF0ZUhhcm5lc3NSZXNwb25zZRJmCg1EZWxldGVIYXJuZXNzEikua2FnZW50LmFwaS52MWFscGhhMS5EZWxldGVIYXJuZXNzUmVxdWVzdBoqLmthZ2VudC5hcGkudjFhbHBoYTEuRGVsZXRlSGFybmVzc1Jlc3BvbnNlQklaR2dpdGh1Yi5jb20va2FnZW50LWRldi9rYWdlbnQvZ28vYXBpL2dlbi9rYWdlbnQvYXBpL3YxYWxwaGExO2FwaXYxYWxwaGExYgZwcm90bzM", [file_kagent_api_v1alpha1_common]); + fileDesc("CiNrYWdlbnQvYXBpL3YxYWxwaGExL2hhcm5lc3Nlcy5wcm90bxITa2FnZW50LmFwaS52MWFscGhhMSKvAQoHSGFybmVzcxIzCgNyZWYYASABKAsyJi5rYWdlbnQuYXBpLnYxYWxwaGExLlJlc291cmNlUmVmZXJlbmNlEjcKCHJlc291cmNlGAIgASgLMiUua2FnZW50LmFwaS52MWFscGhhMS5TdHJ1Y3R1cmVkT2JqZWN0Eg8KB3J1bnRpbWUYAyABKAkSFgoOd29ya2xvYWRfaW1hZ2UYBCABKAkSDQoFcmVhZHkYBSABKAgiKQoUTGlzdEhhcm5lc3Nlc1JlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJIkgKFUxpc3RIYXJuZXNzZXNSZXNwb25zZRIvCgloYXJuZXNzZXMYASADKAsyHC5rYWdlbnQuYXBpLnYxYWxwaGExLkhhcm5lc3MihAEKFENyZWF0ZUhhcm5lc3NSZXF1ZXN0EjMKA3JlZhgBIAEoCzImLmthZ2VudC5hcGkudjFhbHBoYTEuUmVzb3VyY2VSZWZlcmVuY2USNwoIcmVzb3VyY2UYAiABKAsyJS5rYWdlbnQuYXBpLnYxYWxwaGExLlN0cnVjdHVyZWRPYmplY3QiRgoVQ3JlYXRlSGFybmVzc1Jlc3BvbnNlEi0KB2hhcm5lc3MYASABKAsyHC5rYWdlbnQuYXBpLnYxYWxwaGExLkhhcm5lc3MiSwoURGVsZXRlSGFybmVzc1JlcXVlc3QSMwoDcmVmGAEgASgLMiYua2FnZW50LmFwaS52MWFscGhhMS5SZXNvdXJjZVJlZmVyZW5jZSIXChVEZWxldGVIYXJuZXNzUmVzcG9uc2UyyAIKDkhhcm5lc3NTZXJ2aWNlEmYKDUxpc3RIYXJuZXNzZXMSKS5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RIYXJuZXNzZXNSZXF1ZXN0Gioua2FnZW50LmFwaS52MWFscGhhMS5MaXN0SGFybmVzc2VzUmVzcG9uc2USZgoNQ3JlYXRlSGFybmVzcxIpLmthZ2VudC5hcGkudjFhbHBoYTEuQ3JlYXRlSGFybmVzc1JlcXVlc3QaKi5rYWdlbnQuYXBpLnYxYWxwaGExLkNyZWF0ZUhhcm5lc3NSZXNwb25zZRJmCg1EZWxldGVIYXJuZXNzEikua2FnZW50LmFwaS52MWFscGhhMS5EZWxldGVIYXJuZXNzUmVxdWVzdBoqLmthZ2VudC5hcGkudjFhbHBoYTEuRGVsZXRlSGFybmVzc1Jlc3BvbnNlQklaR2dpdGh1Yi5jb20va2FnZW50LWRldi9rYWdlbnQvZ28vYXBpL2dlbi9rYWdlbnQvYXBpL3YxYWxwaGExO2FwaXYxYWxwaGExYgZwcm90bzM", [file_kagent_api_v1alpha1_common]); /** * @generated from message kagent.api.v1alpha1.Harness @@ -97,40 +97,6 @@ export type ListHarnessesResponse = Message<"kagent.api.v1alpha1.ListHarnessesRe export const ListHarnessesResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_kagent_api_v1alpha1_harnesses, 2); -/** - * @generated from message kagent.api.v1alpha1.GetHarnessRequest - */ -export type GetHarnessRequest = Message<"kagent.api.v1alpha1.GetHarnessRequest"> & { - /** - * @generated from field: kagent.api.v1alpha1.ResourceReference ref = 1; - */ - ref?: ResourceReference | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.GetHarnessRequest. - * Use `create(GetHarnessRequestSchema)` to create a new message. - */ -export const GetHarnessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 3); - -/** - * @generated from message kagent.api.v1alpha1.GetHarnessResponse - */ -export type GetHarnessResponse = Message<"kagent.api.v1alpha1.GetHarnessResponse"> & { - /** - * @generated from field: kagent.api.v1alpha1.Harness harness = 1; - */ - harness?: Harness | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.GetHarnessResponse. - * Use `create(GetHarnessResponseSchema)` to create a new message. - */ -export const GetHarnessResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 4); - /** * @generated from message kagent.api.v1alpha1.CreateHarnessRequest */ @@ -151,7 +117,7 @@ export type CreateHarnessRequest = Message<"kagent.api.v1alpha1.CreateHarnessReq * Use `create(CreateHarnessRequestSchema)` to create a new message. */ export const CreateHarnessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 5); + messageDesc(file_kagent_api_v1alpha1_harnesses, 3); /** * @generated from message kagent.api.v1alpha1.CreateHarnessResponse @@ -168,46 +134,7 @@ export type CreateHarnessResponse = Message<"kagent.api.v1alpha1.CreateHarnessRe * Use `create(CreateHarnessResponseSchema)` to create a new message. */ export const CreateHarnessResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 6); - -/** - * @generated from message kagent.api.v1alpha1.UpdateHarnessRequest - */ -export type UpdateHarnessRequest = Message<"kagent.api.v1alpha1.UpdateHarnessRequest"> & { - /** - * @generated from field: kagent.api.v1alpha1.ResourceReference ref = 1; - */ - ref?: ResourceReference | undefined; - - /** - * @generated from field: kagent.api.v1alpha1.StructuredObject resource = 2; - */ - resource?: StructuredObject | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.UpdateHarnessRequest. - * Use `create(UpdateHarnessRequestSchema)` to create a new message. - */ -export const UpdateHarnessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 7); - -/** - * @generated from message kagent.api.v1alpha1.UpdateHarnessResponse - */ -export type UpdateHarnessResponse = Message<"kagent.api.v1alpha1.UpdateHarnessResponse"> & { - /** - * @generated from field: kagent.api.v1alpha1.Harness harness = 1; - */ - harness?: Harness | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.UpdateHarnessResponse. - * Use `create(UpdateHarnessResponseSchema)` to create a new message. - */ -export const UpdateHarnessResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 8); + messageDesc(file_kagent_api_v1alpha1_harnesses, 4); /** * @generated from message kagent.api.v1alpha1.DeleteHarnessRequest @@ -224,7 +151,7 @@ export type DeleteHarnessRequest = Message<"kagent.api.v1alpha1.DeleteHarnessReq * Use `create(DeleteHarnessRequestSchema)` to create a new message. */ export const DeleteHarnessRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 9); + messageDesc(file_kagent_api_v1alpha1_harnesses, 5); /** * @generated from message kagent.api.v1alpha1.DeleteHarnessResponse @@ -237,7 +164,7 @@ export type DeleteHarnessResponse = Message<"kagent.api.v1alpha1.DeleteHarnessRe * Use `create(DeleteHarnessResponseSchema)` to create a new message. */ export const DeleteHarnessResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_harnesses, 10); + messageDesc(file_kagent_api_v1alpha1_harnesses, 6); /** * HarnessService is CRUD over the kagent.dev/v1alpha3 Harness CRD: the runtime @@ -264,14 +191,6 @@ export const HarnessService: GenService<{ input: typeof ListHarnessesRequestSchema; output: typeof ListHarnessesResponseSchema; }, - /** - * @generated from rpc kagent.api.v1alpha1.HarnessService.GetHarness - */ - getHarness: { - methodKind: "unary"; - input: typeof GetHarnessRequestSchema; - output: typeof GetHarnessResponseSchema; - }, /** * @generated from rpc kagent.api.v1alpha1.HarnessService.CreateHarness */ @@ -280,14 +199,6 @@ export const HarnessService: GenService<{ input: typeof CreateHarnessRequestSchema; output: typeof CreateHarnessResponseSchema; }, - /** - * @generated from rpc kagent.api.v1alpha1.HarnessService.UpdateHarness - */ - updateHarness: { - methodKind: "unary"; - input: typeof UpdateHarnessRequestSchema; - output: typeof UpdateHarnessResponseSchema; - }, /** * @generated from rpc kagent.api.v1alpha1.HarnessService.DeleteHarness */ diff --git a/ui/src/generated/kagent/api/v1alpha1/system_pb.ts b/ui/src/generated/kagent/api/v1alpha1/system_pb.ts index b81b63480..84f234834 100644 --- a/ui/src/generated/kagent/api/v1alpha1/system_pb.ts +++ b/ui/src/generated/kagent/api/v1alpha1/system_pb.ts @@ -2,19 +2,16 @@ // @generated from file kagent/api/v1alpha1/system.proto (package kagent.api.v1alpha1, syntax proto3) /* eslint-disable */ -import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; -import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; -import type { Timestamp } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_struct, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; -import type { PageRequest, PageResponse } from "./common_pb"; -import { file_kagent_api_v1alpha1_common } from "./common_pb"; +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_google_protobuf_struct } from "@bufbuild/protobuf/wkt"; import type { JsonObject, Message } from "@bufbuild/protobuf"; /** * Describes the file kagent/api/v1alpha1/system.proto. */ export const file_kagent_api_v1alpha1_system: GenFile = /*@__PURE__*/ - fileDesc("CiBrYWdlbnQvYXBpL3YxYWxwaGExL3N5c3RlbS5wcm90bxITa2FnZW50LmFwaS52MWFscGhhMSITChFHZXRWZXJzaW9uUmVxdWVzdCJUChJHZXRWZXJzaW9uUmVzcG9uc2USFgoOa2FnZW50X3ZlcnNpb24YASABKAkSEgoKZ2l0X2NvbW1pdBgCIAEoCRISCgpidWlsZF9kYXRlGAMgASgJIhcKFUdldEN1cnJlbnRVc2VyUmVxdWVzdCJBChZHZXRDdXJyZW50VXNlclJlc3BvbnNlEicKBmNsYWltcxgBIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QiFwoVTGlzdE5hbWVzcGFjZXNSZXF1ZXN0IikKCU5hbWVzcGFjZRIMCgRuYW1lGAEgASgJEg4KBnN0YXR1cxgCIAEoCSJMChZMaXN0TmFtZXNwYWNlc1Jlc3BvbnNlEjIKCm5hbWVzcGFjZXMYASADKAsyHi5rYWdlbnQuYXBpLnYxYWxwaGExLk5hbWVzcGFjZSIuChlHZXRTdWJzdHJhdGVTdGF0dXNSZXF1ZXN0EhEKCW5hbWVzcGFjZRgBIAEoCSK2AgoaR2V0U3Vic3RyYXRlU3RhdHVzUmVzcG9uc2USDwoHZW5hYmxlZBgBIAEoCBIVCg1hdGVfYXBpX2Vycm9yGAIgASgJEj4KDHdvcmtlcl9wb29scxgDIAMoCzIoLmthZ2VudC5hcGkudjFhbHBoYTEuU3Vic3RyYXRlV29ya2VyUG9vbBJECg9hY3Rvcl90ZW1wbGF0ZXMYBCADKAsyKy5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZUFjdG9yVGVtcGxhdGUSMwoGYWN0b3JzGAUgAygLMiMua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVBY3RvchI1Cgd3b3JrZXJzGAYgAygLMiQua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVXb3JrZXIiLwoaR2V0U3Vic3RyYXRlU3VtbWFyeVJlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJIjUKFFN1YnN0cmF0ZVN0YXR1c0NvdW50Eg4KBnN0YXR1cxgBIAEoCRINCgVjb3VudBgCIAEoBSKnAwobR2V0U3Vic3RyYXRlU3VtbWFyeVJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSFQoNYXRlX2FwaV9lcnJvchgCIAEoCRI+Cgx3b3JrZXJfcG9vbHMYAyADKAsyKC5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZVdvcmtlclBvb2wSRAoPYWN0b3JfdGVtcGxhdGVzGAQgAygLMisua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVBY3RvclRlbXBsYXRlEhMKC2FjdG9yX2NvdW50GAUgASgFEhQKDHdvcmtlcl9jb3VudBgGIAEoBRIbChNydW5uaW5nX2FjdG9yX2NvdW50GAcgASgFEhkKEWJ1c3lfd29ya2VyX2NvdW50GAggASgFEkYKE2FjdG9yX3N0YXR1c19jb3VudHMYCSADKAsyKS5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZVN0YXR1c0NvdW50Ei8KC2NvbXB1dGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCLuAQoaTGlzdFN1YnN0cmF0ZUFjdG9yc1JlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJEg4KBmZpbHRlchgCIAEoCRIuCgRwYWdlGAMgASgLMiAua2FnZW50LmFwaS52MWFscGhhMS5QYWdlUmVxdWVzdBJACgpzb3J0X2ZpZWxkGAQgASgOMiwua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVBY3RvclNvcnRGaWVsZBI7Cgpzb3J0X29yZGVyGAUgASgOMicua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVTb3J0T3JkZXIi1wIKG0xpc3RTdWJzdHJhdGVBY3RvcnNSZXNwb25zZRIzCgZhY3RvcnMYASADKAsyIy5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZUFjdG9yEi8KBHBhZ2UYAiABKAsyIS5rYWdlbnQuYXBpLnYxYWxwaGExLlBhZ2VSZXNwb25zZRISCgp0b3RhbF9zaXplGAMgASgFEkgKEmFwcGxpZWRfc29ydF9maWVsZBgEIAEoDjIsLmthZ2VudC5hcGkudjFhbHBoYTEuU3Vic3RyYXRlQWN0b3JTb3J0RmllbGQSQwoSYXBwbGllZF9zb3J0X29yZGVyGAUgASgOMicua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVTb3J0T3JkZXISLwoLY29tcHV0ZWRfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIvABChtMaXN0U3Vic3RyYXRlV29ya2Vyc1JlcXVlc3QSEQoJbmFtZXNwYWNlGAEgASgJEg4KBmZpbHRlchgCIAEoCRIuCgRwYWdlGAMgASgLMiAua2FnZW50LmFwaS52MWFscGhhMS5QYWdlUmVxdWVzdBJBCgpzb3J0X2ZpZWxkGAQgASgOMi0ua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVXb3JrZXJTb3J0RmllbGQSOwoKc29ydF9vcmRlchgFIAEoDjInLmthZ2VudC5hcGkudjFhbHBoYTEuU3Vic3RyYXRlU29ydE9yZGVyItsCChxMaXN0U3Vic3RyYXRlV29ya2Vyc1Jlc3BvbnNlEjUKB3dvcmtlcnMYASADKAsyJC5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZVdvcmtlchIvCgRwYWdlGAIgASgLMiEua2FnZW50LmFwaS52MWFscGhhMS5QYWdlUmVzcG9uc2USEgoKdG90YWxfc2l6ZRgDIAEoBRJJChJhcHBsaWVkX3NvcnRfZmllbGQYBCABKA4yLS5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZVdvcmtlclNvcnRGaWVsZBJDChJhcHBsaWVkX3NvcnRfb3JkZXIYBSABKA4yJy5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZVNvcnRPcmRlchIvCgtjb21wdXRlZF9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiXQoTU3Vic3RyYXRlV29ya2VyUG9vbBIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRIQCghyZXBsaWNhcxgDIAEoBRITCgthdGVvbV9pbWFnZRgEIAEoCSLbAQoWU3Vic3RyYXRlQWN0b3JUZW1wbGF0ZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCRIXCg9nb2xkZW5fYWN0b3JfaWQYBCABKAkSFwoPZ29sZGVuX3NuYXBzaG90GAUgASgJEhUKDXNhbmRib3hfY2xhc3MYBiABKAkSFwoPd29ya2VyX3NlbGVjdG9yGAcgASgJEhQKDGhhcm5lc3NfbmFtZRgIIAEoCRIZChFtYW5hZ2VkX2J5X2thZ2VudBgJIAEoCCKwAgoOU3Vic3RyYXRlQWN0b3ISEAoIYWN0b3JfaWQYASABKAkSEAoIYXRlc3BhY2UYAiABKAkSDgoGc3RhdHVzGAMgASgJEiAKGGFjdG9yX3RlbXBsYXRlX25hbWVzcGFjZRgEIAEoCRIbChNhY3Rvcl90ZW1wbGF0ZV9uYW1lGAUgASgJEhsKE2F0ZW9tX3BvZF9uYW1lc3BhY2UYBiABKAkSFgoOYXRlb21fcG9kX25hbWUYByABKAkSFAoMYXRlb21fcG9kX2lwGAggASgJEhcKD2xhdGVzdF9zbmFwc2hvdBgJIAEoCRIYChB3b3JrZXJfcG9vbF9uYW1lGAogASgJEhwKFGluX3Byb2dyZXNzX3NuYXBzaG90GAsgASgJEg8KB3ZlcnNpb24YDCABKAMitAEKD1N1YnN0cmF0ZVdvcmtlchIYChB3b3JrZXJfbmFtZXNwYWNlGAEgASgJEhMKC3dvcmtlcl9wb29sGAIgASgJEhIKCndvcmtlcl9wb2QYAyABKAkSFwoPYWN0b3JfbmFtZXNwYWNlGAQgASgJEhYKDmFjdG9yX3RlbXBsYXRlGAUgASgJEhAKCGFjdG9yX2lkGAYgASgJEgoKAmlwGAcgASgJEg8KB3ZlcnNpb24YCCABKAMqgwEKElN1YnN0cmF0ZVNvcnRPcmRlchIkCiBTVUJTVFJBVEVfU09SVF9PUkRFUl9VTlNQRUNJRklFRBAAEiIKHlNVQlNUUkFURV9TT1JUX09SREVSX0FTQ0VORElORxABEiMKH1NVQlNUUkFURV9TT1JUX09SREVSX0RFU0NFTkRJTkcQAirvAQoXU3Vic3RyYXRlQWN0b3JTb3J0RmllbGQSKgomU1VCU1RSQVRFX0FDVE9SX1NPUlRfRklFTERfVU5TUEVDSUZJRUQQABIlCiFTVUJTVFJBVEVfQUNUT1JfU09SVF9GSUVMRF9TVEFUVVMQARInCiNTVUJTVFJBVEVfQUNUT1JfU09SVF9GSUVMRF9BQ1RPUl9JRBACEi0KKVNVQlNUUkFURV9BQ1RPUl9TT1JUX0ZJRUxEX0FDVE9SX1RFTVBMQVRFEAMSKQolU1VCU1RSQVRFX0FDVE9SX1NPUlRfRklFTERfV09SS0VSX1BPRBAEKrkBChhTdWJzdHJhdGVXb3JrZXJTb3J0RmllbGQSKwonU1VCU1RSQVRFX1dPUktFUl9TT1JUX0ZJRUxEX1VOU1BFQ0lGSUVEEAASJAogU1VCU1RSQVRFX1dPUktFUl9TT1JUX0ZJRUxEX1BPT0wQARIjCh9TVUJTVFJBVEVfV09SS0VSX1NPUlRfRklFTERfUE9EEAISJQohU1VCU1RSQVRFX1dPUktFUl9TT1JUX0ZJRUxEX0FDVE9SEAMyrAYKDVN5c3RlbVNlcnZpY2USXQoKR2V0VmVyc2lvbhImLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0VmVyc2lvblJlcXVlc3QaJy5rYWdlbnQuYXBpLnYxYWxwaGExLkdldFZlcnNpb25SZXNwb25zZRJpCg5HZXRDdXJyZW50VXNlchIqLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0Q3VycmVudFVzZXJSZXF1ZXN0Gisua2FnZW50LmFwaS52MWFscGhhMS5HZXRDdXJyZW50VXNlclJlc3BvbnNlEmkKDkxpc3ROYW1lc3BhY2VzEioua2FnZW50LmFwaS52MWFscGhhMS5MaXN0TmFtZXNwYWNlc1JlcXVlc3QaKy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3ROYW1lc3BhY2VzUmVzcG9uc2USdQoSR2V0U3Vic3RyYXRlU3RhdHVzEi4ua2FnZW50LmFwaS52MWFscGhhMS5HZXRTdWJzdHJhdGVTdGF0dXNSZXF1ZXN0Gi8ua2FnZW50LmFwaS52MWFscGhhMS5HZXRTdWJzdHJhdGVTdGF0dXNSZXNwb25zZRJ4ChNHZXRTdWJzdHJhdGVTdW1tYXJ5Ei8ua2FnZW50LmFwaS52MWFscGhhMS5HZXRTdWJzdHJhdGVTdW1tYXJ5UmVxdWVzdBowLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0U3Vic3RyYXRlU3VtbWFyeVJlc3BvbnNlEngKE0xpc3RTdWJzdHJhdGVBY3RvcnMSLy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RTdWJzdHJhdGVBY3RvcnNSZXF1ZXN0GjAua2FnZW50LmFwaS52MWFscGhhMS5MaXN0U3Vic3RyYXRlQWN0b3JzUmVzcG9uc2USewoUTGlzdFN1YnN0cmF0ZVdvcmtlcnMSMC5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3RTdWJzdHJhdGVXb3JrZXJzUmVxdWVzdBoxLmthZ2VudC5hcGkudjFhbHBoYTEuTGlzdFN1YnN0cmF0ZVdvcmtlcnNSZXNwb25zZUJJWkdnaXRodWIuY29tL2thZ2VudC1kZXYva2FnZW50L2dvL2FwaS9nZW4va2FnZW50L2FwaS92MWFscGhhMTthcGl2MWFscGhhMWIGcHJvdG8z", [file_google_protobuf_struct, file_google_protobuf_timestamp, file_kagent_api_v1alpha1_common]); + fileDesc("CiBrYWdlbnQvYXBpL3YxYWxwaGExL3N5c3RlbS5wcm90bxITa2FnZW50LmFwaS52MWFscGhhMSITChFHZXRWZXJzaW9uUmVxdWVzdCJUChJHZXRWZXJzaW9uUmVzcG9uc2USFgoOa2FnZW50X3ZlcnNpb24YASABKAkSEgoKZ2l0X2NvbW1pdBgCIAEoCRISCgpidWlsZF9kYXRlGAMgASgJIhcKFUdldEN1cnJlbnRVc2VyUmVxdWVzdCJBChZHZXRDdXJyZW50VXNlclJlc3BvbnNlEicKBmNsYWltcxgBIAEoCzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3QiFwoVTGlzdE5hbWVzcGFjZXNSZXF1ZXN0IikKCU5hbWVzcGFjZRIMCgRuYW1lGAEgASgJEg4KBnN0YXR1cxgCIAEoCSJMChZMaXN0TmFtZXNwYWNlc1Jlc3BvbnNlEjIKCm5hbWVzcGFjZXMYASADKAsyHi5rYWdlbnQuYXBpLnYxYWxwaGExLk5hbWVzcGFjZSIuChlHZXRTdWJzdHJhdGVTdGF0dXNSZXF1ZXN0EhEKCW5hbWVzcGFjZRgBIAEoCSK2AgoaR2V0U3Vic3RyYXRlU3RhdHVzUmVzcG9uc2USDwoHZW5hYmxlZBgBIAEoCBIVCg1hdGVfYXBpX2Vycm9yGAIgASgJEj4KDHdvcmtlcl9wb29scxgDIAMoCzIoLmthZ2VudC5hcGkudjFhbHBoYTEuU3Vic3RyYXRlV29ya2VyUG9vbBJECg9hY3Rvcl90ZW1wbGF0ZXMYBCADKAsyKy5rYWdlbnQuYXBpLnYxYWxwaGExLlN1YnN0cmF0ZUFjdG9yVGVtcGxhdGUSMwoGYWN0b3JzGAUgAygLMiMua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVBY3RvchI1Cgd3b3JrZXJzGAYgAygLMiQua2FnZW50LmFwaS52MWFscGhhMS5TdWJzdHJhdGVXb3JrZXIiXQoTU3Vic3RyYXRlV29ya2VyUG9vbBIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRIQCghyZXBsaWNhcxgDIAEoBRITCgthdGVvbV9pbWFnZRgEIAEoCSLbAQoWU3Vic3RyYXRlQWN0b3JUZW1wbGF0ZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCRIXCg9nb2xkZW5fYWN0b3JfaWQYBCABKAkSFwoPZ29sZGVuX3NuYXBzaG90GAUgASgJEhUKDXNhbmRib3hfY2xhc3MYBiABKAkSFwoPd29ya2VyX3NlbGVjdG9yGAcgASgJEhQKDGhhcm5lc3NfbmFtZRgIIAEoCRIZChFtYW5hZ2VkX2J5X2thZ2VudBgJIAEoCCKwAgoOU3Vic3RyYXRlQWN0b3ISEAoIYWN0b3JfaWQYASABKAkSEAoIYXRlc3BhY2UYAiABKAkSDgoGc3RhdHVzGAMgASgJEiAKGGFjdG9yX3RlbXBsYXRlX25hbWVzcGFjZRgEIAEoCRIbChNhY3Rvcl90ZW1wbGF0ZV9uYW1lGAUgASgJEhsKE2F0ZW9tX3BvZF9uYW1lc3BhY2UYBiABKAkSFgoOYXRlb21fcG9kX25hbWUYByABKAkSFAoMYXRlb21fcG9kX2lwGAggASgJEhcKD2xhdGVzdF9zbmFwc2hvdBgJIAEoCRIYChB3b3JrZXJfcG9vbF9uYW1lGAogASgJEhwKFGluX3Byb2dyZXNzX3NuYXBzaG90GAsgASgJEg8KB3ZlcnNpb24YDCABKAMitAEKD1N1YnN0cmF0ZVdvcmtlchIYChB3b3JrZXJfbmFtZXNwYWNlGAEgASgJEhMKC3dvcmtlcl9wb29sGAIgASgJEhIKCndvcmtlcl9wb2QYAyABKAkSFwoPYWN0b3JfbmFtZXNwYWNlGAQgASgJEhYKDmFjdG9yX3RlbXBsYXRlGAUgASgJEhAKCGFjdG9yX2lkGAYgASgJEgoKAmlwGAcgASgJEg8KB3ZlcnNpb24YCCABKAMyuwMKDVN5c3RlbVNlcnZpY2USXQoKR2V0VmVyc2lvbhImLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0VmVyc2lvblJlcXVlc3QaJy5rYWdlbnQuYXBpLnYxYWxwaGExLkdldFZlcnNpb25SZXNwb25zZRJpCg5HZXRDdXJyZW50VXNlchIqLmthZ2VudC5hcGkudjFhbHBoYTEuR2V0Q3VycmVudFVzZXJSZXF1ZXN0Gisua2FnZW50LmFwaS52MWFscGhhMS5HZXRDdXJyZW50VXNlclJlc3BvbnNlEmkKDkxpc3ROYW1lc3BhY2VzEioua2FnZW50LmFwaS52MWFscGhhMS5MaXN0TmFtZXNwYWNlc1JlcXVlc3QaKy5rYWdlbnQuYXBpLnYxYWxwaGExLkxpc3ROYW1lc3BhY2VzUmVzcG9uc2USdQoSR2V0U3Vic3RyYXRlU3RhdHVzEi4ua2FnZW50LmFwaS52MWFscGhhMS5HZXRTdWJzdHJhdGVTdGF0dXNSZXF1ZXN0Gi8ua2FnZW50LmFwaS52MWFscGhhMS5HZXRTdWJzdHJhdGVTdGF0dXNSZXNwb25zZUJJWkdnaXRodWIuY29tL2thZ2VudC1kZXYva2FnZW50L2dvL2FwaS9nZW4va2FnZW50L2FwaS92MWFscGhhMTthcGl2MWFscGhhMWIGcHJvdG8z", [file_google_protobuf_struct]); /** * @generated from message kagent.api.v1alpha1.GetVersionRequest @@ -197,341 +194,6 @@ export type GetSubstrateStatusResponse = Message<"kagent.api.v1alpha1.GetSubstra export const GetSubstrateStatusResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_kagent_api_v1alpha1_system, 8); -/** - * @generated from message kagent.api.v1alpha1.GetSubstrateSummaryRequest - */ -export type GetSubstrateSummaryRequest = Message<"kagent.api.v1alpha1.GetSubstrateSummaryRequest"> & { - /** - * Namespace narrows the inventory. Empty means every namespace the - * controller observes, as it does on GetSubstrateStatusRequest. - * - * @generated from field: string namespace = 1; - */ - namespace: string; -}; - -/** - * Describes the message kagent.api.v1alpha1.GetSubstrateSummaryRequest. - * Use `create(GetSubstrateSummaryRequestSchema)` to create a new message. - */ -export const GetSubstrateSummaryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 9); - -/** - * SubstrateStatusCount is how many rows carry one status. - * - * Status is a plain string on the wire rather than an enum: ate-api and the - * ActorTemplate controller each fill it in their own vocabulary, so a closed - * set here would drop a status a newer substrate reports. Counting whatever - * arrives keeps the tally complete even when a value is one this build has - * never seen. - * - * @generated from message kagent.api.v1alpha1.SubstrateStatusCount - */ -export type SubstrateStatusCount = Message<"kagent.api.v1alpha1.SubstrateStatusCount"> & { - /** - * @generated from field: string status = 1; - */ - status: string; - - /** - * @generated from field: int32 count = 2; - */ - count: number; -}; - -/** - * Describes the message kagent.api.v1alpha1.SubstrateStatusCount. - * Use `create(SubstrateStatusCountSchema)` to create a new message. - */ -export const SubstrateStatusCountSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 10); - -/** - * @generated from message kagent.api.v1alpha1.GetSubstrateSummaryResponse - */ -export type GetSubstrateSummaryResponse = Message<"kagent.api.v1alpha1.GetSubstrateSummaryResponse"> & { - /** - * Enabled is false when the controller has no ate-api endpoint configured, - * which is an ordinary deployment rather than a failure. - * - * @generated from field: bool enabled = 1; - */ - enabled: boolean; - - /** - * AteApiError is set when ate-api answered with an error on an otherwise - * successful read: the Kubernetes-derived halves below are complete while the - * runtime counts may be short. Distinct from the RPC failing, and worth - * reporting differently. - * - * @generated from field: string ate_api_error = 2; - */ - ateApiError: string; - - /** - * Worker pools and actor templates are bounded by how the cluster is - * configured rather than by how much work it is doing — a handful either way - * — so they ride inline instead of costing two more round trips. - * - * @generated from field: repeated kagent.api.v1alpha1.SubstrateWorkerPool worker_pools = 3; - */ - workerPools: SubstrateWorkerPool[]; - - /** - * @generated from field: repeated kagent.api.v1alpha1.SubstrateActorTemplate actor_templates = 4; - */ - actorTemplates: SubstrateActorTemplate[]; - - /** - * Totals over everything in scope, before any filter. - * - * @generated from field: int32 actor_count = 5; - */ - actorCount: number; - - /** - * @generated from field: int32 worker_count = 6; - */ - workerCount: number; - - /** - * RunningActorCount and BusyWorkerCount are the numerators the inventory is - * actually read by: how much of what exists is doing something. A worker is - * busy when an actor is placed on it. - * - * @generated from field: int32 running_actor_count = 7; - */ - runningActorCount: number; - - /** - * @generated from field: int32 busy_worker_count = 8; - */ - busyWorkerCount: number; - - /** - * ActorStatusCounts is every status present, with how many actors hold it, - * ordered by status. The whole distribution rather than the running count - * alone, so a caller can say what the rest are without reading them. - * - * @generated from field: repeated kagent.api.v1alpha1.SubstrateStatusCount actor_status_counts = 9; - */ - actorStatusCounts: SubstrateStatusCount[]; - - /** - * ComputedAt is when this answer was produced, which is not necessarily now. - * - * The substrate reads are memoised for a fraction of a second, because each one - * walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - * them. A cache is also exactly how a polling control becomes a lie, so the age - * travels with the answer: a caller can say "as of 0.4s ago" rather than - * implying "now", and a reader can tell a stalled cluster from a stalled read. - * - * @generated from field: google.protobuf.Timestamp computed_at = 10; - */ - computedAt?: Timestamp | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.GetSubstrateSummaryResponse. - * Use `create(GetSubstrateSummaryResponseSchema)` to create a new message. - */ -export const GetSubstrateSummaryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 11); - -/** - * @generated from message kagent.api.v1alpha1.ListSubstrateActorsRequest - */ -export type ListSubstrateActorsRequest = Message<"kagent.api.v1alpha1.ListSubstrateActorsRequest"> & { - /** - * @generated from field: string namespace = 1; - */ - namespace: string; - - /** - * Filter is matched case-insensitively as a substring against the actor's id, - * status, actor template and worker pod — the fields a row displays. Empty - * matches everything. - * - * @generated from field: string filter = 2; - */ - filter: string; - - /** - * @generated from field: kagent.api.v1alpha1.PageRequest page = 3; - */ - page?: PageRequest | undefined; - - /** - * Sorting is server-side because the rows are paged: ordering a page that has - * already been fetched reorders a hundred rows out of hundreds of thousands, - * which looks like sorting and is not. - * - * @generated from field: kagent.api.v1alpha1.SubstrateActorSortField sort_field = 4; - */ - sortField: SubstrateActorSortField; - - /** - * @generated from field: kagent.api.v1alpha1.SubstrateSortOrder sort_order = 5; - */ - sortOrder: SubstrateSortOrder; -}; - -/** - * Describes the message kagent.api.v1alpha1.ListSubstrateActorsRequest. - * Use `create(ListSubstrateActorsRequestSchema)` to create a new message. - */ -export const ListSubstrateActorsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 12); - -/** - * @generated from message kagent.api.v1alpha1.ListSubstrateActorsResponse - */ -export type ListSubstrateActorsResponse = Message<"kagent.api.v1alpha1.ListSubstrateActorsResponse"> & { - /** - * @generated from field: repeated kagent.api.v1alpha1.SubstrateActor actors = 1; - */ - actors: SubstrateActor[]; - - /** - * @generated from field: kagent.api.v1alpha1.PageResponse page = 2; - */ - page?: PageResponse | undefined; - - /** - * TotalSize is how many actors match the filter across every page, so a - * caller can say "20 of 4,312" rather than implying the page is the whole - * result. - * - * @generated from field: int32 total_size = 3; - */ - totalSize: number; - - /** - * The order actually applied, so a caller can say how the rows are sorted - * rather than assuming its request was honoured. An unspecified field and an - * unspecified order both resolve to a concrete value here. - * - * @generated from field: kagent.api.v1alpha1.SubstrateActorSortField applied_sort_field = 4; - */ - appliedSortField: SubstrateActorSortField; - - /** - * @generated from field: kagent.api.v1alpha1.SubstrateSortOrder applied_sort_order = 5; - */ - appliedSortOrder: SubstrateSortOrder; - - /** - * ComputedAt is when this answer was produced, which is not necessarily now. - * - * The substrate reads are memoised for a fraction of a second, because each one - * walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - * them. A cache is also exactly how a polling control becomes a lie, so the age - * travels with the answer: a caller can say "as of 0.4s ago" rather than - * implying "now", and a reader can tell a stalled cluster from a stalled read. - * - * @generated from field: google.protobuf.Timestamp computed_at = 6; - */ - computedAt?: Timestamp | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.ListSubstrateActorsResponse. - * Use `create(ListSubstrateActorsResponseSchema)` to create a new message. - */ -export const ListSubstrateActorsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 13); - -/** - * @generated from message kagent.api.v1alpha1.ListSubstrateWorkersRequest - */ -export type ListSubstrateWorkersRequest = Message<"kagent.api.v1alpha1.ListSubstrateWorkersRequest"> & { - /** - * @generated from field: string namespace = 1; - */ - namespace: string; - - /** - * Filter is matched case-insensitively as a substring against the worker's - * namespace, pod, pool and placed actor. - * - * @generated from field: string filter = 2; - */ - filter: string; - - /** - * @generated from field: kagent.api.v1alpha1.PageRequest page = 3; - */ - page?: PageRequest | undefined; - - /** - * @generated from field: kagent.api.v1alpha1.SubstrateWorkerSortField sort_field = 4; - */ - sortField: SubstrateWorkerSortField; - - /** - * @generated from field: kagent.api.v1alpha1.SubstrateSortOrder sort_order = 5; - */ - sortOrder: SubstrateSortOrder; -}; - -/** - * Describes the message kagent.api.v1alpha1.ListSubstrateWorkersRequest. - * Use `create(ListSubstrateWorkersRequestSchema)` to create a new message. - */ -export const ListSubstrateWorkersRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 14); - -/** - * @generated from message kagent.api.v1alpha1.ListSubstrateWorkersResponse - */ -export type ListSubstrateWorkersResponse = Message<"kagent.api.v1alpha1.ListSubstrateWorkersResponse"> & { - /** - * @generated from field: repeated kagent.api.v1alpha1.SubstrateWorker workers = 1; - */ - workers: SubstrateWorker[]; - - /** - * @generated from field: kagent.api.v1alpha1.PageResponse page = 2; - */ - page?: PageResponse | undefined; - - /** - * @generated from field: int32 total_size = 3; - */ - totalSize: number; - - /** - * @generated from field: kagent.api.v1alpha1.SubstrateWorkerSortField applied_sort_field = 4; - */ - appliedSortField: SubstrateWorkerSortField; - - /** - * @generated from field: kagent.api.v1alpha1.SubstrateSortOrder applied_sort_order = 5; - */ - appliedSortOrder: SubstrateSortOrder; - - /** - * ComputedAt is when this answer was produced, which is not necessarily now. - * - * The substrate reads are memoised for a fraction of a second, because each one - * walks ate-api's whole actor list — ~1.6s on a deployment holding 410,110 of - * them. A cache is also exactly how a polling control becomes a lie, so the age - * travels with the answer: a caller can say "as of 0.4s ago" rather than - * implying "now", and a reader can tell a stalled cluster from a stalled read. - * - * @generated from field: google.protobuf.Timestamp computed_at = 6; - */ - computedAt?: Timestamp | undefined; -}; - -/** - * Describes the message kagent.api.v1alpha1.ListSubstrateWorkersResponse. - * Use `create(ListSubstrateWorkersResponseSchema)` to create a new message. - */ -export const ListSubstrateWorkersResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 15); - /** * @generated from message kagent.api.v1alpha1.SubstrateWorkerPool */ @@ -562,7 +224,7 @@ export type SubstrateWorkerPool = Message<"kagent.api.v1alpha1.SubstrateWorkerPo * Use `create(SubstrateWorkerPoolSchema)` to create a new message. */ export const SubstrateWorkerPoolSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 16); + messageDesc(file_kagent_api_v1alpha1_system, 9); /** * @generated from message kagent.api.v1alpha1.SubstrateActorTemplate @@ -619,7 +281,7 @@ export type SubstrateActorTemplate = Message<"kagent.api.v1alpha1.SubstrateActor * Use `create(SubstrateActorTemplateSchema)` to create a new message. */ export const SubstrateActorTemplateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 17); + messageDesc(file_kagent_api_v1alpha1_system, 10); /** * @generated from message kagent.api.v1alpha1.SubstrateActor @@ -691,7 +353,7 @@ export type SubstrateActor = Message<"kagent.api.v1alpha1.SubstrateActor"> & { * Use `create(SubstrateActorSchema)` to create a new message. */ export const SubstrateActorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 18); + messageDesc(file_kagent_api_v1alpha1_system, 11); /** * @generated from message kagent.api.v1alpha1.SubstrateWorker @@ -743,121 +405,7 @@ export type SubstrateWorker = Message<"kagent.api.v1alpha1.SubstrateWorker"> & { * Use `create(SubstrateWorkerSchema)` to create a new message. */ export const SubstrateWorkerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_kagent_api_v1alpha1_system, 19); - -/** - * SubstrateSortOrder is the direction a paged substrate read is sorted in. - * - * @generated from enum kagent.api.v1alpha1.SubstrateSortOrder - */ -export enum SubstrateSortOrder { - /** - * Unspecified sorts ascending, which is what every default order below reads - * naturally in. - * - * @generated from enum value: SUBSTRATE_SORT_ORDER_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: SUBSTRATE_SORT_ORDER_ASCENDING = 1; - */ - ASCENDING = 1, - - /** - * @generated from enum value: SUBSTRATE_SORT_ORDER_DESCENDING = 2; - */ - DESCENDING = 2, -} - -/** - * Describes the enum kagent.api.v1alpha1.SubstrateSortOrder. - */ -export const SubstrateSortOrderSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_kagent_api_v1alpha1_system, 0); - -/** - * SubstrateActorSortField is the column ListSubstrateActors orders by. - * - * Every order ends in the actor id, which is unique — so a page token, which is - * the sort key of the last row already sent, always identifies exactly one row. - * A key that could tie would skip or repeat rows at a page boundary. - * - * @generated from enum kagent.api.v1alpha1.SubstrateActorSortField - */ -export enum SubstrateActorSortField { - /** - * Unspecified groups by status and orders by id within each group. That is the - * order the inventory is most usefully read in, and it is stable: ate-api - * returns actors in whatever order it holds them, so an unsorted list puts a - * different actor on every page each time it is asked. - * - * @generated from enum value: SUBSTRATE_ACTOR_SORT_FIELD_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: SUBSTRATE_ACTOR_SORT_FIELD_STATUS = 1; - */ - STATUS = 1, - - /** - * @generated from enum value: SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_ID = 2; - */ - ACTOR_ID = 2, - - /** - * @generated from enum value: SUBSTRATE_ACTOR_SORT_FIELD_ACTOR_TEMPLATE = 3; - */ - ACTOR_TEMPLATE = 3, - - /** - * @generated from enum value: SUBSTRATE_ACTOR_SORT_FIELD_WORKER_POD = 4; - */ - WORKER_POD = 4, -} - -/** - * Describes the enum kagent.api.v1alpha1.SubstrateActorSortField. - */ -export const SubstrateActorSortFieldSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_kagent_api_v1alpha1_system, 1); - -/** - * SubstrateWorkerSortField is the column ListSubstrateWorkers orders by. - * Every order ends in the worker pod, which is unique within its namespace. - * - * @generated from enum kagent.api.v1alpha1.SubstrateWorkerSortField - */ -export enum SubstrateWorkerSortField { - /** - * Unspecified groups by pool and orders by pod within each group. - * - * @generated from enum value: SUBSTRATE_WORKER_SORT_FIELD_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: SUBSTRATE_WORKER_SORT_FIELD_POOL = 1; - */ - POOL = 1, - - /** - * @generated from enum value: SUBSTRATE_WORKER_SORT_FIELD_POD = 2; - */ - POD = 2, - - /** - * @generated from enum value: SUBSTRATE_WORKER_SORT_FIELD_ACTOR = 3; - */ - ACTOR = 3, -} - -/** - * Describes the enum kagent.api.v1alpha1.SubstrateWorkerSortField. - */ -export const SubstrateWorkerSortFieldSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_kagent_api_v1alpha1_system, 2); + messageDesc(file_kagent_api_v1alpha1_system, 12); /** * @generated from service kagent.api.v1alpha1.SystemService @@ -888,20 +436,6 @@ export const SystemService: GenService<{ output: typeof ListNamespacesResponseSchema; }, /** - * GetSubstrateStatus returns the entire inventory in one message: every - * worker pool, actor template, actor and worker, unpaginated and unfiltered. - * - * It does not survive a real cluster. A deployment reporting 103,134 actors - * answers with a message the gRPC client refuses outright — "trying to send - * message larger than max (43016460 vs. 16777216)" — so the caller gets no - * inventory at all rather than a large one. Raising the ceiling moves the - * number without changing the shape. - * - * Prefer GetSubstrateSummary with ListSubstrateActors and - * ListSubstrateWorkers, which bound what any single response can carry. This - * RPC is kept for callers that predate them and for the small clusters where - * it still works. - * * @generated from rpc kagent.api.v1alpha1.SystemService.GetSubstrateStatus */ getSubstrateStatus: { @@ -909,48 +443,6 @@ export const SystemService: GenService<{ input: typeof GetSubstrateStatusRequestSchema; output: typeof GetSubstrateStatusResponseSchema; }, - /** - * GetSubstrateSummary returns counts computed server-side, plus the two lists - * that are inherently small. - * - * This is the only honest source of a total. A caller that counts a page and - * presents the result as a total reports "3 actors" for a cluster running a - * hundred thousand, which is the specific failure the paged RPCs below would - * otherwise introduce. - * - * @generated from rpc kagent.api.v1alpha1.SystemService.GetSubstrateSummary - */ - getSubstrateSummary: { - methodKind: "unary"; - input: typeof GetSubstrateSummaryRequestSchema; - output: typeof GetSubstrateSummaryResponseSchema; - }, - /** - * ListSubstrateActors pages the actors, narrowing them server-side. - * - * Paged because this is one of the two lists whose length is set by the - * cluster rather than by configuration, and filtered server-side for the same - * reason: narrowing a page that has already been fetched searches only what - * was fetched, so a match on page nine reads on screen as "no matches". - * - * @generated from rpc kagent.api.v1alpha1.SystemService.ListSubstrateActors - */ - listSubstrateActors: { - methodKind: "unary"; - input: typeof ListSubstrateActorsRequestSchema; - output: typeof ListSubstrateActorsResponseSchema; - }, - /** - * ListSubstrateWorkers pages the worker assignments. The mirror of - * ListSubstrateActors. - * - * @generated from rpc kagent.api.v1alpha1.SystemService.ListSubstrateWorkers - */ - listSubstrateWorkers: { - methodKind: "unary"; - input: typeof ListSubstrateWorkersRequestSchema; - output: typeof ListSubstrateWorkersResponseSchema; - }, }> = /*@__PURE__*/ serviceDesc(file_kagent_api_v1alpha1_system, 0); diff --git a/ui/src/mocks/transport.ts b/ui/src/mocks/transport.ts index 45aa65300..7d32597f2 100644 --- a/ui/src/mocks/transport.ts +++ b/ui/src/mocks/transport.ts @@ -91,12 +91,7 @@ import { type SessionSchema, type SessionShareSchema, } from "@/generated/kagent/api/v1alpha1/sessions_pb"; -import { - SubstrateActorSortField as PbActorSortField, - SubstrateSortOrder as PbSortOrder, - SubstrateWorkerSortField as PbWorkerSortField, - SystemService, -} from "@/generated/kagent/api/v1alpha1/system_pb"; +import { SystemService } from "@/generated/kagent/api/v1alpha1/system_pb"; import { Role as PbRole, TaskState as PbTaskState } from "@/generated/a2a_pb"; import { AgentInstanceOperation as PbAgentInstanceOperation, @@ -1765,269 +1760,6 @@ on(SystemService.method.getSubstrateStatus, (input, call) => { * because a version string is exactly the sort of thing that gets pasted into a * bug report. */ -/* - * The summary and the two paged reads that replaced the one unbounded inventory. - * - * Counted from the same fixture the unpaged read uses, so the tiles and the tables - * cannot disagree with each other — which on a real cluster they cannot either, - * because the server counts. - */ -on(SystemService.method.getSubstrateSummary, (input, call) => { - if (call.scenario === "empty") return { enabled: false }; - - const status = mockSubstrateStatus; - const scope = input.namespace.trim(); - const inScope = (namespace: string | undefined) => - scope === "" || !namespace || namespace === scope; - - const actors = status.actors.filter((actor) => inScope(actor.actorTemplateNamespace)); - const workers = status.workers.filter((worker) => inScope(worker.workerNamespace)); - - const statusCounts = new Map(); - for (const actor of actors) { - const label = actor.status ?? ""; - statusCounts.set(label, (statusCounts.get(label) ?? 0) + 1); - } - - return { - enabled: status.enabled, - ateApiError: status.ateApiError ?? "", - workerPools: status.workerPools - .filter((pool) => inScope(pool.namespace)) - .map((pool) => ({ - namespace: pool.namespace, - name: pool.name, - replicas: pool.replicas ?? 0, - ateomImage: pool.ateomImage ?? "", - })), - actorTemplates: status.actorTemplates - .filter((template) => inScope(template.namespace)) - .map((template) => ({ - namespace: template.namespace, - name: template.name, - phase: template.phase ?? "", - goldenActorId: template.goldenActorId ?? "", - goldenSnapshot: template.goldenSnapshot ?? "", - sandboxClass: template.sandboxClass ?? "", - workerSelector: template.workerSelector ?? "", - harnessName: template.harnessName ?? "", - managedByKagent: template.managedByKagent ?? false, - })), - actorCount: actors.length, - workerCount: workers.length, - runningActorCount: actors.filter( - (actor) => (actor.status ?? "").toLowerCase() === "running", - ).length, - busyWorkerCount: workers.filter((worker) => Boolean(worker.actorId)).length, - actorStatusCounts: [...statusCounts.entries()] - .map(([status, count]) => ({ status, count })) - .sort((left, right) => left.status.localeCompare(right.status)), - computedAt: timestampFromDate(new Date()), - }; -}); - -on(SystemService.method.listSubstrateActors, (input, call) => { - if (call.scenario === "empty") return { actors: [], page: {}, totalSize: 0 }; - - const scope = input.namespace.trim(); - const needle = input.filter.trim().toLowerCase(); - const matching = mockSubstrateStatus.actors - .filter( - (actor) => - scope === "" || - !actor.actorTemplateNamespace || - actor.actorTemplateNamespace === scope, - ) - // Matched against the fields a row displays, as the controller does — a search - // that hit on something off screen would read as filtering at random. - .filter( - (actor) => - needle === "" || - [ - actor.actorId, - actor.status, - actor.actorTemplateNamespace, - actor.actorTemplateName, - actor.ateomPodNamespace, - actor.ateomPodName, - actor.ateomPodIp, - ] - .filter(Boolean) - .join(" ") - .toLowerCase() - .includes(needle), - ) - ; - - /* - * The same sort keys the controller uses, and for the same reason: each one ends - * in a unique tiebreaker so the page token — which is the key of the last row - * sent — names exactly one row. - */ - const actorKey = (actor: (typeof matching)[number]) => { - switch (input.sortField) { - case PbActorSortField.ACTOR_ID: - return actor.actorId; - case PbActorSortField.ACTOR_TEMPLATE: - return `${actor.actorTemplateNamespace ?? ""}/${actor.actorTemplateName ?? ""}\u0000${actor.actorId}`; - case PbActorSortField.WORKER_POD: - return `${actor.ateomPodNamespace ?? ""}/${actor.ateomPodName ?? ""}\u0000${actor.actorId}`; - default: - return `${actor.status ?? ""}\u0000${actor.actorId}`; - } - }; - const descending = input.sortOrder === PbSortOrder.DESCENDING; - matching.sort((left, right) => { - const compared = actorKey(left).localeCompare(actorKey(right)); - return descending ? -compared : compared; - }); - - const { page, nextPageToken } = paged( - matching, - input.page?.limit ?? 0, - input.page?.pageToken ?? "", - actorKey, - descending, - ); - - return { - actors: page.map((actor) => ({ - actorId: actor.actorId, - atespace: actor.atespace ?? "", - status: actor.status ?? "", - actorTemplateNamespace: actor.actorTemplateNamespace ?? "", - actorTemplateName: actor.actorTemplateName ?? "", - ateomPodNamespace: actor.ateomPodNamespace ?? "", - ateomPodName: actor.ateomPodName ?? "", - ateomPodIp: actor.ateomPodIp ?? "", - latestSnapshot: actor.latestSnapshot ?? "", - workerPoolName: actor.workerPoolName ?? "", - inProgressSnapshot: actor.inProgressSnapshot ?? "", - version: BigInt(actor.version ?? 0), - })), - page: { nextPageToken }, - // The whole matching count, not the page's length: it is what lets a caller say - // "3 of 40" rather than implying the page is the lot. - totalSize: matching.length, - // The order actually applied, which is what the table reports — a fixture that - // echoed the request would agree with a client that had ignored its own. - appliedSortField: input.sortField, - appliedSortOrder: descending - ? PbSortOrder.DESCENDING - : PbSortOrder.ASCENDING, - computedAt: timestampFromDate(new Date()), - }; -}); - -on(SystemService.method.listSubstrateWorkers, (input, call) => { - if (call.scenario === "empty") return { workers: [], page: {}, totalSize: 0 }; - - const scope = input.namespace.trim(); - const needle = input.filter.trim().toLowerCase(); - const matching = mockSubstrateStatus.workers - .filter( - (worker) => - scope === "" || !worker.workerNamespace || worker.workerNamespace === scope, - ) - .filter( - (worker) => - needle === "" || - [ - worker.workerNamespace, - worker.workerPool, - worker.workerPod, - worker.actorNamespace, - worker.actorTemplate, - worker.actorId, - worker.ip, - ] - .filter(Boolean) - .join(" ") - .toLowerCase() - .includes(needle), - ) - ; - - const pod = (worker: (typeof matching)[number]) => - `${worker.workerNamespace}/${worker.workerPod}`; - const workerKey = (worker: (typeof matching)[number]) => { - switch (input.sortField) { - case PbWorkerSortField.POD: - return pod(worker); - case PbWorkerSortField.ACTOR: - // Idle workers sort together, and after the busy ones ascending — an empty - // string would put every available worker first. - return `${worker.actorId || "\uffff"}\u0000${pod(worker)}`; - default: - return `${worker.workerPool}\u0000${pod(worker)}`; - } - }; - const descending = input.sortOrder === PbSortOrder.DESCENDING; - matching.sort((left, right) => { - const compared = workerKey(left).localeCompare(workerKey(right)); - return descending ? -compared : compared; - }); - - const { page, nextPageToken } = paged( - matching, - input.page?.limit ?? 0, - input.page?.pageToken ?? "", - workerKey, - descending, - ); - - return { - workers: page.map((worker) => ({ - workerNamespace: worker.workerNamespace, - workerPool: worker.workerPool, - workerPod: worker.workerPod, - actorNamespace: worker.actorNamespace ?? "", - actorTemplate: worker.actorTemplate ?? "", - actorId: worker.actorId ?? "", - ip: worker.ip ?? "", - version: BigInt(worker.version ?? 0), - })), - page: { nextPageToken }, - totalSize: matching.length, - appliedSortField: input.sortField, - appliedSortOrder: descending - ? PbSortOrder.DESCENDING - : PbSortOrder.ASCENDING, - computedAt: timestampFromDate(new Date()), - }; -}); - -/** - * One page of a sorted list, keyed the way the controller keys it. - * - * The token is the sort key of the last row already sent, and it is empty on the - * last page — so a caller never fetches an empty page to discover it has finished. - * Shared by both paged reads so they cannot page differently from each other. - */ -function paged( - rows: readonly T[], - limit: number, - pageToken: string, - key: (row: T) => string, - descending = false, -): { page: T[]; nextPageToken: string } { - const size = limit > 0 ? limit : SUBSTRATE_DEFAULT_PAGE_SIZE; - // "After the token" means *later in the chosen order*, so descending compares the - // other way. Getting this wrong drops rows at a page boundary, silently. - const after = (rowKey: string) => - descending ? rowKey < pageToken : rowKey > pageToken; - const start = pageToken ? rows.findIndex((row) => after(key(row))) : 0; - if (start === -1) return { page: [], nextPageToken: "" }; - const page = rows.slice(start, start + size); - const more = start + size < rows.length; - return { - page, - nextPageToken: more ? key(page[page.length - 1]) : "", - }; -} - -/** What a paged substrate read answers with when the caller does not ask. */ -const SUBSTRATE_DEFAULT_PAGE_SIZE = 50; on(SystemService.method.getVersion, () => ({ kagentVersion: "0.0.0-mock", From c2501b6a86dc29094c6905eda7b2c63d45854238 Mon Sep 17 00:00:00 2001 From: Nicholas Bucher Date: Wed, 26 Aug 2026 11:11:51 -0400 Subject: [PATCH 04/25] test(ui): fix four flaky browser tests, and let CI run the suite in parallel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `workers: 1` in CI was buying real stability, and this is what it was hiding. Seven full-suite runs at high concurrency produced five failures on five different tests, about one per run. Each is now diagnosed rather than retried away. Two were the suite's own fault, and were passing for the wrong reason rather than merely flaking. `agents: pressing a row looks different from hovering it` and `mcp servers: the row and its expand control behave as one` read `getComputedStyle` in the same tick as the `mouse.down()` that was supposed to change it. antd transitions a table cell over 200ms, so — measured on an idle machine — the value immediately after the press is still `rgba(0, 0, 0, 0)` and only reaches `rgba(109, 40, 217, 0.3)` some 400ms later. The "a press must not look like a hover" assertion was therefore comparing two unpainted frames and passing because they happened to differ; the same read under load returned the same colour twice and failed. A claim that something changes now polls until it does, and a claim that nothing changes waits the transition out first — there is no event for a transition that never starts, which is why the second is a duration. `helpers/style` carries the measurements. Both assertions were checked against a deliberately broken `:active` rule afterwards, so polling has not made them unfalsifiable. Two more measured the machine and reported it as the app. Both substrate polling tests counted re-reads inside a fixed window with no margin: three within 2.2s at a half-second rate, which allows four, and two within 2.2s at a one-second rate, which allows exactly two. One late tick failed either. They wait for the re-reads now. The cadence itself is deliberately not asserted — it cannot be, from outside, without also asserting the hardware — and the exact end of the claim is kept where it belongs: at zero the timer must not fire at all, and "never" does not depend on how fast anything is. The fifth was budget, not logic: `Test timeout of 30000ms exceeded` partway through a six-step journey that had done nothing wrong. The mock suite gets sixty seconds. `expect.timeout` stays at its default five, so a genuinely missing element is still found quickly and a real failure does not sit here spending the larger budget. One remains unattributed — a share link taking longer than fifteen seconds to appear on Firefox under twelve workers. It is starvation rather than a race, and `retries` stays at two deliberately: raising concurrency without keeping the net would trade a slow suite for a suite that fails on other people's pull requests. CI now takes a share of the runner rather than a count. `ubuntu-latest` is four cores, where a flat number would thrash with two engines running, and a bigger runner should get the benefit without another edit. Two `list-filters` tests are gone, and no coverage with them. `FilterBar.test.tsx` already owns the bar's own behaviour in eleven cases — a pill per chosen value, a pill removing only its own filter, the last pill taking the parameter with it, clear dropping the lot, the term reaching the address — and those claims were being made a second time in a browser, in two engines, at a page load each. What is left is what a jsdom test cannot say: that a page wired the bar to its own read, that the view survives a real reload, that a genuinely server-side filter is sent to the server, and that two writes in one tick do not lose one of each other. The file's own header said the pages carried a note about where narrowing happens; those notes were removed in the previous commit, so it said something untrue and now describes what is there. Also reworded the agents list note, which read as though the configurations rather than the agents were available. Signed-off-by: Nicholas Bucher --- ui/playwright.config.ts | 46 ++++- ui/playwright/helpers/style.ts | 47 +++++ ui/playwright/tests/agents/agents.spec.ts | 23 ++- .../tests/lists/list-filters.spec.ts | 164 +++++------------- .../tests/mcp-servers/row-interaction.spec.ts | 46 +++-- .../tests/substrate/substrate-polling.spec.ts | 55 ++++-- ui/src/pages/AgentsPage.tsx | 4 +- 7 files changed, 212 insertions(+), 173 deletions(-) create mode 100644 ui/playwright/helpers/style.ts diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 29563f92a..f9894d85e 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -109,12 +109,48 @@ export default defineConfig({ fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, + /* + * Parallel in CI too, at a share of the runner rather than a count. + * + * It was one worker, and that was buying real stability: at high concurrency this + * suite produced roughly one failure per two full runs, always a different test. + * Two of those were the suite's own fault and are fixed — see `helpers/style` for + * the press-state reads that compared unpainted frames. The rest were starvation + * rather than logic: a mock-backed click that normally settles in under a second + * timing out at fifteen, because two dev servers and two browser engines on one + * machine leave the servers transforming modules for whoever asks first. + * + * A share, not a number, because that failure mode is about how much machine there + * is: `ubuntu-latest` is four cores, where a flat `4` would thrash with two engines + * running, and a runner that grows should get the benefit without another edit. + * + * `retries` stays at two deliberately. Raising concurrency without keeping the net + * would trade a slow suite for a suite that fails on other people's pull requests, + * and the starvation above is not fixed — only made less likely by asking for less + * of the machine than the local runs that provoked it. + */ + workers: process.env.CI ? "50%" : undefined, reporter: process.env.CI ? "github" : "list", - // A real backend behind a port-forward answers in tens of seconds where the - // in-browser mock answers in milliseconds, so the defaults that suit the mock - // suite are too tight to distinguish "slow cluster" from "broken page". - ...(LIVE ? { timeout: 120_000, expect: { timeout: 30_000 } } : {}), + /* + * A real backend behind a port-forward answers in tens of seconds where the + * in-browser mock answers in milliseconds, so the defaults that suit the mock + * suite are too tight to distinguish "slow cluster" from "broken page". + * + * The mock suite gets sixty rather than the default thirty, which is about the + * machine and not about the app. Several of these tests are journeys — six steps, + * a page load in most of them — and thirty seconds is a comfortable budget on an + * idle laptop and a tight one on a runner sharing itself with another engine. The + * failure it produced said `Test timeout of 30000ms exceeded` partway through a + * step that had done nothing wrong, which is a report about contention dressed as + * a defect. + * + * `expect.timeout` is deliberately left at its default. That is the one that keeps + * a genuinely missing element fast to find: an assertion still gives up in five + * seconds, so a real failure does not sit here spending the larger budget. + */ + ...(LIVE + ? { timeout: 120_000, expect: { timeout: 30_000 } } + : { timeout: 60_000 }), use: { trace: "on-first-retry", screenshot: "only-on-failure", diff --git a/ui/playwright/helpers/style.ts b/ui/playwright/helpers/style.ts new file mode 100644 index 000000000..a2c93eb42 --- /dev/null +++ b/ui/playwright/helpers/style.ts @@ -0,0 +1,47 @@ +import type { Locator } from "@playwright/test"; + +/** + * Reading colours that are still on their way somewhere. + * + * antd transitions a table cell's background over 200ms, so `getComputedStyle` taken in + * the same tick as the hover or press that triggered it returns the colour being left + * rather than the colour being reached. Measured on an idle machine: `transition: + * background-color 0.2s`, at rest `rgba(0, 0, 0, 0)`, immediately after `mouse.down()` + * still `rgba(0, 0, 0, 0)`, and only 400ms later the pressed `rgba(109, 40, 217, 0.3)`. + * + * Sampled once, that produced two different untruths. A test asserting the colour + * *changed* was comparing two unpainted frames, and passed only because they happened + * to differ for an unrelated reason — then failed outright under a full parallel run, + * which is how this was found. A test asserting the colour did *not* change passed + * without ever having looked at the state it was about. + * + * So a claim that something changes polls until it does, and a claim that nothing + * changes waits the transition out first. There is no event for a transition that + * never starts, which is why the second one is a duration and not a wait for a signal. + */ + +/** Comfortably longer than the 200ms transition, with a frame to paint in. */ +const SETTLE_MS = 350; + +/** What the element looks like right now, mid-transition or not. */ +export function paint(locator: Locator) { + return locator.evaluate((el) => { + const style = getComputedStyle(el); + return { + background: style.backgroundColor, + shadow: style.boxShadow, + colour: style.color, + }; + }); +} + +/** What the element looks like once it has stopped moving. */ +export async function settledPaint(locator: Locator) { + await locator.page().waitForTimeout(SETTLE_MS); + return paint(locator); +} + +/** Just the background, for polling towards a colour. */ +export function background(locator: Locator) { + return locator.evaluate((el) => getComputedStyle(el).backgroundColor); +} diff --git a/ui/playwright/tests/agents/agents.spec.ts b/ui/playwright/tests/agents/agents.spec.ts index 7e750050a..ed29bcd47 100644 --- a/ui/playwright/tests/agents/agents.spec.ts +++ b/ui/playwright/tests/agents/agents.spec.ts @@ -8,6 +8,7 @@ import { rowNamed, routes, } from "../../helpers/app"; +import { background, settledPaint } from "../../helpers/style"; /** * Agents — what can be run, and what each one is. @@ -316,19 +317,27 @@ test("agents: pressing a row looks different from hovering it", async ({ page }) const row = page.locator("tbody tr.clickable-table-row").first(); await expect(row).toBeVisible({ timeout: 30_000 }); - const background = () => - row.locator("td").first().evaluate((cell) => getComputedStyle(cell).backgroundColor); + const cell = row.locator("td").first(); await row.hover(); - const hovered = await background(); + // Settled, so this really is the hover colour. Read in the same tick it would be + // the at-rest colour, and the assertion below would be comparing the pressed state + // against the wrong thing entirely — which is what it was doing. + const hovered = (await settledPaint(cell)).background; const box = await row.boundingBox(); await page.mouse.move(box!.x + 30, box!.y + 10); await page.mouse.down(); - const pressed = await background(); - await page.mouse.up(); - - expect(pressed, "a press must not look like a hover").not.toBe(hovered); + try { + // Polled: the cell transitions over 200ms, so the first read after `mouse.down` + // is still the colour being left. See `helpers/style` for the measurements. + await expect + .poll(() => background(cell), { message: "a press must not look like a hover" }) + .not.toBe(hovered); + } finally { + // Released even on a failure, so a held button cannot affect later tests. + await page.mouse.up(); + } }); /** diff --git a/ui/playwright/tests/lists/list-filters.spec.ts b/ui/playwright/tests/lists/list-filters.spec.ts index 7cf236b17..35ea8e7cd 100644 --- a/ui/playwright/tests/lists/list-filters.spec.ts +++ b/ui/playwright/tests/lists/list-filters.spec.ts @@ -3,25 +3,25 @@ import { test, expect } from "../../fixtures/test"; import { dataRows, expectSettled, loadPage, rowNamed, routes } from "../../helpers/app"; /** - * The filter bar, the URL state behind it, and what the three list pages claim about - * where their narrowing happens. + * The filter bar as the list pages actually use it. * - * These are the properties the shared machinery exists for, and each of them replaces - * something a page was previously doing worse: + * Deliberately not the bar's own behaviour. `FilterBar.test.tsx` owns that — eleven + * cases covering a pill per chosen value, a pill removing only its own filter, the last + * pill taking the parameter with it, clear dropping the lot, the term reaching the + * address, and a filter change returning to page one. Those claims were asserted here + * too for a while, which bought nothing and cost two browser engines and a page load + * each. What is left is what a jsdom test cannot reach: * - * - **Selecting no namespaces means every namespace.** The old pattern was a separate - * "all namespaces" toggle beside a single-select — two controls answering one - * question, able to disagree. An empty multi-select has one state. - * - **The choices are visible without opening the control.** A trigger reading - * "2 selected" hides which two, and "why can I not see the row I am looking for" is - * the question a filtered list most often provokes. - * - **The view is in the address**, so it can be linked to and survives a reload. - * - **The pages say where the narrowing happens.** All three of these RPCs return the - * whole list — `ListModelConfigs` and `ListToolServers` take an empty request, and - * `ListPromptTemplates` takes only a namespace — so a search in the browser searches - * every row. That is the fact the note on each page states, and it is what makes a - * client-side control honest here where it would not be on the substrate page's - * paged tables. + * - **A page narrows its own rows.** The bar can be rendered perfectly and never + * passed to the read behind the table, and only real data in a real table shows it. + * - **The view is in the address**, so it can be linked to and survives a real reload — + * a reload being the thing under test, which rules out simulating it. + * - **A filter that is genuinely the server's is sent there.** `ListPromptTemplates` + * takes a namespace and refuses a request without one, so choosing namespaces on + * that page is one call each rather than a narrowing of something already fetched. + * Whether a call happened is not observable from the component. + * - **Two writes in one tick do not lose one of each other**, which needs the router + * and the page together. * * Driving the antd multi-select needs one piece of local knowledge, which is why it * has a helper: rc-select renders a *second*, invisible `role="listbox"` for screen @@ -39,131 +39,49 @@ async function chooseFilter(page: Page, filterTestId: string, label: string) { await page.keyboard.press("Escape"); } -test("lists: no namespaces chosen means every namespace, and each choice becomes a pill", async ({ - page, -}) => { - await test.step("1. the page opens unnarrowed, with no pills at all", async () => { +test("lists: a page's filter narrows that page's own rows", async ({ page }) => { + /* + * What the browser is needed to say, and nothing more. + * + * The bar's own mechanics — a pill per chosen value, a pill removing only its own + * filter, the last pill taking the parameter with it, clear dropping everything, the + * term reaching the address — are `FilterBar.test.tsx`, eleven cases of it, and they + * used to be re-asserted here as well: the same claims a second time, in two browser + * engines, at a page load each. What a unit test cannot say is that *this page* + * wired the bar to its own read, so that is what is left. + */ + await test.step("1. unnarrowed is every row, and nothing claims otherwise", async () => { await loadPage(page, routes.models, { title: "Models" }); await expectSettled(page); // Four configurations across three namespaces — the whole fixture set, which is - // what "nothing selected" has to mean. A control that read an empty selection as + // what "nothing selected" has to mean. A control reading an empty selection as // "narrow to nothing" would show an empty table here. await expect(dataRows(page)).toHaveCount(4); await expect(page.getByTestId("models-filters-pills")).toHaveCount(0); - await expect(page.getByTestId("models-filters-pill-clear")).toHaveCount(0); }); - await test.step("2. choosing one namespace narrows the list and raises one pill", async () => { + await test.step("2. choosing a namespace narrows the rows, and says so in the address", async () => { await chooseFilter(page, "models-filters-filter-ns", "kagent"); - await expect(page.getByTestId("models-filters-pill-ns-kagent")).toContainText( - "Namespace: kagent", - ); - await expect(rowNamed(page, "default-model-config")).toHaveCount(1); - await expect(rowNamed(page, "ollama-local")).toHaveCount(0); + // The wiring, in one assertion: the page's own rows respond. A page that rendered + // the bar and never passed the selection to its read would keep all four. await expect(dataRows(page)).toHaveCount(2); - }); - - await test.step("3. a second namespace adds to the first rather than replacing it", async () => { - // The whole reason for a multi-select. A single-select answers "which one - // namespace"; a reader comparing two namespaces has to be able to ask for both. - await chooseFilter(page, "models-filters-filter-ns", "platform"); - - await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); - await expect(page.getByTestId("models-filters-pill-ns-platform")).toBeVisible(); - await expect(dataRows(page)).toHaveCount(3); - await expect(page.getByTestId("models-summary")).toContainText("3 of 4"); - }); - - await test.step("4. a second filter narrows further and keeps its own pill", async () => { - // Two different filters at once is what the bar takes definitions for. A - // component built around namespaces could not do this at all. - await chooseFilter(page, "models-filters-filter-provider", "OpenAI"); - - await expect(page.getByTestId("models-filters-pill-provider-OpenAI")).toContainText( - "Provider: OpenAI", - ); - await expect(dataRows(page)).toHaveCount(1); - await expect(rowNamed(page, "default-model-config")).toHaveCount(1); - }); - - await test.step("5. clicking a pill removes that filter and leaves the rest alone", async () => { - await page.getByTestId("models-filters-pill-provider-OpenAI").click(); - - await expect(page.getByTestId("models-filters-pill-provider-OpenAI")).toHaveCount(0); - // The two namespace pills are untouched: removing one choice is not a reset. await expect(page.getByTestId("models-filters-pill-ns-kagent")).toBeVisible(); - await expect(page.getByTestId("models-filters-pill-ns-platform")).toBeVisible(); - await expect(dataRows(page)).toHaveCount(3); + await expect(page).toHaveURL(/ns=kagent/); }); - await test.step("6. the last pill of a filter takes the parameter with it", async () => { + await test.step("3. a term and a filter chosen in quick succession both survive", async () => { /* - * Removed back to back, with no wait between them. - * - * Each removal used to compute the remainder from the render it was drawn in, so - * two clicks landing in the same frame both worked from the same list: the second - * wrote back the namespace the first had just taken out, and a pill survived being - * clicked. `no wait` is the assertion — pausing here would pass either way. + * A regression this build actually had, and the one thing here no unit test + * reaches: the two writes landed before React had re-rendered, so the second read + * the address as it was before the first and put the cleared filter straight back + * — a filter that would not clear, and a search reporting no matches for a row + * plainly on the page. Driven without waiting in between, because waiting is what + * hid it. */ - await page.getByTestId("models-filters-pill-ns-platform").click({ noWaitAfter: true }); - await page.getByTestId("models-filters-pill-ns-kagent").click({ noWaitAfter: true }); - - // Not `?ns=`, which would read as "narrowed to nothing" — the address has to - // become the address of the unfiltered list again, or a link to "everything" and a - // link to "nothing" would look the same. - await expect(page).not.toHaveURL(/ns=/); - await expect(page.getByTestId("models-filters-pills")).toHaveCount(0); - await expect(dataRows(page)).toHaveCount(4); - }); -}); - -test("lists: the search term is a filter too, and clearing means everything", async ({ - page, -}) => { - await test.step("1. models has a search box, which it did not before", async () => { - // The page's only way to find a configuration used to be reading the table. await loadPage(page, routes.models, { title: "Models" }); await expectSettled(page); - await expect(page.getByTestId("models-filters-search")).toBeVisible(); - }); - - await test.step("2. a term narrows the list and appears as its own pill", async () => { - await page.getByTestId("models-filters-search").fill("haiku"); - - // Matched on the model rather than the name, which is the point of searching - // every column the row displays. - await expect(rowNamed(page, "bedrock-haiku")).toHaveCount(1); - await expect(dataRows(page)).toHaveCount(1); - await expect(page.getByTestId("models-filters-pill-search")).toContainText( - "Search: haiku", - ); - }); - - await test.step("3. clear filters drops the term and every filter at once", async () => { - await chooseFilter(page, "models-filters-filter-ns", "analytics"); - await expect(page.getByTestId("models-filters-pill-ns-analytics")).toBeVisible(); - - await page.getByTestId("models-filters-pill-clear").click(); - - // Everything: the term as well as the namespace. A term left in the box is the - // filter a reader is most likely to have forgotten, so a control that cleared the - // pills and left it would still be hiding rows. - await expect(page.getByTestId("models-filters-pills")).toHaveCount(0); - await expect(page.getByTestId("models-filters-search")).toHaveValue(""); - await expect(dataRows(page)).toHaveCount(4); - await expect(page).toHaveURL(/\/models\?mock=ok$/); - }); - - await test.step("4. a term and a filter chosen in quick succession both survive", async () => { - /* - * A regression this build actually had. The two writes landed before React had - * re-rendered, so the second read the address as it was before the first and put - * the cleared filter straight back — a filter that would not clear, and a search - * reporting no matches for a row plainly on the page. Driven without waiting in - * between, because waiting is what hid it. - */ await page.getByTestId("models-filters-search").fill("model"); await chooseFilter(page, "models-filters-filter-ns", "kagent"); diff --git a/ui/playwright/tests/mcp-servers/row-interaction.spec.ts b/ui/playwright/tests/mcp-servers/row-interaction.spec.ts index 8572f3285..f981c6fbf 100644 --- a/ui/playwright/tests/mcp-servers/row-interaction.spec.ts +++ b/ui/playwright/tests/mcp-servers/row-interaction.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "../../fixtures/test"; +import { background, paint, settledPaint } from "../../helpers/style"; /** * One row, one affordance. @@ -23,41 +24,48 @@ test("mcp servers: the row and its expand control behave as one", async ({ page const icon = row.locator(".ant-table-row-expand-icon"); await expect(icon).toBeVisible(); - const styles = (locator: typeof icon) => - locator.evaluate((el) => { - const cs = getComputedStyle(el); - return { background: cs.backgroundColor, shadow: cs.boxShadow, colour: cs.color }; - }); - await test.step("1. hovering the control adds nothing of its own", async () => { - const atRest = await styles(icon); + const atRest = await paint(icon); await icon.hover(); - expect(await styles(icon)).toEqual(atRest); + // Settled before comparing, not read in the same tick: a colour that has not + // started moving yet matches the colour it started from, so an unsettled read + // would report "nothing changed" about a state it never saw. See `helpers/style`. + expect(await settledPaint(icon)).toEqual(atRest); }); await test.step("2. pressing it adds nothing of its own either", async () => { - const atRest = await styles(icon); + const atRest = await paint(icon); const box = await icon.boundingBox(); await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2); await page.mouse.down(); - const pressed = await styles(icon); - await page.mouse.up(); - - expect(pressed.background).toBe(atRest.background); - expect(pressed.shadow).toBe(atRest.shadow); + try { + const pressed = await settledPaint(icon); + expect(pressed.background).toBe(atRest.background); + expect(pressed.shadow).toBe(atRest.shadow); + } finally { + // Released even on a failure: a button left held poisons every step after it. + await page.mouse.up(); + } }); await test.step("3. the row itself does respond to being pressed", async () => { const cell = row.locator("td").first(); - const before = await cell.evaluate((el) => getComputedStyle(el).backgroundColor); + const before = await background(cell); const box = await cell.boundingBox(); await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height / 2); await page.mouse.down(); - const during = await cell.evaluate((el) => getComputedStyle(el).backgroundColor); - await page.mouse.up(); - - expect(during, "a pressed row must look pressed").not.toBe(before); + try { + // Polled rather than sampled once. The cell transitions over 200ms, so the + // first read is reliably still the colour it is leaving — this assertion used + // to pass only because that colour differed from the *hover* one it was being + // compared against, and it failed outright under a full parallel run. + await expect + .poll(() => background(cell), { message: "a pressed row must look pressed" }) + .not.toBe(before); + } finally { + await page.mouse.up(); + } }); await test.step("4. and clicking anywhere on it still expands the server", async () => { diff --git a/ui/playwright/tests/substrate/substrate-polling.spec.ts b/ui/playwright/tests/substrate/substrate-polling.spec.ts index 67fb33f7e..0888c3872 100644 --- a/ui/playwright/tests/substrate/substrate-polling.spec.ts +++ b/ui/playwright/tests/substrate/substrate-polling.spec.ts @@ -78,18 +78,24 @@ test("substrate: polling is off until asked for, then re-reads the inventory", a await page.getByTestId("substrate-poll-toggle").click(); await expect(page.getByTestId("substrate-poll-toggle")).toContainText("enabled"); - await page.waitForTimeout(2_200); - const during = await readCounts(page); - - // Two reads in 2.2s at the default of one second, allowing for the first tick - // landing late. - expect( - during[POLLED] - before[POLLED], - "the inventory should be re-read while polling", - ).toBeGreaterThanOrEqual(2); + /* + * Waited for, not counted inside a window. + * + * This asked for two reads within 2.2 seconds, which at the default of one second + * allows exactly two — no margin at all, so a single tick landing late failed it. + * Under a parallel run a late tick is ordinary. The claim is that polling re-reads + * repeatedly; how quickly it gets there is the machine's business, and step 1 and + * step 4 are what pin down the other end, where "must not re-read" is exact. + */ + await expect + .poll(async () => (await readCounts(page))[POLLED] - before[POLLED], { + timeout: 15_000, + message: "the inventory should be re-read while polling", + }) + .toBeGreaterThanOrEqual(2); expect( - during[NOT_POLLED], + (await readCounts(page))[NOT_POLLED], "the namespace list is the scope control, not the data — polling must leave it alone", ).toBe(before[NOT_POLLED]); }); @@ -134,16 +140,31 @@ test("substrate: the polling interval is the reader's, and zero stops it", async await expect(page.getByTestId("substrate-poll-interval")).toContainText("second"); }); - await test.step("3. a faster rate is read faster", async () => { + await test.step("3. the rate the reader set is the rate it re-reads at", async () => { await interval.fill("0.5"); await interval.blur(); const before = await readCounts(page); - await page.waitForTimeout(2_200); - const during = await readCounts(page); - expect( - during[POLLED] - before[POLLED], - "half a second should re-read more often than a second", - ).toBeGreaterThanOrEqual(3); + + /* + * Waited for rather than counted inside a fixed window. + * + * This asked for three re-reads within 2.2 seconds, which at half a second allows + * four — so one stalled tick failed it, and under a parallel run one stalled tick + * is ordinary. It was measuring how fast the machine could serve four reads, and + * reporting a busy laptop as a broken timer. + * + * The claim kept is the one about the app: the timer fires repeatedly at the rate + * it was given, rather than once. The exact cadence is deliberately not asserted — + * it cannot be, from outside, without also asserting the hardware. Step 5 is what + * holds the other end down: at zero it must not fire at all, and that one is + * exact because "never" does not depend on how fast anything is. + */ + await expect + .poll(async () => (await readCounts(page))[POLLED] - before[POLLED], { + timeout: 15_000, + message: "polling at half a second should re-read repeatedly", + }) + .toBeGreaterThanOrEqual(3); }); await test.step("4. below the floor is read as the floor, not refused", async () => { diff --git a/ui/src/pages/AgentsPage.tsx b/ui/src/pages/AgentsPage.tsx index a1d3cfbf6..516b604da 100644 --- a/ui/src/pages/AgentsPage.tsx +++ b/ui/src/pages/AgentsPage.tsx @@ -383,8 +383,8 @@ export function AgentsTab() { answer to "why is there no New agent button", and it is wanted by somebody looking at the list rather than by somebody reading the model. */} - The agents list is populated automatically from the template and harness - configurations available. + The agents list is populated automatically from the available template and harness + configurations. {/* From 062d679d73f54764c6505fa5144c3e8afb19d405 Mon Sep 17 00:00:00 2001 From: Nicholas Bucher Date: Wed, 26 Aug 2026 11:11:58 -0400 Subject: [PATCH 05/25] fix(ui): name a message the same thing on every read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported as a defect: in a conversation several turns long, sending a message and then clicking away from the browser and back put an earlier agent reply underneath the newest one. It took a couple of attempts to provoke and a refresh cleared it, which made it look like a rendering fault. It was not. The gateway does not name an agent reply, and `messagesFromTask` gave an unnamed one an id from a process-wide counter. So the same reply came back as `history-1` on one read and `history-3` on the next. `useLiveTranscript` re-reads the transcript on `visibilitychange` — and every four seconds while the tab is visible — and the merge treats the id as identity, so on every re-read the copy already on screen stopped matching what the server sent and was kept as a *local addition*. Local additions are appended after the server's messages, on the reasoning that the only way to have one is to have just sent it. A reload dropped the local copy and reset the counter together, which is exactly why refreshing put it right. Ids for unnamed messages and artifacts are derived from the task and the position in it now. Position is stable for the same payload and unaffected by the task gaining later messages, which is all the merge needs. The counter stays where it belongs: on the stream, where an id is minted once for a message being written and upserted under it. The test asserts two reads of the *same* task, because one read cannot show this. Without the fix it reports `['m1', 'history-1', 'artifact-2']` becoming `['m1', 'history-3', 'artifact-4']` — the named message stable, the unnamed ones renamed underneath it. Signed-off-by: Nicholas Bucher --- ui/src/api/chat/a2aGrpcChatClient.test.ts | 43 +++++++++++++++++++++++ ui/src/api/chat/a2aGrpcChatClient.ts | 21 +++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/ui/src/api/chat/a2aGrpcChatClient.test.ts b/ui/src/api/chat/a2aGrpcChatClient.test.ts index 05d0d1dd5..efef8ddd1 100644 --- a/ui/src/api/chat/a2aGrpcChatClient.test.ts +++ b/ui/src/api/chat/a2aGrpcChatClient.test.ts @@ -618,6 +618,49 @@ describe("A2AGrpcChatClient.history", () => { expect(messages).toHaveLength(1); }); + it("names a message the same thing on a re-read, so a merge can recognise it", async () => { + /* + * Reported as a defect: in a conversation several turns long, tabbing away and + * back put an earlier agent reply underneath the newest one, and a refresh + * cleared it. + * + * The gateway does not name an agent reply, and the id for an unnamed one came + * from a process-wide counter — so the same reply was `history-1` on one read and + * `history-2` on the next. `useLiveTranscript` re-reads on `visibilitychange`, + * the transcript merge treats the id as identity, and a copy that no longer + * matched was kept as a local addition and appended after everything the server + * sent. A reload dropped the copy and reset the counter together, which is why it + * looked like a rendering fault rather than a merge. + * + * Asserted across two reads of the *same* task, because one read cannot show it. + */ + const task = { + id: "task-1", + contextId: CONVERSATION.id, + status: { state: TaskState.COMPLETED, timestamp: { seconds: 1767225600n } }, + history: [ + { messageId: "m1", role: Role.USER, parts: [text("how many pods?")] }, + // Unnamed, as an agent reply arrives. + { role: Role.AGENT, parts: [text("3 pods")] }, + ], + artifacts: [{ parts: [text("and one pending")] }], + }; + + serveTasks([task]); + const first = await new A2AGrpcChatClient().history(CONVERSATION); + serveTasks([task]); + const second = await new A2AGrpcChatClient().history(CONVERSATION); + + expect(second.messages.map((message) => message.id)).toEqual( + first.messages.map((message) => message.id), + ); + // And the derived ids are still distinct from each other, or the merge would + // collapse two different messages into one. + expect(new Set(first.messages.map((message) => message.id)).size).toBe( + first.messages.length, + ); + }); + it("drops an artifact repeating text already in the history", async () => { serveTasks([ { diff --git a/ui/src/api/chat/a2aGrpcChatClient.ts b/ui/src/api/chat/a2aGrpcChatClient.ts index c6c520198..c6dd37b3e 100644 --- a/ui/src/api/chat/a2aGrpcChatClient.ts +++ b/ui/src/api/chat/a2aGrpcChatClient.ts @@ -741,7 +741,22 @@ export function messagesFromTask(task: A2ATask): ChatMessage[] { if (taken.has(identity)) return; taken.add(identity); messages.push({ - id: message.messageId || nextId("history"), + /* + * Derived from the task and the position in it, never from a counter. + * + * The gateway does not name an agent reply, so this is the branch most replies + * take — and it used to take a process-wide counter, which meant the same reply + * came back as `history-1` on one read and `history-2` on the next. The + * transcript merge treats the id as identity, so on every re-read the copy + * already on screen stopped matching and was kept as a local extra, appended + * after everything the server sent: a reply from three turns ago reappearing + * under the newest one, and a refresh — which drops the local copy and the + * counter together — putting it right. Focus was enough to trigger it. + * + * Position within the task is stable for the same payload and unaffected by the + * task gaining later messages, which is all the merge needs. + */ + id: message.messageId || `${task.id || "task"}-message-${messages.length}`, role: message.role === Role.AGENT ? "agent" : "user", parts, createdAt, @@ -760,7 +775,9 @@ export function messagesFromTask(task: A2ATask): ChatMessage[] { const body = textOf(parts); if (parts.length === 0 || (body !== "" && shown.has(body))) continue; messages.push({ - id: artifact.artifactId || nextId("artifact"), + // Derived, for the reason given against the message id above: an unnamed + // artifact renamed on every read is an artifact the merge cannot recognise. + id: artifact.artifactId || `${task.id || "task"}-artifact-${messages.length}`, role: "agent", parts, createdAt, From 21595c1862a80e6959042d00bc6090046304c65c Mon Sep 17 00:00:00 2001 From: Nicholas Bucher Date: Wed, 26 Aug 2026 11:11:58 -0400 Subject: [PATCH 06/25] feat(ui): put the newest conversation at the top of the agent rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rail rendered in whatever order `ListAgentInstances` answered in, which is an order in no particular order — so a conversation started a minute ago could sit anywhere in the list. By when a conversation was **started**, and deliberately not by when it was last spoken in, which is the more useful ordering and is not available here. `AgentInstance.updatedAt` looks like the field for it and is not: `agent_instance` carries no timestamp columns at all, the two on the message come out of the row's serialized blob, and `UpdatedAt` is written in exactly two places: when the instance is created, and when it goes `CREATING` to `READY`. Sending a message never touches it, so ordering by it would be creation order wearing a better name. The signal that would answer it is `agent_instance_task.updated_at`, bumped on every task upsert. It is on the task table and `ListAgentInstances` does not return it, and reaching it from the browser costs one `ListTasks` per row — which is why `useConversationTitles` budgets thirty of those for titles alone. When the read grows a last-activity timestamp, the comparator is the one thing that needs to change; the note against it says so. Ties break on the id, so equal timestamps give one fixed order rather than whatever the sort happened to do with them. A rail that reshuffled equal rows between reads would be the same defect arriving by a different door. Two things this shook out. The suspended fixture's timestamp is now load-bearing: it is older than its named sibling and renders first when nothing sorts, so the new test cannot pass against an unsorted rail — checked by removing the sort. And the delete test was targeting `.first()`, an unstated dependency on render order: sorting made it the *open* conversation, and deleting the conversation you are looking at navigates away, so the test counted rows on a page it had left. It names the sibling now, which is the row it was always about — one that goes without taking the page with it. Signed-off-by: Nicholas Bucher --- ui/playwright/tests/chat/agent-rail.spec.ts | 56 ++++++++++++++++++- .../agent-instances/conversationOrder.test.ts | 42 ++++++++++++++ .../agent-instances/conversationOrder.ts | 31 ++++++++++ ui/src/components/agent/AgentRail.tsx | 21 ++++--- ui/src/mocks/fixtures.ts | 8 +++ 5 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 ui/src/components/agent-instances/conversationOrder.test.ts create mode 100644 ui/src/components/agent-instances/conversationOrder.ts diff --git a/ui/playwright/tests/chat/agent-rail.spec.ts b/ui/playwright/tests/chat/agent-rail.spec.ts index 0ce891590..f54f815ae 100644 --- a/ui/playwright/tests/chat/agent-rail.spec.ts +++ b/ui/playwright/tests/chat/agent-rail.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from "../../fixtures/test"; -import { agentChat, agentDetail, agentPage, agents, instances } from "../../helpers/app"; +import { + agentChat, + agentDetail, + agentPage, + agents, + instances, + SIBLING_OF_READY, +} from "../../helpers/app"; /** * The agent rail — the navigation for when you are inside one agent. @@ -75,8 +82,19 @@ test("agent rail: a conversation is deleted from a menu, on every surface", asyn const item = page.getByRole("menuitem", { name: "Delete chat" }); + /* + * A named row, and deliberately not "the first one". + * + * `.first()` was an unstated dependency on the order the rail happened to render in. + * Once the rail sorted newest-first that became the *open* conversation, and deleting + * the conversation you are looking at navigates away — so the rail went with it and + * this test failed counting rows on a page it had left. The sibling is the row this + * was always about: one that goes without taking the page with it. + */ + const sibling = `[data-testid="chat-session-menu-${SIBLING_OF_READY}"]`; + await test.step("1. the menu offers it, and the row is otherwise quiet", async () => { - const menu = rail.locator('[data-testid^="chat-session-menu-"]').first(); + const menu = rail.locator(sibling); // Present for a pointer to find, but not drawn until the row is hovered. await expect(menu).toHaveCSS("opacity", "0"); await menu.click({ force: true }); @@ -99,7 +117,7 @@ test("agent rail: a conversation is deleted from a menu, on every surface", asyn await test.step("3. and Delete removes exactly one", async () => { // The dialog animates out, and a click while it is still there lands on its mask. await expect(page.locator(".ant-modal:visible")).toHaveCount(0); - await page.locator('[data-testid^="chat-session-menu-"]').first().click({ force: true }); + await page.locator(sibling).click({ force: true }); await page.waitForTimeout(400); await page.getByRole("menuitem", { name: "Delete chat" }).click(); await page.locator(".ant-modal:visible").getByRole("button", { name: "Delete" }).click(); @@ -507,3 +525,35 @@ test("agent rail: several conversations can be picked and deleted together", asy await expect(confirm).toHaveCount(0); }); }); + +test("agent rail: the newest conversation is at the top", async ({ page }) => { + /* + * The rail rendered in whatever order `ListAgentInstances` answered in, which is an + * order in no particular order — so a conversation started a minute ago could sit + * anywhere in the list. + * + * By when it was *started*, not when it was last spoken in. The latter is the more + * useful ordering and is not available: `AgentInstance.updatedAt` is written when the + * instance is created and when it goes `CREATING` -> `READY`, and never when a message + * is sent, so ordering by it would be creation order wearing a better name. The + * comparator carries the note, and `conversationOrder.test.ts` carries the cases. + * + * The fixtures are arranged against this deliberately: unsorted, this rail renders + * the *older* suspended sibling first, so a rail that sorts has to move it and a + * rail that does not cannot accidentally pass. Asserted on ids rather than titles, + * because a title is derived and a row's identity is not. + */ + await page.goto(agentChat(instances.ready)); + const rail = page.getByTestId("chat-sessions"); + const rows = rail.locator('a[data-testid^="chat-session-"]'); + await expect(rows.first()).toBeVisible({ timeout: 30_000 }); + + await expect(rows.first()).toHaveAttribute( + "data-testid", + `chat-session-${instances.ready}`, + ); + await expect(rows.nth(1)).toHaveAttribute( + "data-testid", + `chat-session-${instances.suspended}`, + ); +}); diff --git a/ui/src/components/agent-instances/conversationOrder.test.ts b/ui/src/components/agent-instances/conversationOrder.test.ts new file mode 100644 index 000000000..ad9e054bc --- /dev/null +++ b/ui/src/components/agent-instances/conversationOrder.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import type { AgentInstance } from "@/api"; +import { byNewestFirst } from "./conversationOrder"; + +/** Only the fields the comparator reads; the rest of an instance is irrelevant here. */ +const at = (id: string, createdAt: string) => ({ id, createdAt }) as AgentInstance; + +describe("byNewestFirst", () => { + it("puts the most recently started conversation at the top", () => { + const rows = [ + at("older", "2026-08-11T16:40:00Z"), + at("newest", "2026-08-21T07:55:00Z"), + at("middle", "2026-08-18T09:12:00Z"), + ]; + + expect([...rows].sort(byNewestFirst).map((row) => row.id)).toEqual([ + "newest", + "middle", + "older", + ]); + }); + + it("gives equal timestamps one fixed order rather than whatever the sort did", () => { + // A rail that reshuffled equal rows between reads is the defect this removes, so + // the tie-break is part of the behaviour and not an implementation detail. + const same = "2026-08-18T09:12:00Z"; + const one = [at("b", same), at("a", same), at("c", same)]; + const other = [at("c", same), at("b", same), at("a", same)]; + + expect([...one].sort(byNewestFirst).map((r) => r.id)).toEqual(["a", "b", "c"]); + expect([...other].sort(byNewestFirst).map((r) => r.id)).toEqual(["a", "b", "c"]); + }); + + it("does not throw a conversation with no timestamp out of the list", () => { + // `createdAt` is empty on at least one fixture, and a comparator that treated that + // as unorderable would drop the row or move it unpredictably. It sorts last, which + // is where a conversation with no known start belongs. + const rows = [at("undated", ""), at("dated", "2026-08-18T09:12:00Z")]; + + expect([...rows].sort(byNewestFirst).map((r) => r.id)).toEqual(["dated", "undated"]); + }); +}); diff --git a/ui/src/components/agent-instances/conversationOrder.ts b/ui/src/components/agent-instances/conversationOrder.ts new file mode 100644 index 000000000..cb973a235 --- /dev/null +++ b/ui/src/components/agent-instances/conversationOrder.ts @@ -0,0 +1,31 @@ +import type { AgentInstance } from "@/api"; + +/** + * Newest conversation first. + * + * The rail used to render in whatever order `ListAgentInstances` answered in, which is + * an order in no particular order — so the conversation somebody started a minute ago + * could sit anywhere in the list. + * + * By when it was **started**, and deliberately not by when it was last spoken in, which + * is the more useful thing and is not available here. `AgentInstance.updatedAt` looks + * like the field for it and is not: the `agent_instance` table carries no timestamp + * columns at all, the two on the message come out of the row's serialized blob, and + * `UpdatedAt` is written in exactly two places — when the instance is created and when + * it goes `CREATING` -> `READY`. Sending a message never touches it, so ordering by it + * would be creation order under a label claiming otherwise. + * + * The signal that *would* answer it is `agent_instance_task.updated_at`, bumped on every + * task upsert — on the task table, and not returned by `ListAgentInstances`. Reaching it + * from here costs one `ListTasks` per row, which is why `useConversationTitles` budgets + * thirty of them for titles alone. When the read grows a last-activity timestamp, this + * comparator is the one thing that needs to change. + * + * Ties break on the id so that equal timestamps give one fixed order rather than + * whatever the sort happened to do with them — a rail that reshuffles equal rows between + * reads is the defect this is meant to remove. + */ +export function byNewestFirst(left: AgentInstance, right: AgentInstance): number { + const when = right.createdAt.localeCompare(left.createdAt); + return when !== 0 ? when : left.id.localeCompare(right.id); +} diff --git a/ui/src/components/agent/AgentRail.tsx b/ui/src/components/agent/AgentRail.tsx index 98c8f5eb8..d5134d442 100644 --- a/ui/src/components/agent/AgentRail.tsx +++ b/ui/src/components/agent/AgentRail.tsx @@ -12,6 +12,7 @@ import { Typography, } from "antd"; import { useTheme, type Theme } from "@emotion/react"; +import { byNewestFirst } from "@/components/agent-instances/conversationOrder"; import { useConversationTitles } from "@/api/hooks/useConversationTitles"; import toast from "react-hot-toast"; import { @@ -336,15 +337,17 @@ export function AgentRail({ ) : (conversations.data ?? []); const needle = query.trim().toLowerCase(); - if (!needle) return siblings; - // The name as well as the id: a reader who titled a conversation searches for - // what they called it, and a box that only matched hex would find nothing while - // the row they wanted was on screen. - return siblings.filter( - (candidate) => - candidate.id.toLowerCase().includes(needle) || - candidate.name.toLowerCase().includes(needle), - ); + const found = !needle + ? siblings + : // The name as well as the id: a reader who titled a conversation searches for + // what they called it, and a box that only matched hex would find nothing while + // the row they wanted was on screen. + siblings.filter( + (candidate) => + candidate.id.toLowerCase().includes(needle) || + candidate.name.toLowerCase().includes(needle), + ); + return [...found].sort(byNewestFirst); }, [conversations.data, instance, query]); /* diff --git a/ui/src/mocks/fixtures.ts b/ui/src/mocks/fixtures.ts index a03f8ce6b..06dd6a256 100644 --- a/ui/src/mocks/fixtures.ts +++ b/ui/src/mocks/fixtures.ts @@ -623,6 +623,14 @@ export const mockAgentInstances: AgentInstance[] = [ a2aAuthority: "k8s-agent-b28e4f13.kagent.svc.cluster.local:8080", state: "suspended", operation: "unspecified", + /* + * Older than the named sibling above it, which the rail renders *after* this one + * when nothing sorts them. + * + * Deliberate, and load-bearing for `agent rail: the newest conversation is at the + * top`: unsorted, this row comes first, so a rail that sorts newest-first has to + * move it and one that does not cannot accidentally pass. + */ createdAt: "2026-08-11T16:40:00Z", updatedAt: "2026-08-19T08:22:00Z", labels: { team: "platform" }, From b44a18d9a48bf52cc7d429558696fac023265ae9 Mon Sep 17 00:00:00 2001 From: Nicholas Bucher Date: Wed, 26 Aug 2026 11:11:58 -0400 Subject: [PATCH 07/25] refactor(ui): stop enumerating an extension's settings, and drop what nothing reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings documentation described another product rather than this one, in a public repository, and named three of its configuration keys to do it. `UI_BACKEND_HOST` and `UI_BACKEND_TOKEN` read as *this* application's backend, which is the opposite of what they were: an API an extension reaches instead of `API_BASE_URL`. `UI_BACKEND_HOST` was documented as a management plane and `LOCAL_CLUSTER_NAME` as the cluster that management plane is installed on, with `mgmt-cluster` as the example value and the same string in `env.test.ts`. That is another product's architecture, and this application has no opinion about it. None of the three is named here any more, because none of them is this repository's business. An extension's settings are a prefix now rather than a list: anything called `EXTENSION_*` is passed through by the dev server and by `scripts/init.sh`, and read back with `readEnv`, which already took any key against an already-open record. So an extension gains a setting by naming one, with no change here — where before, a public repository had to be edited to add a key nothing in it reads. The four `OIDC_*` keys are gone rather than renamed. Nothing here read them: they appeared only in the pass-through list, and `git log -S` puts them in `ui/` for the first time in the rewrite. The comment against them claimed the app "runs the authorization code flow itself rather than expecting an authentication proxy in front of it", which is the reverse of the truth — the one implemented source is `oauth2ProxyAuthSource`, there is no PKCE anywhere in the app, and what it supports is exactly the proxy that comment disclaimed, configured through `SSO_REDIRECT_PATH`. `.env.example` carried twelve lines of setup guidance for that absent flow, down to a Keycloak URL and a realm belonging to somebody's own machine. The prefix is still bounded, which is the whole point of `CORE_ENV_KEYS` being a list: the dev server inlines these into the document, so a wholesale copy of the environment would publish every credential on the machine into the HTML. A variable now has to be deliberately named for an extension — but only that deliberately, so a stray `EXTENSION_` in a shell will be inlined too. The comment says so rather than implying a guarantee it does not give. `init.sh` was run against a value carrying quotes, a backslash and a `