diff --git a/docs/server/docs.go b/docs/server/docs.go index 33bfcb311..b5e6c101c 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -6,7 +6,7 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"callback_port":{"description":"CallbackPort is the specific port to use for the OAuth callback server\nIf not specified, a random available port will be used","type":"integer"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuthParams contains additional OAuth parameters to include in the authorization request\nThese are server-specific parameters like \"prompt\", \"response_mode\", etc.","type":"object"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RemoteAuthConfig":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorizeURL":{"type":"string"},"callbackPort":{"type":"integer"},"clientID":{"type":"string"},"clientSecret":{"type":"string"},"clientSecretFile":{"type":"string"},"envVars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array"},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array"},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauthParams":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array"},"skipBrowser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"tokenURL":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"remote_auth_config":{"$ref":"#/components/schemas/runner.RemoteAuthConfig"},"remote_url":{"description":"RemoteURL is the URL of the remote MCP server (if running remotely)","type":"string"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tool_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolOverride is the map of tool names to override. Tools to override are\nspecified as ToolOverride structs.","type":"object"},"tool_override_file":{"description":"ToolOverrideFile is the path to a file containing tool overrides.\nThe file is a JSON struct mapping actual names to ToolOverride structs.","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]}},"type":"object"}}}, + "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"callback_port":{"description":"CallbackPort is the specific port to use for the OAuth callback server\nIf not specified, a random available port will be used","type":"integer"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuthParams contains additional OAuth parameters to include in the authorization request\nThese are server-specific parameters like \"prompt\", \"response_mode\", etc.","type":"object"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RemoteAuthConfig":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorizeURL":{"type":"string"},"callbackPort":{"type":"integer"},"clientID":{"type":"string"},"clientSecret":{"type":"string"},"clientSecretFile":{"type":"string"},"envVars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array"},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array"},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauthParams":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array"},"skipBrowser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"tokenURL":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"remote_auth_config":{"$ref":"#/components/schemas/runner.RemoteAuthConfig"},"remote_url":{"description":"RemoteURL is the URL of the remote MCP server (if running remotely)","type":"string"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tool_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolOverride is the map of tool names to override. Tools to override are\nspecified as ToolOverride structs.","type":"object"},"tool_override_file":{"description":"ToolOverrideFile is the path to a file containing tool overrides.\nThe file is a JSON struct mapping actual names to ToolOverride structs.","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"url":{"description":"Remote server specific fields","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.remoteOAuthConfig":{"description":"OAuth configuration for remote server authentication","properties":{"authorize_url":{"description":"OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"callback_port":{"description":"Specific port for OAuth callback server","type":"integer"},"client_id":{"description":"OAuth client ID for authentication","type":"string"},"client_secret":{"$ref":"#/components/schemas/secrets.SecretParameter"},"issuer":{"description":"OAuth/OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters for server-specific customization","type":"object"},"scopes":{"description":"OAuth scopes to request","items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"description":"Whether to skip opening browser for OAuth flow (defaults to false)","type":"boolean"},"token_url":{"description":"OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"use_pkce":{"description":"Whether to use PKCE for the OAuth flow","type":"boolean"}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"url":{"description":"Remote server specific fields","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]}},"type":"object"}}}, "info": {"description":"{{escape .Description}}","title":"{{.Title}}","version":"{{.Version}}"}, "externalDocs": {"description":"","url":""}, "paths": {"/api/openapi.json":{"get":{"description":"Returns the OpenAPI specification for the API","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OpenAPI specification"}},"summary":"Get OpenAPI specification","tags":["system"]}},"/api/v1beta/clients":{"get":{"description":"List all registered clients in ToolHive","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/client.RegisteredClient"},"type":"array"}}},"description":"OK"}},"summary":"List all clients","tags":["clients"]},"post":{"description":"Register a new client with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientRequest"}}},"description":"Client to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register a new client","tags":["clients"]}},"/api/v1beta/clients/register":{"post":{"description":"Register multiple clients with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/v1.createClientResponse"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register multiple clients","tags":["clients"]}},"/api/v1beta/clients/unregister":{"post":{"description":"Unregister multiple clients from ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to unregister","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister multiple clients","tags":["clients"]}},"/api/v1beta/clients/{name}":{"delete":{"description":"Unregister a client from ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister a client","tags":["clients"]}},"/api/v1beta/clients/{name}/groups/{group}":{"delete":{"description":"Unregister a client from a specific group in ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Group name to remove client from","in":"path","name":"group","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Client or group not found"}},"summary":"Unregister a client from a specific group","tags":["clients"]}},"/api/v1beta/discovery/clients":{"get":{"description":"List all clients compatible with ToolHive and their status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.clientStatusResponse"}}},"description":"OK"}},"summary":"List all clients status","tags":["discovery"]}},"/api/v1beta/groups":{"get":{"description":"Get a list of all groups","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.groupListResponse"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List all groups","tags":["groups"]},"post":{"description":"Create a new group with the specified name","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupRequest"}}},"description":"Group creation request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new group","tags":["groups"]}},"/api/v1beta/groups/{name}":{"delete":{"description":"Delete a group by name.","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Delete all workloads in the group (default: false, moves workloads to default group)","in":"query","name":"with-workloads","schema":{"type":"boolean"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a group","tags":["groups"]},"get":{"description":"Get details of a specific group","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/groups.Group"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get group details","tags":["groups"]}},"/api/v1beta/registry":{"get":{"description":"Get a list of the current registries","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.registryListResponse"}}},"description":"OK"}},"summary":"List registries","tags":["registry"]},"post":{"description":"Add a new registry","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"501":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Implemented"}},"summary":"Add a registry","tags":["registry"]}},"/api/v1beta/registry/{name}":{"delete":{"description":"Remove a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Remove a registry","tags":["registry"]},"get":{"description":"Get details of a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getRegistryResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a registry","tags":["registry"]},"put":{"description":"Update registry URL or local path for the default registry","parameters":[{"description":"Registry name (must be 'default')","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryRequest"}}},"description":"Registry configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update registry configuration","tags":["registry"]}},"/api/v1beta/registry/{name}/servers":{"get":{"description":"Get a list of servers in a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listServersResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"List servers in a registry","tags":["registry"]}},"/api/v1beta/registry/{name}/servers/{serverName}":{"get":{"description":"Get details of a specific server in a registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"ImageMetadata name","in":"path","name":"serverName","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getServerResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a server from a registry","tags":["registry"]}},"/api/v1beta/secrets":{"post":{"description":"Setup the secrets provider with the specified type and configuration.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsRequest"}}},"description":"Setup secrets provider request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Setup or reconfigure secrets provider","tags":["secrets"]}},"/api/v1beta/secrets/default":{"get":{"description":"Get details of the default secrets provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getSecretsProviderResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get secrets provider details","tags":["secrets"]}},"/api/v1beta/secrets/default/keys":{"get":{"description":"Get a list of all secret keys from the default provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listSecretsResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support listing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List secrets","tags":["secrets"]},"post":{"description":"Create a new secret in the default provider (encrypted provider only)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretRequest"}}},"description":"Create secret request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict - Secret already exists"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new secret","tags":["secrets"]}},"/api/v1beta/secrets/default/keys/{key}":{"delete":{"description":"Delete a secret from the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support deletion"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a secret","tags":["secrets"]},"put":{"description":"Update an existing secret in the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretRequest"}}},"description":"Update secret request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Update a secret","tags":["secrets"]}},"/api/v1beta/version":{"get":{"description":"Returns the current version of the server","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.versionResponse"}}},"description":"OK"}},"summary":"Get server version","tags":["version"]}},"/api/v1beta/workloads":{"get":{"description":"Get a list of all running workloads, optionally filtered by group","parameters":[{"description":"List all workloads, including stopped ones","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter workloads by group name","in":"query","name":"group","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.workloadListResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Group not found"}},"summary":"List all workloads","tags":["workloads"]},"post":{"description":"Create and start a new workload","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"Create workload request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"}},"summary":"Create a new workload","tags":["workloads"]}},"/api/v1beta/workloads/delete":{"post":{"description":"Delete multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk delete request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Delete workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/restart":{"post":{"description":"Restart multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk restart request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Restart workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/stop":{"post":{"description":"Stop multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk stop request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Stop workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/{name}":{"delete":{"description":"Delete a workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Delete a workload","tags":["workloads"]},"get":{"description":"Get details of a specific workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload details","tags":["workloads"]}},"/api/v1beta/workloads/{name}/edit":{"post":{"description":"Update an existing workload configuration","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateRequest"}}},"description":"Update workload request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/export":{"get":{"description":"Export a workload's run configuration as JSON","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/runner.RunConfig"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Export workload configuration","tags":["workloads"]}},"/api/v1beta/workloads/{name}/logs":{"get":{"description":"Retrieve at most 100 lines of logs for a specific workload by name.","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}}},"description":"Logs for the specified workload"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get logs for a specific workload","tags":["logs"]}},"/api/v1beta/workloads/{name}/restart":{"post":{"description":"Restart a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Restart a workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/status":{"get":{"description":"Get the current status of a specific workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.workloadStatusResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload status","tags":["workloads"]}},"/api/v1beta/workloads/{name}/stop":{"post":{"description":"Stop a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Stop a workload","tags":["workloads"]}},"/health":{"get":{"description":"Check if the API is healthy","responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"}},"summary":"Health check","tags":["system"]}}}, diff --git a/docs/server/swagger.json b/docs/server/swagger.json index 0e3378ce4..0b7da740b 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -1,5 +1,5 @@ { - "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"callback_port":{"description":"CallbackPort is the specific port to use for the OAuth callback server\nIf not specified, a random available port will be used","type":"integer"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuthParams contains additional OAuth parameters to include in the authorization request\nThese are server-specific parameters like \"prompt\", \"response_mode\", etc.","type":"object"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RemoteAuthConfig":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorizeURL":{"type":"string"},"callbackPort":{"type":"integer"},"clientID":{"type":"string"},"clientSecret":{"type":"string"},"clientSecretFile":{"type":"string"},"envVars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array"},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array"},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauthParams":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array"},"skipBrowser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"tokenURL":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"remote_auth_config":{"$ref":"#/components/schemas/runner.RemoteAuthConfig"},"remote_url":{"description":"RemoteURL is the URL of the remote MCP server (if running remotely)","type":"string"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tool_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolOverride is the map of tool names to override. Tools to override are\nspecified as ToolOverride structs.","type":"object"},"tool_override_file":{"description":"ToolOverrideFile is the path to a file containing tool overrides.\nThe file is a JSON struct mapping actual names to ToolOverride structs.","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]}},"type":"object"}}}, + "components": {"schemas":{"audit.Config":{"description":"AuditConfig contains the audit logging configuration","properties":{"component":{"description":"Component is the component name to use in audit events","type":"string"},"event_types":{"description":"EventTypes specifies which event types to audit. If empty, all events are audited.","items":{"type":"string"},"type":"array","uniqueItems":false},"exclude_event_types":{"description":"ExcludeEventTypes specifies which event types to exclude from auditing.\nThis takes precedence over EventTypes.","items":{"type":"string"},"type":"array","uniqueItems":false},"include_request_data":{"description":"IncludeRequestData determines whether to include request data in audit logs","type":"boolean"},"include_response_data":{"description":"IncludeResponseData determines whether to include response data in audit logs","type":"boolean"},"log_file":{"description":"LogFile specifies the file path for audit logs. If empty, logs to stdout.","type":"string"},"max_data_size":{"description":"MaxDataSize limits the size of request/response data included in audit logs (in bytes)","type":"integer"}},"type":"object"},"auth.TokenValidatorConfig":{"description":"OIDCConfig contains OIDC configuration","properties":{"allowPrivateIP":{"description":"AllowPrivateIP allows JWKS/OIDC endpoints on private IP addresses","type":"boolean"},"audience":{"description":"Audience is the expected audience for the token","type":"string"},"authTokenFile":{"description":"AuthTokenFile is the path to file containing bearer token for authentication","type":"string"},"cacertPath":{"description":"CACertPath is the path to the CA certificate bundle for HTTPS requests","type":"string"},"clientID":{"description":"ClientID is the OIDC client ID","type":"string"},"clientSecret":{"description":"ClientSecret is the optional OIDC client secret for introspection","type":"string"},"introspectionURL":{"description":"IntrospectionURL is the optional introspection endpoint for validating tokens","type":"string"},"issuer":{"description":"Issuer is the OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"jwksurl":{"description":"JWKSURL is the URL to fetch the JWKS from","type":"string"},"resourceURL":{"description":"ResourceURL is the explicit resource URL for OAuth discovery (RFC 9728)","type":"string"}},"type":"object"},"authz.CedarConfig":{"description":"Cedar is the Cedar-specific configuration.\nThis is only used when Type is ConfigTypeCedarV1.","properties":{"entities_json":{"description":"EntitiesJSON is the JSON string representing Cedar entities","type":"string"},"policies":{"description":"Policies is a list of Cedar policy strings","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"authz.Config":{"description":"AuthzConfig contains the authorization configuration","properties":{"cedar":{"$ref":"#/components/schemas/authz.CedarConfig"},"type":{"$ref":"#/components/schemas/authz.ConfigType"},"version":{"description":"Version is the version of the configuration format.","type":"string"}},"type":"object"},"authz.ConfigType":{"description":"Type is the type of authorization configuration.","type":"string","x-enum-varnames":["ConfigTypeCedarV1"]},"client.MCPClient":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"client.MCPClientStatus":{"properties":{"client_type":{"description":"ClientType is the type of MCP client","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"installed":{"description":"Installed indicates whether the client is installed on the system","type":"boolean"},"registered":{"description":"Registered indicates whether the client is registered in the ToolHive configuration","type":"boolean"}},"type":"object"},"client.RegisteredClient":{"properties":{"groups":{"items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"$ref":"#/components/schemas/client.MCPClient"}},"type":"object"},"core.Workload":{"properties":{"created_at":{"description":"CreatedAt is the timestamp when the workload was created.","type":"string"},"group":{"description":"Group is the name of the group this workload belongs to, if any.","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Labels are the container labels (excluding standard ToolHive labels)","type":"object"},"name":{"description":"Name is the name of the workload.\nIt is used as a unique identifier.","type":"string"},"package":{"description":"Package specifies the Workload Package used to create this Workload.","type":"string"},"port":{"description":"Port is the port on which the workload is exposed.\nThis is embedded in the URL.","type":"integer"},"remote":{"description":"Remote indicates whether this is a remote workload (true) or a container workload (false).","type":"boolean"},"status":{"$ref":"#/components/schemas/runtime.WorkloadStatus"},"status_context":{"description":"StatusContext provides additional context about the workload's status.\nThe exact meaning is determined by the status and the underlying runtime.","type":"string"},"tool_type":{"description":"ToolType is the type of tool this workload represents.\nFor now, it will always be \"mcp\" - representing an MCP server.","type":"string"},"tools":{"description":"ToolsFilter is the filter on tools applied to the workload.","items":{"type":"string"},"type":"array","uniqueItems":false},"transport_type":{"$ref":"#/components/schemas/types.TransportType"},"url":{"description":"URL is the URL of the workload exposed by the ToolHive proxy.","type":"string"}},"type":"object"},"groups.Group":{"properties":{"name":{"type":"string"},"registered_clients":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"ignore.Config":{"description":"IgnoreConfig contains configuration for ignore processing","properties":{"loadGlobal":{"description":"Whether to load global ignore patterns","type":"boolean"},"printOverlays":{"description":"Whether to print resolved overlay paths for debugging","type":"boolean"}},"type":"object"},"permissions.NetworkPermissions":{"description":"Network defines network permissions","properties":{"outbound":{"$ref":"#/components/schemas/permissions.OutboundNetworkPermissions"}},"type":"object"},"permissions.OutboundNetworkPermissions":{"description":"Outbound defines outbound network permissions","properties":{"allow_host":{"description":"AllowHost is a list of allowed hosts","items":{"type":"string"},"type":"array","uniqueItems":false},"allow_port":{"description":"AllowPort is a list of allowed ports","items":{"type":"integer"},"type":"array","uniqueItems":false},"insecure_allow_all":{"description":"InsecureAllowAll allows all outbound network connections","type":"boolean"}},"type":"object"},"permissions.Profile":{"description":"PermissionProfile is the permission profile to use","properties":{"name":{"description":"Name is the name of the profile","type":"string"},"network":{"$ref":"#/components/schemas/permissions.NetworkPermissions"},"privileged":{"description":"Privileged indicates whether the container should run in privileged mode\nWhen true, the container has access to all host devices and capabilities\nUse with extreme caution as this removes most security isolation","type":"boolean"},"read":{"description":"Read is a list of mount declarations that the container can read from\nThese can be in the following formats:\n- A single path: The same path will be mounted from host to container\n- host-path:container-path: Different paths for host and container\n- resource-uri:container-path: Mount a resource identified by URI to a container path","items":{"type":"string"},"type":"array","uniqueItems":false},"write":{"description":"Write is a list of mount declarations that the container can write to\nThese follow the same format as Read mounts but with write permissions","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"registry.EnvVar":{"properties":{"default":{"description":"Default is the value to use if the environment variable is not explicitly provided\nOnly used for non-required variables","type":"string"},"description":{"description":"Description is a human-readable explanation of the variable's purpose","type":"string"},"name":{"description":"Name is the environment variable name (e.g., API_KEY)","type":"string"},"required":{"description":"Required indicates whether this environment variable must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this environment variable contains sensitive information\nIf true, the value will be stored as a secret rather than as a plain environment variable","type":"boolean"}},"type":"object"},"registry.Header":{"properties":{"choices":{"description":"Choices provides a list of valid values for the header (optional)","items":{"type":"string"},"type":"array","uniqueItems":false},"default":{"description":"Default is the value to use if the header is not explicitly provided\nOnly used for non-required headers","type":"string"},"description":{"description":"Description is a human-readable explanation of the header's purpose","type":"string"},"name":{"description":"Name is the header name (e.g., X-API-Key, Authorization)","type":"string"},"required":{"description":"Required indicates whether this header must be provided\nIf true and not provided via command line or secrets, the user will be prompted for a value","type":"boolean"},"secret":{"description":"Secret indicates whether this header contains sensitive information\nIf true, the value will be stored as a secret rather than as plain text","type":"boolean"}},"type":"object"},"registry.ImageMetadata":{"description":"Container server details (if it's a container server)","properties":{"args":{"description":"Args are the default command-line arguments to pass to the MCP server container.\nThese arguments will be used only if no command-line arguments are provided by the user.\nIf the user provides arguments, they will override these defaults.","items":{"type":"string"},"type":"array","uniqueItems":false},"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"docker_tags":{"description":"DockerTags lists the available Docker tags for this server image","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"description":"EnvVars defines environment variables that can be passed to the server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"image":{"description":"Image is the Docker image reference for the MCP server","type":"string"},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"permissions":{"$ref":"#/components/schemas/permissions.Profile"},"provenance":{"$ref":"#/components/schemas/registry.Provenance"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE and Streamable HTTP transports)","type":"integer"},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"}},"type":"object"},"registry.Metadata":{"description":"Metadata contains additional information about the server such as popularity metrics","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the server was last updated, in RFC3339 format","type":"string"},"pulls":{"description":"Pulls indicates how many times the server image has been downloaded","type":"integer"},"stars":{"description":"Stars represents the popularity rating or number of stars for the server","type":"integer"}},"type":"object"},"registry.OAuthConfig":{"description":"OAuthConfig provides OAuth/OIDC configuration for authentication to the remote server\nUsed with the thv proxy command's --remote-auth flags","properties":{"authorize_url":{"description":"AuthorizeURL is the OAuth authorization endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"callback_port":{"description":"CallbackPort is the specific port to use for the OAuth callback server\nIf not specified, a random available port will be used","type":"integer"},"client_id":{"description":"ClientID is the OAuth client ID for authentication","type":"string"},"issuer":{"description":"Issuer is the OAuth/OIDC issuer URL (e.g., https://accounts.google.com)\nUsed for OIDC discovery to find authorization and token endpoints","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"OAuthParams contains additional OAuth parameters to include in the authorization request\nThese are server-specific parameters like \"prompt\", \"response_mode\", etc.","type":"object"},"scopes":{"description":"Scopes are the OAuth scopes to request\nIf not specified, defaults to [\"openid\", \"profile\", \"email\"] for OIDC","items":{"type":"string"},"type":"array","uniqueItems":false},"token_url":{"description":"TokenURL is the OAuth token endpoint URL\nUsed for non-OIDC OAuth flows when issuer is not provided","type":"string"},"use_pkce":{"description":"UsePKCE indicates whether to use PKCE for the OAuth flow\nDefaults to true for enhanced security","type":"boolean"}},"type":"object"},"registry.Provenance":{"description":"Provenance contains verification and signing metadata","properties":{"attestation":{"$ref":"#/components/schemas/registry.VerifiedAttestation"},"cert_issuer":{"type":"string"},"repository_ref":{"type":"string"},"repository_uri":{"type":"string"},"runner_environment":{"type":"string"},"signer_identity":{"type":"string"},"sigstore_url":{"type":"string"}},"type":"object"},"registry.Registry":{"description":"Full registry data","properties":{"last_updated":{"description":"LastUpdated is the timestamp when the registry was last updated, in RFC3339 format","type":"string"},"remote_servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"description":"RemoteServers is a map of server names to their corresponding remote server definitions\nThese are MCP servers accessed via HTTP/HTTPS using the thv proxy command","type":"object"},"servers":{"additionalProperties":{"$ref":"#/components/schemas/registry.ImageMetadata"},"description":"Servers is a map of server names to their corresponding server definitions","type":"object"},"version":{"description":"Version is the schema version of the registry","type":"string"}},"type":"object"},"registry.RemoteServerMetadata":{"description":"Remote server details (if it's a remote server)","properties":{"custom_metadata":{"additionalProperties":{},"description":"CustomMetadata allows for additional user-defined metadata","type":"object"},"description":{"description":"Description is a human-readable description of the server's purpose and functionality","type":"string"},"env_vars":{"description":"EnvVars defines environment variables that can be passed to configure the client\nThese might be needed for client-side configuration when connecting to the remote server","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array","uniqueItems":false},"headers":{"description":"Headers defines HTTP headers that can be passed to the remote server for authentication\nThese are used with the thv proxy command's authentication features","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"metadata":{"$ref":"#/components/schemas/registry.Metadata"},"name":{"description":"Name is the identifier for the MCP server, used when referencing the server in commands\nIf not provided, it will be auto-generated from the registry key","type":"string"},"oauth_config":{"$ref":"#/components/schemas/registry.OAuthConfig"},"repository_url":{"description":"RepositoryURL is the URL to the source code repository for the server","type":"string"},"status":{"description":"Status indicates whether the server is currently active or deprecated","type":"string"},"tags":{"description":"Tags are categorization labels for the server to aid in discovery and filtering","items":{"type":"string"},"type":"array","uniqueItems":false},"tier":{"description":"Tier represents the tier classification level of the server, e.g., \"Official\" or \"Community\"","type":"string"},"tools":{"description":"Tools is a list of tool names provided by this MCP server","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport defines the communication protocol for the server\nFor containers: stdio, sse, or streamable-http\nFor remote servers: sse or streamable-http (stdio not supported)","type":"string"},"url":{"description":"URL is the endpoint URL for the remote MCP server (e.g., https://api.example.com/mcp)","type":"string"}},"type":"object"},"registry.VerifiedAttestation":{"properties":{"predicate":{},"predicate_type":{"type":"string"}},"type":"object"},"runner.RemoteAuthConfig":{"description":"RemoteAuthConfig contains OAuth configuration for remote MCP servers","properties":{"authorizeURL":{"type":"string"},"callbackPort":{"type":"integer"},"clientID":{"type":"string"},"clientSecret":{"type":"string"},"clientSecretFile":{"type":"string"},"envVars":{"description":"Environment variables for the client","items":{"$ref":"#/components/schemas/registry.EnvVar"},"type":"array"},"headers":{"description":"Headers for HTTP requests","items":{"$ref":"#/components/schemas/registry.Header"},"type":"array"},"issuer":{"description":"OAuth endpoint configuration (from registry)","type":"string"},"oauthParams":{"additionalProperties":{"type":"string"},"description":"OAuth parameters for server-specific customization","type":"object"},"scopes":{"items":{"type":"string"},"type":"array"},"skipBrowser":{"type":"boolean"},"timeout":{"example":"5m","type":"string"},"tokenURL":{"type":"string"}},"type":"object"},"runner.RunConfig":{"properties":{"audit_config":{"$ref":"#/components/schemas/audit.Config"},"audit_config_path":{"description":"AuditConfigPath is the path to the audit configuration file","type":"string"},"authz_config":{"$ref":"#/components/schemas/authz.Config"},"authz_config_path":{"description":"AuthzConfigPath is the path to the authorization configuration file","type":"string"},"base_name":{"description":"BaseName is the base name used for the container (without prefixes)","type":"string"},"cmd_args":{"description":"CmdArgs are the arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"container_labels":{"additionalProperties":{"type":"string"},"description":"ContainerLabels are the labels to apply to the container","type":"object"},"container_name":{"description":"ContainerName is the name of the container","type":"string"},"debug":{"description":"Debug indicates whether debug mode is enabled","type":"boolean"},"env_vars":{"additionalProperties":{"type":"string"},"description":"EnvVars are the parsed environment variables as key-value pairs","type":"object"},"group":{"description":"Group is the name of the group this workload belongs to, if any","type":"string"},"host":{"description":"Host is the host for the HTTP proxy","type":"string"},"ignore_config":{"$ref":"#/components/schemas/ignore.Config"},"image":{"description":"Image is the Docker image to run","type":"string"},"isolate_network":{"description":"IsolateNetwork indicates whether to isolate the network for the container","type":"boolean"},"jwks_auth_token_file":{"description":"JWKSAuthTokenFile is the path to file containing auth token for JWKS/OIDC requests","type":"string"},"k8s_pod_template_patch":{"description":"K8sPodTemplatePatch is a JSON string to patch the Kubernetes pod template\nOnly applicable when using Kubernetes runtime","type":"string"},"middleware_configs":{"description":"MiddlewareConfigs contains the list of middleware to apply to the transport\nand the configuration for each middleware.","items":{"$ref":"#/components/schemas/types.MiddlewareConfig"},"type":"array","uniqueItems":false},"name":{"description":"Name is the name of the MCP server","type":"string"},"oidc_config":{"$ref":"#/components/schemas/auth.TokenValidatorConfig"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"permission_profile_name_or_path":{"description":"PermissionProfileNameOrPath is the name or path of the permission profile","type":"string"},"port":{"description":"Port is the port for the HTTP proxy to listen on (host port)","type":"integer"},"proxy_mode":{"$ref":"#/components/schemas/types.ProxyMode"},"remote_auth_config":{"$ref":"#/components/schemas/runner.RemoteAuthConfig"},"remote_url":{"description":"RemoteURL is the URL of the remote MCP server (if running remotely)","type":"string"},"schema_version":{"description":"SchemaVersion is the version of the RunConfig schema","type":"string"},"secrets":{"description":"Secrets are the secret parameters to pass to the container\nFormat: \"\u003csecret name\u003e,target=\u003ctarget environment variable\u003e\"","items":{"type":"string"},"type":"array","uniqueItems":false},"target_host":{"description":"TargetHost is the host to forward traffic to (only applicable to SSE transport)","type":"string"},"target_port":{"description":"TargetPort is the port for the container to expose (only applicable to SSE transport)","type":"integer"},"telemetry_config":{"$ref":"#/components/schemas/telemetry.Config"},"thv_ca_bundle":{"description":"ThvCABundle is the path to the CA certificate bundle for ToolHive HTTP operations","type":"string"},"tool_override":{"additionalProperties":{"$ref":"#/components/schemas/runner.ToolOverride"},"description":"ToolOverride is the map of tool names to override. Tools to override are\nspecified as ToolOverride structs.","type":"object"},"tool_override_file":{"description":"ToolOverrideFile is the path to a file containing tool overrides.\nThe file is a JSON struct mapping actual names to ToolOverride structs.","type":"string"},"tools_filter":{"description":"ToolsFilter is the list of tools to filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport is the transport mode (stdio, sse, or streamable-http)","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"volumes":{"description":"Volumes are the directory mounts to pass to the container\nFormat: \"host-path:container-path[:ro]\"","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"runner.ToolOverride":{"properties":{"description":{"description":"Description is the redefined description of the tool","type":"string"},"name":{"description":"Name is the redefined name of the tool","type":"string"}},"type":"object"},"runtime.WorkloadStatus":{"description":"Status is the current status of the workload.","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]},"secrets.SecretParameter":{"properties":{"name":{"type":"string"},"target":{"type":"string"}},"type":"object"},"telemetry.Config":{"description":"TelemetryConfig contains the OpenTelemetry configuration","properties":{"enablePrometheusMetricsPath":{"description":"EnablePrometheusMetricsPath controls whether to expose Prometheus-style /metrics endpoint\nThe metrics are served on the main transport port at /metrics\nThis is separate from OTLP metrics which are sent to the Endpoint","type":"boolean"},"endpoint":{"description":"Endpoint is the OTLP endpoint URL","type":"string"},"environmentVariables":{"description":"EnvironmentVariables is a list of environment variable names that should be\nincluded in telemetry spans as attributes. Only variables in this list will\nbe read from the host machine and included in spans for observability.\nExample: []string{\"NODE_ENV\", \"DEPLOYMENT_ENV\", \"SERVICE_VERSION\"}","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Headers contains authentication headers for the OTLP endpoint","type":"object"},"insecure":{"description":"Insecure indicates whether to use HTTP instead of HTTPS for the OTLP endpoint","type":"boolean"},"samplingRate":{"description":"SamplingRate is the trace sampling rate (0.0-1.0)","type":"number"},"serviceName":{"description":"ServiceName is the service name for telemetry","type":"string"},"serviceVersion":{"description":"ServiceVersion is the service version for telemetry","type":"string"}},"type":"object"},"types.MiddlewareConfig":{"properties":{"parameters":{"description":"Parameters is a JSON object containing the middleware parameters.\nIt is stored as a raw message to allow flexible parameter types.","type":"object"},"type":{"description":"Type is a string representing the middleware type.","type":"string"}},"type":"object"},"types.ProxyMode":{"description":"ProxyMode is the proxy mode for stdio transport (\"sse\" or \"streamable-http\")","type":"string","x-enum-varnames":["ProxyModeSSE","ProxyModeStreamableHTTP"]},"types.TransportType":{"description":"TransportType is the type of transport used for this workload.","type":"string","x-enum-varnames":["TransportTypeStdio","TransportTypeSSE","TransportTypeStreamableHTTP","TransportTypeInspector"]},"v1.RegistryType":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"v1.UpdateRegistryRequest":{"description":"Request containing registry configuration updates","properties":{"allow_private_ip":{"description":"Allow private IP addresses for registry URL","type":"boolean"},"local_path":{"description":"Local registry file path","type":"string"},"url":{"description":"Registry URL (for remote registries)","type":"string"}},"type":"object"},"v1.UpdateRegistryResponse":{"description":"Response containing update result","properties":{"message":{"description":"Status message","type":"string"},"type":{"description":"Registry type after update","type":"string"}},"type":"object"},"v1.bulkClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"names":{"description":"Names is the list of client names to operate on.","items":{"type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]},"type":"array","uniqueItems":false}},"type":"object"},"v1.bulkOperationRequest":{"properties":{"group":{"description":"Group name to operate on (mutually exclusive with names)","type":"string"},"names":{"description":"Names of the workloads to operate on","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.clientStatusResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/client.MCPClientStatus"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createClientRequest":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client to register.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createClientResponse":{"properties":{"groups":{"description":"Groups is the list of groups configured on the client.","items":{"type":"string"},"type":"array","uniqueItems":false},"name":{"description":"Name is the type of the client that was registered.","type":"string","x-enum-varnames":["RooCode","Cline","Cursor","VSCodeInsider","VSCode","ClaudeCode","Windsurf","WindsurfJetBrains","AmpCli","AmpVSCode","AmpCursor","AmpVSCodeInsider","AmpWindsurf","LMStudio","Goose"]}},"type":"object"},"v1.createGroupRequest":{"properties":{"name":{"description":"Name of the group to create","type":"string"}},"type":"object"},"v1.createGroupResponse":{"properties":{"name":{"description":"Name of the created group","type":"string"}},"type":"object"},"v1.createRequest":{"description":"Request to create a new workload","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"name":{"description":"Name of the workload","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"url":{"description":"Remote server specific fields","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.createSecretRequest":{"description":"Request to create a new secret","properties":{"key":{"description":"Secret key name","type":"string"},"value":{"description":"Secret value","type":"string"}},"type":"object"},"v1.createSecretResponse":{"description":"Response after creating a secret","properties":{"key":{"description":"Secret key that was created","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.createWorkloadResponse":{"description":"Response after successfully creating a workload","properties":{"name":{"description":"Name of the created workload","type":"string"},"port":{"description":"Port the workload is listening on","type":"integer"}},"type":"object"},"v1.getRegistryResponse":{"description":"Response containing registry details","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"registry":{"$ref":"#/components/schemas/registry.Registry"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"description":"Type of registry (file, url, or default)","type":"string","x-enum-varnames":["RegistryTypeFile","RegistryTypeURL","RegistryTypeDefault"]},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.getSecretsProviderResponse":{"description":"Response containing secrets provider details","properties":{"capabilities":{"$ref":"#/components/schemas/v1.providerCapabilitiesResponse"},"name":{"description":"Name of the secrets provider","type":"string"},"provider_type":{"description":"Type of the secrets provider","type":"string"}},"type":"object"},"v1.getServerResponse":{"description":"Response containing server details","properties":{"is_remote":{"description":"Indicates if this is a remote server","type":"boolean"},"remote_server":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"server":{"$ref":"#/components/schemas/registry.ImageMetadata"}},"type":"object"},"v1.groupListResponse":{"properties":{"groups":{"description":"List of groups","items":{"$ref":"#/components/schemas/groups.Group"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listSecretsResponse":{"description":"Response containing a list of secret keys","properties":{"keys":{"description":"List of secret keys","items":{"$ref":"#/components/schemas/v1.secretKeyResponse"},"type":"array","uniqueItems":false}},"type":"object"},"v1.listServersResponse":{"description":"Response containing a list of servers","properties":{"remote_servers":{"description":"List of remote servers in the registry (if any)","items":{"$ref":"#/components/schemas/registry.RemoteServerMetadata"},"type":"array","uniqueItems":false},"servers":{"description":"List of container servers in the registry","items":{"$ref":"#/components/schemas/registry.ImageMetadata"},"type":"array","uniqueItems":false}},"type":"object"},"v1.oidcOptions":{"description":"OIDC configuration options","properties":{"audience":{"description":"Expected audience","type":"string"},"client_id":{"description":"OAuth2 client ID","type":"string"},"client_secret":{"description":"OAuth2 client secret","type":"string"},"introspection_url":{"description":"Token introspection URL for OIDC","type":"string"},"issuer":{"description":"OIDC issuer URL","type":"string"},"jwks_url":{"description":"JWKS URL for key verification","type":"string"}},"type":"object"},"v1.providerCapabilitiesResponse":{"description":"Capabilities of the secrets provider","properties":{"can_cleanup":{"description":"Whether the provider can cleanup all secrets","type":"boolean"},"can_delete":{"description":"Whether the provider can delete secrets","type":"boolean"},"can_list":{"description":"Whether the provider can list secrets","type":"boolean"},"can_read":{"description":"Whether the provider can read secrets","type":"boolean"},"can_write":{"description":"Whether the provider can write secrets","type":"boolean"}},"type":"object"},"v1.registryInfo":{"description":"Basic information about a registry","properties":{"last_updated":{"description":"Last updated timestamp","type":"string"},"name":{"description":"Name of the registry","type":"string"},"server_count":{"description":"Number of servers in the registry","type":"integer"},"source":{"description":"Source of the registry (URL, file path, or empty string for built-in)","type":"string"},"type":{"$ref":"#/components/schemas/v1.RegistryType"},"version":{"description":"Version of the registry schema","type":"string"}},"type":"object"},"v1.registryListResponse":{"description":"Response containing a list of registries","properties":{"registries":{"description":"List of registries","items":{"$ref":"#/components/schemas/v1.registryInfo"},"type":"array","uniqueItems":false}},"type":"object"},"v1.remoteOAuthConfig":{"description":"OAuth configuration for remote server authentication","properties":{"authorize_url":{"description":"OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"callback_port":{"description":"Specific port for OAuth callback server","type":"integer"},"client_id":{"description":"OAuth client ID for authentication","type":"string"},"client_secret":{"$ref":"#/components/schemas/secrets.SecretParameter"},"issuer":{"description":"OAuth/OIDC issuer URL (e.g., https://accounts.google.com)","type":"string"},"oauth_params":{"additionalProperties":{"type":"string"},"description":"Additional OAuth parameters for server-specific customization","type":"object"},"scopes":{"description":"OAuth scopes to request","items":{"type":"string"},"type":"array","uniqueItems":false},"skip_browser":{"description":"Whether to skip opening browser for OAuth flow (defaults to false)","type":"boolean"},"token_url":{"description":"OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth)","type":"string"},"use_pkce":{"description":"Whether to use PKCE for the OAuth flow","type":"boolean"}},"type":"object"},"v1.secretKeyResponse":{"description":"Secret key information","properties":{"description":{"description":"Optional description of the secret","type":"string"},"key":{"description":"Secret key name","type":"string"}},"type":"object"},"v1.setupSecretsRequest":{"description":"Request to setup a secrets provider","properties":{"password":{"description":"Password for encrypted provider (optional, can be set via environment variable)\nTODO Review environment variable for this","type":"string"},"provider_type":{"description":"Type of the secrets provider (encrypted, 1password, none)","type":"string"}},"type":"object"},"v1.setupSecretsResponse":{"description":"Response after initializing a secrets provider","properties":{"message":{"description":"Success message","type":"string"},"provider_type":{"description":"Type of the secrets provider that was setup","type":"string"}},"type":"object"},"v1.updateRequest":{"description":"Request to update an existing workload (name cannot be changed)","properties":{"authz_config":{"description":"Authorization configuration","type":"string"},"cmd_arguments":{"description":"Command arguments to pass to the container","items":{"type":"string"},"type":"array","uniqueItems":false},"env_vars":{"additionalProperties":{"type":"string"},"description":"Environment variables to set in the container","type":"object"},"group":{"description":"Group name this workload belongs to","type":"string"},"headers":{"items":{"$ref":"#/components/schemas/registry.Header"},"type":"array","uniqueItems":false},"host":{"description":"Host to bind to","type":"string"},"image":{"description":"Docker image to use","type":"string"},"network_isolation":{"description":"Whether network isolation is turned on. This applies the rules in the permission profile.","type":"boolean"},"oauth_config":{"$ref":"#/components/schemas/v1.remoteOAuthConfig"},"oidc":{"$ref":"#/components/schemas/v1.oidcOptions"},"permission_profile":{"$ref":"#/components/schemas/permissions.Profile"},"proxy_mode":{"description":"Proxy mode to use","type":"string"},"secrets":{"description":"Secret parameters to inject","items":{"$ref":"#/components/schemas/secrets.SecretParameter"},"type":"array","uniqueItems":false},"target_port":{"description":"Port to expose from the container","type":"integer"},"tools":{"description":"Tools filter","items":{"type":"string"},"type":"array","uniqueItems":false},"transport":{"description":"Transport configuration","type":"string"},"url":{"description":"Remote server specific fields","type":"string"},"volumes":{"description":"Volume mounts","items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"v1.updateSecretRequest":{"description":"Request to update an existing secret","properties":{"value":{"description":"New secret value","type":"string"}},"type":"object"},"v1.updateSecretResponse":{"description":"Response after updating a secret","properties":{"key":{"description":"Secret key that was updated","type":"string"},"message":{"description":"Success message","type":"string"}},"type":"object"},"v1.versionResponse":{"properties":{"version":{"type":"string"}},"type":"object"},"v1.workloadListResponse":{"description":"Response containing a list of workloads","properties":{"workloads":{"description":"List of container information for each workload","items":{"$ref":"#/components/schemas/core.Workload"},"type":"array","uniqueItems":false}},"type":"object"},"v1.workloadStatusResponse":{"description":"Response containing workload status information","properties":{"status":{"description":"Current status of the workload","type":"string","x-enum-varnames":["WorkloadStatusRunning","WorkloadStatusStopped","WorkloadStatusError","WorkloadStatusStarting","WorkloadStatusStopping","WorkloadStatusUnhealthy","WorkloadStatusRemoving","WorkloadStatusUnknown"]}},"type":"object"}}}, "info": {"description":"This is the ToolHive API server.","title":"ToolHive API","version":"1.0"}, "externalDocs": {"description":"","url":""}, "paths": {"/api/openapi.json":{"get":{"description":"Returns the OpenAPI specification for the API","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OpenAPI specification"}},"summary":"Get OpenAPI specification","tags":["system"]}},"/api/v1beta/clients":{"get":{"description":"List all registered clients in ToolHive","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/client.RegisteredClient"},"type":"array"}}},"description":"OK"}},"summary":"List all clients","tags":["clients"]},"post":{"description":"Register a new client with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientRequest"}}},"description":"Client to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createClientResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register a new client","tags":["clients"]}},"/api/v1beta/clients/register":{"post":{"description":"Register multiple clients with ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to register","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/v1.createClientResponse"},"type":"array"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Register multiple clients","tags":["clients"]}},"/api/v1beta/clients/unregister":{"post":{"description":"Unregister multiple clients from ToolHive","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkClientRequest"}}},"description":"Clients to unregister","required":true},"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister multiple clients","tags":["clients"]}},"/api/v1beta/clients/{name}":{"delete":{"description":"Unregister a client from ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"}},"summary":"Unregister a client","tags":["clients"]}},"/api/v1beta/clients/{name}/groups/{group}":{"delete":{"description":"Unregister a client from a specific group in ToolHive","parameters":[{"description":"Client name to unregister","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Group name to remove client from","in":"path","name":"group","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Invalid request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Client or group not found"}},"summary":"Unregister a client from a specific group","tags":["clients"]}},"/api/v1beta/discovery/clients":{"get":{"description":"List all clients compatible with ToolHive and their status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.clientStatusResponse"}}},"description":"OK"}},"summary":"List all clients status","tags":["discovery"]}},"/api/v1beta/groups":{"get":{"description":"Get a list of all groups","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.groupListResponse"}}},"description":"OK"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List all groups","tags":["groups"]},"post":{"description":"Create a new group with the specified name","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupRequest"}}},"description":"Group creation request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createGroupResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new group","tags":["groups"]}},"/api/v1beta/groups/{name}":{"delete":{"description":"Delete a group by name.","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"Delete all workloads in the group (default: false, moves workloads to default group)","in":"query","name":"with-workloads","schema":{"type":"boolean"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a group","tags":["groups"]},"get":{"description":"Get details of a specific group","parameters":[{"description":"Group name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/groups.Group"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get group details","tags":["groups"]}},"/api/v1beta/registry":{"get":{"description":"Get a list of the current registries","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.registryListResponse"}}},"description":"OK"}},"summary":"List registries","tags":["registry"]},"post":{"description":"Add a new registry","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"501":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Implemented"}},"summary":"Add a registry","tags":["registry"]}},"/api/v1beta/registry/{name}":{"delete":{"description":"Remove a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Remove a registry","tags":["registry"]},"get":{"description":"Get details of a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getRegistryResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a registry","tags":["registry"]},"put":{"description":"Update registry URL or local path for the default registry","parameters":[{"description":"Registry name (must be 'default')","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryRequest"}}},"description":"Registry configuration","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UpdateRegistryResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update registry configuration","tags":["registry"]}},"/api/v1beta/registry/{name}/servers":{"get":{"description":"Get a list of servers in a specific registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listServersResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"List servers in a registry","tags":["registry"]}},"/api/v1beta/registry/{name}/servers/{serverName}":{"get":{"description":"Get details of a specific server in a registry","parameters":[{"description":"Registry name","in":"path","name":"name","required":true,"schema":{"type":"string"}},{"description":"ImageMetadata name","in":"path","name":"serverName","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getServerResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get a server from a registry","tags":["registry"]}},"/api/v1beta/secrets":{"post":{"description":"Setup the secrets provider with the specified type and configuration.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsRequest"}}},"description":"Setup secrets provider request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.setupSecretsResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Setup or reconfigure secrets provider","tags":["secrets"]}},"/api/v1beta/secrets/default":{"get":{"description":"Get details of the default secrets provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.getSecretsProviderResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Get secrets provider details","tags":["secrets"]}},"/api/v1beta/secrets/default/keys":{"get":{"description":"Get a list of all secret keys from the default provider","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.listSecretsResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support listing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"List secrets","tags":["secrets"]},"post":{"description":"Create a new secret in the default provider (encrypted provider only)","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretRequest"}}},"description":"Create secret request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createSecretResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict - Secret already exists"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Create a new secret","tags":["secrets"]}},"/api/v1beta/secrets/default/keys/{key}":{"delete":{"description":"Delete a secret from the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support deletion"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Delete a secret","tags":["secrets"]},"put":{"description":"Update an existing secret in the default provider (encrypted provider only)","parameters":[{"description":"Secret key","in":"path","name":"key","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretRequest"}}},"description":"Update secret request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateSecretResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found - Provider not setup or secret not found"},"405":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Method Not Allowed - Provider doesn't support writing"},"500":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Internal Server Error"}},"summary":"Update a secret","tags":["secrets"]}},"/api/v1beta/version":{"get":{"description":"Returns the current version of the server","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.versionResponse"}}},"description":"OK"}},"summary":"Get server version","tags":["version"]}},"/api/v1beta/workloads":{"get":{"description":"Get a list of all running workloads, optionally filtered by group","parameters":[{"description":"List all workloads, including stopped ones","in":"query","name":"all","schema":{"type":"boolean"}},{"description":"Filter workloads by group name","in":"query","name":"group","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.workloadListResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Group not found"}},"summary":"List all workloads","tags":["workloads"]},"post":{"description":"Create and start a new workload","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"Create workload request","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"409":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Conflict"}},"summary":"Create a new workload","tags":["workloads"]}},"/api/v1beta/workloads/delete":{"post":{"description":"Delete multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk delete request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Delete workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/restart":{"post":{"description":"Restart multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk restart request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Restart workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/stop":{"post":{"description":"Stop multiple workloads by name or by group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.bulkOperationRequest"}}},"description":"Bulk stop request (names or group)","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"}},"summary":"Stop workloads in bulk","tags":["workloads"]}},"/api/v1beta/workloads/{name}":{"delete":{"description":"Delete a workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Delete a workload","tags":["workloads"]},"get":{"description":"Get details of a specific workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createRequest"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload details","tags":["workloads"]}},"/api/v1beta/workloads/{name}/edit":{"post":{"description":"Update an existing workload configuration","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.updateRequest"}}},"description":"Update workload request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.createWorkloadResponse"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Update workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/export":{"get":{"description":"Export a workload's run configuration as JSON","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/runner.RunConfig"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Export workload configuration","tags":["workloads"]}},"/api/v1beta/workloads/{name}/logs":{"get":{"description":"Retrieve at most 100 lines of logs for a specific workload by name.","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}}},"description":"Logs for the specified workload"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get logs for a specific workload","tags":["logs"]}},"/api/v1beta/workloads/{name}/restart":{"post":{"description":"Restart a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Restart a workload","tags":["workloads"]}},"/api/v1beta/workloads/{name}/status":{"get":{"description":"Get the current status of a specific workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.workloadStatusResponse"}}},"description":"OK"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Get workload status","tags":["workloads"]}},"/api/v1beta/workloads/{name}/stop":{"post":{"description":"Stop a running workload","parameters":[{"description":"Workload name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Accepted"},"400":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Bad Request"},"404":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Not Found"}},"summary":"Stop a workload","tags":["workloads"]}},"/health":{"get":{"description":"Check if the API is healthy","responses":{"204":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"No Content"}},"summary":"Health check","tags":["system"]}}}, diff --git a/docs/server/swagger.yaml b/docs/server/swagger.yaml index 77e35de62..2b933b6f6 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -1082,6 +1082,11 @@ components: group: description: Group name this workload belongs to type: string + headers: + items: + $ref: '#/components/schemas/registry.Header' + type: array + uniqueItems: false host: description: Host to bind to type: string @@ -1095,6 +1100,8 @@ components: description: Whether network isolation is turned on. This applies the rules in the permission profile. type: boolean + oauth_config: + $ref: '#/components/schemas/v1.remoteOAuthConfig' oidc: $ref: '#/components/schemas/v1.oidcOptions' permission_profile: @@ -1120,6 +1127,9 @@ components: transport: description: Transport configuration type: string + url: + description: Remote server specific fields + type: string volumes: description: Volume mounts items: @@ -1317,6 +1327,47 @@ components: type: array uniqueItems: false type: object + v1.remoteOAuthConfig: + description: OAuth configuration for remote server authentication + properties: + authorize_url: + description: OAuth authorization endpoint URL (alternative to issuer for + non-OIDC OAuth) + type: string + callback_port: + description: Specific port for OAuth callback server + type: integer + client_id: + description: OAuth client ID for authentication + type: string + client_secret: + $ref: '#/components/schemas/secrets.SecretParameter' + issuer: + description: OAuth/OIDC issuer URL (e.g., https://accounts.google.com) + type: string + oauth_params: + additionalProperties: + type: string + description: Additional OAuth parameters for server-specific customization + type: object + scopes: + description: OAuth scopes to request + items: + type: string + type: array + uniqueItems: false + skip_browser: + description: Whether to skip opening browser for OAuth flow (defaults to + false) + type: boolean + token_url: + description: OAuth token endpoint URL (alternative to issuer for non-OIDC + OAuth) + type: string + use_pkce: + description: Whether to use PKCE for the OAuth flow + type: boolean + type: object v1.secretKeyResponse: description: Secret key information properties: @@ -1369,6 +1420,11 @@ components: group: description: Group name this workload belongs to type: string + headers: + items: + $ref: '#/components/schemas/registry.Header' + type: array + uniqueItems: false host: description: Host to bind to type: string @@ -1379,6 +1435,8 @@ components: description: Whether network isolation is turned on. This applies the rules in the permission profile. type: boolean + oauth_config: + $ref: '#/components/schemas/v1.remoteOAuthConfig' oidc: $ref: '#/components/schemas/v1.oidcOptions' permission_profile: @@ -1404,6 +1462,9 @@ components: transport: description: Transport configuration type: string + url: + description: Remote server specific fields + type: string volumes: description: Volume mounts items: diff --git a/pkg/api/server.go b/pkg/api/server.go index 0e710df90..9f93833ce 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -29,10 +29,12 @@ import ( v1 "github.com/stacklok/toolhive/pkg/api/v1" "github.com/stacklok/toolhive/pkg/auth" "github.com/stacklok/toolhive/pkg/client" + "github.com/stacklok/toolhive/pkg/config" "github.com/stacklok/toolhive/pkg/container" "github.com/stacklok/toolhive/pkg/container/runtime" "github.com/stacklok/toolhive/pkg/groups" "github.com/stacklok/toolhive/pkg/logger" + "github.com/stacklok/toolhive/pkg/secrets" "github.com/stacklok/toolhive/pkg/updates" "github.com/stacklok/toolhive/pkg/workloads" ) @@ -57,6 +59,7 @@ type ServerBuilder struct { clientManager client.Manager workloadManager workloads.Manager groupManager groups.Manager + secretsProvider secrets.Provider } // NewServerBuilder creates a new ServerBuilder with default configuration @@ -133,6 +136,12 @@ func (b *ServerBuilder) WithGroupManager(manager groups.Manager) *ServerBuilder return b } +// WithSecretsProvider sets the secrets provider +func (b *ServerBuilder) WithSecretsProvider(provider secrets.Provider) *ServerBuilder { + b.secretsProvider = provider + return b +} + // Build creates and configures the HTTP router func (b *ServerBuilder) Build(ctx context.Context) (*chi.Mux, error) { r := chi.NewRouter() @@ -207,20 +216,49 @@ func (b *ServerBuilder) createDefaultManagers(ctx context.Context) error { return fmt.Errorf("failed to create group manager: %v", err) } } + if b.secretsProvider == nil { + b.secretsProvider, err = getSecretsManager() + if err != nil { + return fmt.Errorf("failed to create secrets provider: %v", err) + } + } return nil } +// getSecretsManager is a helper function to get the secrets manager +func getSecretsManager() (secrets.Provider, error) { + cfg := config.NewDefaultProvider().GetConfig() + + // Check if secrets setup has been completed + if !cfg.Secrets.SetupCompleted { + return nil, secrets.ErrSecretsNotSetup + } + + providerType, err := cfg.Secrets.GetProviderType() + if err != nil { + return nil, err + } + + return secrets.CreateSecretProvider(providerType) +} + // setupDefaultRoutes sets up the default API routes func (b *ServerBuilder) setupDefaultRoutes(r *chi.Mux) { routers := map[string]http.Handler{ - "/health": v1.HealthcheckRouter(b.containerRuntime), - "/api/v1beta/version": v1.VersionRouter(), - "/api/v1beta/workloads": v1.WorkloadRouter(b.workloadManager, b.containerRuntime, b.groupManager, b.debugMode), + "/health": v1.HealthcheckRouter(b.containerRuntime), + "/api/v1beta/version": v1.VersionRouter(), + "/api/v1beta/workloads": v1.WorkloadRouter( + b.workloadManager, + b.containerRuntime, + b.groupManager, + b.secretsProvider, + b.debugMode, + ), "/api/v1beta/registry": v1.RegistryRouter(), "/api/v1beta/discovery": v1.DiscoveryRouter(), "/api/v1beta/clients": v1.ClientRouter(b.clientManager, b.workloadManager, b.groupManager), - "/api/v1beta/secrets": v1.SecretsRouter(), + "/api/v1beta/secrets": v1.SecretsRouter(b.secretsProvider), "/api/v1beta/groups": v1.GroupsRouter(b.groupManager, b.workloadManager, b.clientManager), } diff --git a/pkg/api/v1/secrets.go b/pkg/api/v1/secrets.go index a19f34fa9..9e94b05d3 100644 --- a/pkg/api/v1/secrets.go +++ b/pkg/api/v1/secrets.go @@ -22,12 +22,14 @@ const ( // SecretsRoutes defines the routes for the secrets API. type SecretsRoutes struct { configProvider config.Provider + provider secrets.Provider } // NewSecretsRoutes creates a new SecretsRoutes with the default config provider -func NewSecretsRoutes() *SecretsRoutes { +func NewSecretsRoutes(provider secrets.Provider) *SecretsRoutes { return &SecretsRoutes{ configProvider: config.NewDefaultProvider(), + provider: provider, } } @@ -39,8 +41,8 @@ func NewSecretsRoutesWithProvider(provider config.Provider) *SecretsRoutes { } // SecretsRouter creates a new router for the secrets API. -func SecretsRouter() http.Handler { - routes := NewSecretsRoutes() +func SecretsRouter(provider secrets.Provider) http.Handler { + routes := NewSecretsRoutes(provider) return secretsRouterWithRoutes(routes) } @@ -233,15 +235,7 @@ func (s *SecretsRoutes) getSecretsProvider(w http.ResponseWriter, _ *http.Reques return } - // Get provider capabilities - provider, err := s.getSecretsManager() - if err != nil { - logger.Errorf("Failed to create secrets provider: %v", err) - http.Error(w, "Failed to access secrets provider", http.StatusInternalServerError) - return - } - - capabilities := provider.Capabilities() + capabilities := s.provider.Capabilities() w.Header().Set("Content-Type", "application/json") resp := getSecretsProviderResponse{ @@ -274,24 +268,14 @@ func (s *SecretsRoutes) getSecretsProvider(w http.ResponseWriter, _ *http.Reques // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1beta/secrets/default/keys [get] func (s *SecretsRoutes) listSecrets(w http.ResponseWriter, r *http.Request) { - provider, err := s.getSecretsManager() - if err != nil { - if errors.Is(err, secrets.ErrSecretsNotSetup) { - http.Error(w, "Secrets provider not setup", http.StatusNotFound) - return - } - logger.Errorf("Failed to get secrets manager: %v", err) - http.Error(w, "Failed to access secrets provider", http.StatusInternalServerError) - return - } // Check if provider supports listing - if !provider.Capabilities().CanList { + if !s.provider.Capabilities().CanList { http.Error(w, "Secrets provider does not support listing keys", http.StatusMethodNotAllowed) return } - secretDescriptions, err := provider.ListSecrets(r.Context()) + secretDescriptions, err := s.provider.ListSecrets(r.Context()) if err != nil { logger.Errorf("Failed to list secrets: %v", err) http.Error(w, "Failed to list secrets", http.StatusInternalServerError) @@ -343,26 +327,15 @@ func (s *SecretsRoutes) createSecret(w http.ResponseWriter, r *http.Request) { return } - provider, err := s.getSecretsManager() - if err != nil { - if errors.Is(err, secrets.ErrSecretsNotSetup) { - http.Error(w, "Secrets provider not setup", http.StatusNotFound) - return - } - logger.Errorf("Failed to get secrets manager: %v", err) - http.Error(w, "Failed to access secrets provider", http.StatusInternalServerError) - return - } - // Check if provider supports writing - if !provider.Capabilities().CanWrite { + if !s.provider.Capabilities().CanWrite { http.Error(w, "Secrets provider does not support creating secrets", http.StatusMethodNotAllowed) return } // Check if secret already exists (if provider supports reading) - if provider.Capabilities().CanRead { - _, err := provider.GetSecret(r.Context(), req.Key) + if s.provider.Capabilities().CanRead { + _, err := s.provider.GetSecret(r.Context(), req.Key) if err == nil { http.Error(w, "Secret already exists", http.StatusConflict) return @@ -370,7 +343,7 @@ func (s *SecretsRoutes) createSecret(w http.ResponseWriter, r *http.Request) { } // Create the secret - if err := provider.SetSecret(r.Context(), req.Key, req.Value); err != nil { + if err := s.provider.SetSecret(r.Context(), req.Key, req.Value); err != nil { logger.Errorf("Failed to create secret: %v", err) http.Error(w, "Failed to create secret", http.StatusInternalServerError) return @@ -423,26 +396,15 @@ func (s *SecretsRoutes) updateSecret(w http.ResponseWriter, r *http.Request) { return } - provider, err := s.getSecretsManager() - if err != nil { - if errors.Is(err, secrets.ErrSecretsNotSetup) { - http.Error(w, "Secrets provider not setup", http.StatusNotFound) - return - } - logger.Errorf("Failed to get secrets manager: %v", err) - http.Error(w, "Failed to access secrets provider", http.StatusInternalServerError) - return - } - // Check if provider supports writing - if !provider.Capabilities().CanWrite { + if !s.provider.Capabilities().CanWrite { http.Error(w, "Secrets provider does not support updating secrets", http.StatusMethodNotAllowed) return } // Check if secret exists (if provider supports reading) - if provider.Capabilities().CanRead { - _, err := provider.GetSecret(r.Context(), key) + if s.provider.Capabilities().CanRead { + _, err := s.provider.GetSecret(r.Context(), key) if err != nil { http.Error(w, "Secret not found", http.StatusNotFound) return @@ -450,7 +412,7 @@ func (s *SecretsRoutes) updateSecret(w http.ResponseWriter, r *http.Request) { } // Update the secret - if err := provider.SetSecret(r.Context(), key, req.Value); err != nil { + if err := s.provider.SetSecret(r.Context(), key, req.Value); err != nil { logger.Errorf("Failed to update secret: %v", err) http.Error(w, "Failed to update secret", http.StatusInternalServerError) return @@ -486,25 +448,14 @@ func (s *SecretsRoutes) deleteSecret(w http.ResponseWriter, r *http.Request) { return } - provider, err := s.getSecretsManager() - if err != nil { - if errors.Is(err, secrets.ErrSecretsNotSetup) { - http.Error(w, "Secrets provider not setup", http.StatusNotFound) - return - } - logger.Errorf("Failed to get secrets manager: %v", err) - http.Error(w, "Failed to access secrets provider", http.StatusInternalServerError) - return - } - // Check if provider supports deletion - if !provider.Capabilities().CanDelete { + if !s.provider.Capabilities().CanDelete { http.Error(w, "Secrets provider does not support deleting secrets", http.StatusMethodNotAllowed) return } // Delete the secret - if err := provider.DeleteSecret(r.Context(), key); err != nil { + if err := s.provider.DeleteSecret(r.Context(), key); err != nil { logger.Errorf("Failed to delete secret: %v", err) // Check if it's a "not found" error if err.Error() == "cannot delete non-existent secret: "+key { @@ -518,23 +469,6 @@ func (s *SecretsRoutes) deleteSecret(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -// getSecretsManager is a helper function to get the secrets manager -func (s *SecretsRoutes) getSecretsManager() (secrets.Provider, error) { - cfg := s.configProvider.GetConfig() - - // Check if secrets setup has been completed - if !cfg.Secrets.SetupCompleted { - return nil, secrets.ErrSecretsNotSetup - } - - providerType, err := cfg.Secrets.GetProviderType() - if err != nil { - return nil, err - } - - return secrets.CreateSecretProvider(providerType) -} - // Request and response type definitions // setupSecretsRequest represents the request for initializing a secrets provider diff --git a/pkg/api/v1/workload_service.go b/pkg/api/v1/workload_service.go new file mode 100644 index 000000000..9b8975830 --- /dev/null +++ b/pkg/api/v1/workload_service.go @@ -0,0 +1,229 @@ +package v1 + +import ( + "context" + "fmt" + + "github.com/stacklok/toolhive/pkg/container/runtime" + "github.com/stacklok/toolhive/pkg/groups" + "github.com/stacklok/toolhive/pkg/logger" + "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/runner" + "github.com/stacklok/toolhive/pkg/runner/retriever" + "github.com/stacklok/toolhive/pkg/secrets" + "github.com/stacklok/toolhive/pkg/transport" + "github.com/stacklok/toolhive/pkg/transport/types" + "github.com/stacklok/toolhive/pkg/validation" + "github.com/stacklok/toolhive/pkg/workloads" +) + +// WorkloadService handles business logic for workload operations +type WorkloadService struct { + workloadManager workloads.Manager + groupManager groups.Manager + secretsProvider secrets.Provider + containerRuntime runtime.Runtime + debugMode bool +} + +// NewWorkloadService creates a new WorkloadService instance +func NewWorkloadService( + workloadManager workloads.Manager, + groupManager groups.Manager, + secretsProvider secrets.Provider, + containerRuntime runtime.Runtime, + debugMode bool, +) *WorkloadService { + return &WorkloadService{ + workloadManager: workloadManager, + groupManager: groupManager, + secretsProvider: secretsProvider, + containerRuntime: containerRuntime, + debugMode: debugMode, + } +} + +// CreateWorkloadFromRequest creates a workload from a request +func (s *WorkloadService) CreateWorkloadFromRequest(ctx context.Context, req *createRequest) (*runner.RunConfig, error) { + // Default group if not specified + groupName := req.Group + if groupName == "" { + groupName = groups.DefaultGroup + } + + // Validate that the group exists + exists, err := s.groupManager.Exists(ctx, groupName) + if err != nil { + return nil, fmt.Errorf("failed to check if group exists: %w", err) + } + if !exists { + return nil, fmt.Errorf("group '%s' does not exist", groupName) + } + + var remoteAuthConfig *runner.RemoteAuthConfig + var imageURL string + var imageMetadata *registry.ImageMetadata + + if req.URL != "" { + // Configure remote authentication if OAuth config is provided + if req.Transport == "" { + req.Transport = types.TransportTypeStreamableHTTP.String() + } + remoteAuthConfig, err = s.createRequestToRemoteAuthConfig(ctx, req) + if err != nil { + return nil, err + } + } else { + var serverMetadata registry.ServerMetadata + // Fetch or build the requested image + imageURL, serverMetadata, err = retriever.GetMCPServer( + ctx, + req.Image, + "", // We do not let the user specify a CA cert path here. + retriever.VerifyImageWarn, + ) + if err != nil { + return nil, fmt.Errorf("failed to retrieve MCP server image: %w", err) + } + + if remoteServerMetadata, ok := serverMetadata.(*registry.RemoteServerMetadata); ok { + if remoteServerMetadata.OAuthConfig != nil { + clientSecret, err := s.resolveClientSecret(ctx, req.OAuthConfig.ClientSecret) + if err != nil { + return nil, err + } + remoteAuthConfig = &runner.RemoteAuthConfig{ + ClientID: req.OAuthConfig.ClientID, + ClientSecret: clientSecret, + Scopes: remoteServerMetadata.OAuthConfig.Scopes, + CallbackPort: remoteServerMetadata.OAuthConfig.CallbackPort, + Issuer: remoteServerMetadata.OAuthConfig.Issuer, + AuthorizeURL: remoteServerMetadata.OAuthConfig.AuthorizeURL, + TokenURL: remoteServerMetadata.OAuthConfig.TokenURL, + OAuthParams: remoteServerMetadata.OAuthConfig.OAuthParams, + Headers: remoteServerMetadata.Headers, + EnvVars: remoteServerMetadata.EnvVars, + } + } + } + // Handle server metadata - API only supports container servers + imageMetadata, _ = serverMetadata.(*registry.ImageMetadata) + } + + // Build RunConfig + runSecrets := secrets.SecretParametersToCLI(req.Secrets) + + runConfig, err := runner.NewRunConfigBuilder(). + WithRuntime(s.containerRuntime). + WithCmdArgs(req.CmdArguments). + WithName(req.Name). + WithGroup(groupName). + WithImage(imageURL). + WithRemoteURL(req.URL). + WithRemoteAuth(remoteAuthConfig). + WithHost(req.Host). + WithTargetHost(transport.LocalhostIPv4). + WithDebug(s.debugMode). + WithVolumes(req.Volumes). + WithSecrets(runSecrets). + WithAuthzConfigPath(req.AuthzConfig). + WithAuditConfigPath(""). + WithPermissionProfile(req.PermissionProfile). + WithNetworkIsolation(req.NetworkIsolation). + WithK8sPodPatch(""). + WithProxyMode(types.ProxyMode(req.ProxyMode)). + WithTransportAndPorts(req.Transport, 0, req.TargetPort). + WithAuditEnabled(false, ""). + WithOIDCConfig(req.OIDC.Issuer, req.OIDC.Audience, req.OIDC.JwksURL, req.OIDC.ClientID, + "", "", "", "", "", false). + WithTelemetryConfig("", false, "", 0.0, nil, false, nil). + WithToolsFilter(req.ToolsFilter). + Build(ctx, imageMetadata, req.EnvVars, &runner.DetachedEnvVarValidator{}) + + if err != nil { + logger.Errorf("Failed to build run config: %v", err) + return nil, fmt.Errorf("%w: Failed to build run config: %v", retriever.ErrInvalidRunConfig, err) + } + // Save the workload state + if err := runConfig.SaveState(ctx); err != nil { + logger.Errorf("Failed to save workload config: %v", err) + return nil, fmt.Errorf("failed to save workload config: %w", err) + } + + // Start workload + if err := s.workloadManager.RunWorkloadDetached(ctx, runConfig); err != nil { + logger.Errorf("Failed to start workload: %v", err) + return nil, fmt.Errorf("failed to start workload: %w", err) + } + + return runConfig, nil +} + +// createRequestToRemoteAuthConfig converts API request to runner RemoteAuthConfig +func (s *WorkloadService) createRequestToRemoteAuthConfig( + ctx context.Context, + req *createRequest, +) (*runner.RemoteAuthConfig, error) { + + // Resolve client secret from secret management if provided + clientSecret, err := s.resolveClientSecret(ctx, req.OAuthConfig.ClientSecret) + if err != nil { + return nil, err + } + + // Create RemoteAuthConfig + return &runner.RemoteAuthConfig{ + ClientID: req.OAuthConfig.ClientID, + ClientSecret: clientSecret, + Scopes: req.OAuthConfig.Scopes, + Issuer: req.OAuthConfig.Issuer, + AuthorizeURL: req.OAuthConfig.AuthorizeURL, + TokenURL: req.OAuthConfig.TokenURL, + OAuthParams: req.OAuthConfig.OAuthParams, + CallbackPort: req.OAuthConfig.CallbackPort, + SkipBrowser: req.OAuthConfig.SkipBrowser, + Headers: req.Headers, + }, nil +} + +// resolveClientSecret resolves client secret from secret management +func (s *WorkloadService) resolveClientSecret(ctx context.Context, secretParam *secrets.SecretParameter) (string, error) { + var clientSecret string + if secretParam != nil { + // Get the secret from the secrets manager + secretValue, err := s.secretsProvider.GetSecret(ctx, secretParam.Name) + if err != nil { + return "", fmt.Errorf("failed to resolve OAuth client secret: %w", err) + } + clientSecret = secretValue + } + return clientSecret, nil +} + +// GetWorkloadNamesFromRequest gets workload names from either the names field or group +func (s *WorkloadService) GetWorkloadNamesFromRequest(ctx context.Context, req bulkOperationRequest) ([]string, error) { + if len(req.Names) > 0 { + return req.Names, nil + } + + if err := validation.ValidateGroupName(req.Group); err != nil { + return nil, fmt.Errorf("invalid group name: %w", err) + } + + // Check if the group exists + exists, err := s.groupManager.Exists(ctx, req.Group) + if err != nil { + return nil, fmt.Errorf("failed to check if group exists: %w", err) + } + if !exists { + return nil, fmt.Errorf("group '%s' does not exist", req.Group) + } + + // Get all workload names in the group + workloadNames, err := s.workloadManager.ListWorkloadsInGroup(ctx, req.Group) + if err != nil { + return nil, fmt.Errorf("failed to list workloads in group: %w", err) + } + + return workloadNames, nil +} diff --git a/pkg/api/v1/workload_service_test.go b/pkg/api/v1/workload_service_test.go new file mode 100644 index 000000000..c601f1c94 --- /dev/null +++ b/pkg/api/v1/workload_service_test.go @@ -0,0 +1,287 @@ +package v1 + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + groupsmocks "github.com/stacklok/toolhive/pkg/groups/mocks" + "github.com/stacklok/toolhive/pkg/secrets" + secretsmocks "github.com/stacklok/toolhive/pkg/secrets/mocks" + workloadsmocks "github.com/stacklok/toolhive/pkg/workloads/mocks" +) + +func TestWorkloadService_resolveClientSecret(t *testing.T) { + t.Parallel() + + t.Run("with secret parameter", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSecretsProvider := secretsmocks.NewMockProvider(ctrl) + mockSecretsProvider.EXPECT(). + GetSecret(gomock.Any(), "secret-name"). + Return("secret-value", nil) + + service := &WorkloadService{ + secretsProvider: mockSecretsProvider, + } + + secretParam := &secrets.SecretParameter{Name: "secret-name"} + + result, err := service.resolveClientSecret(context.Background(), secretParam) + + require.NoError(t, err) + assert.Equal(t, "secret-value", result) + }) + + t.Run("without secret parameter", func(t *testing.T) { + t.Parallel() + + service := &WorkloadService{} + + result, err := service.resolveClientSecret(context.Background(), nil) + + require.NoError(t, err) + assert.Equal(t, "", result) + }) + + t.Run("secret provider error", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSecretsProvider := secretsmocks.NewMockProvider(ctrl) + mockSecretsProvider.EXPECT(). + GetSecret(gomock.Any(), "non-existent-secret"). + Return("", errors.New("secret not found")) + + service := &WorkloadService{ + secretsProvider: mockSecretsProvider, + } + + secretParam := &secrets.SecretParameter{Name: "non-existent-secret"} + + result, err := service.resolveClientSecret(context.Background(), secretParam) + + assert.Error(t, err) + assert.Equal(t, "", result) + assert.Contains(t, err.Error(), "failed to resolve OAuth client secret") + }) +} + +func TestWorkloadService_createRequestToRemoteAuthConfig(t *testing.T) { + t.Parallel() + + t.Run("with OAuth config", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSecretsProvider := secretsmocks.NewMockProvider(ctrl) + mockSecretsProvider.EXPECT(). + GetSecret(gomock.Any(), "secret-name"). + Return("secret-value", nil) + + service := &WorkloadService{ + secretsProvider: mockSecretsProvider, + } + + req := &createRequest{ + updateRequest: updateRequest{ + OAuthConfig: remoteOAuthConfig{ + ClientID: "client-id", + ClientSecret: &secrets.SecretParameter{Name: "secret-name"}, + Scopes: []string{"read", "write"}, + Issuer: "https://oauth.example.com", + AuthorizeURL: "https://oauth.example.com/auth", + TokenURL: "https://oauth.example.com/token", + OAuthParams: map[string]string{"custom": "param"}, + CallbackPort: 8081, + }, + }, + } + + result, err := service.createRequestToRemoteAuthConfig(context.Background(), req) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "client-id", result.ClientID) + assert.Equal(t, "secret-value", result.ClientSecret) + assert.Equal(t, []string{"read", "write"}, result.Scopes) + assert.Equal(t, "https://oauth.example.com", result.Issuer) + assert.Equal(t, "https://oauth.example.com/auth", result.AuthorizeURL) + assert.Equal(t, "https://oauth.example.com/token", result.TokenURL) + assert.Equal(t, map[string]string{"custom": "param"}, result.OAuthParams) + assert.Equal(t, 8081, result.CallbackPort) + }) + + t.Run("secret resolution fails", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSecretsProvider := secretsmocks.NewMockProvider(ctrl) + mockSecretsProvider.EXPECT(). + GetSecret(gomock.Any(), "secret-name"). + Return("", errors.New("secret not found")) + + service := &WorkloadService{ + secretsProvider: mockSecretsProvider, + } + + req := &createRequest{ + updateRequest: updateRequest{ + OAuthConfig: remoteOAuthConfig{ + ClientSecret: &secrets.SecretParameter{Name: "secret-name"}, + }, + }, + } + + result, err := service.createRequestToRemoteAuthConfig(context.Background(), req) + + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "failed to resolve OAuth client secret") + }) +} + +func TestWorkloadService_GetWorkloadNamesFromRequest(t *testing.T) { + t.Parallel() + + t.Run("with names", func(t *testing.T) { + t.Parallel() + + service := &WorkloadService{} + + req := bulkOperationRequest{ + Names: []string{"workload1", "workload2"}, + } + + result, err := service.GetWorkloadNamesFromRequest(context.Background(), req) + + require.NoError(t, err) + assert.Equal(t, []string{"workload1", "workload2"}, result) + }) + + t.Run("with group", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGroupManager := groupsmocks.NewMockManager(ctrl) + mockGroupManager.EXPECT(). + Exists(gomock.Any(), "test-group"). + Return(true, nil) + + mockWorkloadManager := workloadsmocks.NewMockManager(ctrl) + mockWorkloadManager.EXPECT(). + ListWorkloadsInGroup(gomock.Any(), "test-group"). + Return([]string{"workload1", "workload2"}, nil) + + service := &WorkloadService{ + groupManager: mockGroupManager, + workloadManager: mockWorkloadManager, + } + + req := bulkOperationRequest{ + Group: "test-group", + } + + result, err := service.GetWorkloadNamesFromRequest(context.Background(), req) + + require.NoError(t, err) + assert.Equal(t, []string{"workload1", "workload2"}, result) + }) + + t.Run("invalid group name", func(t *testing.T) { + t.Parallel() + + service := &WorkloadService{} + + req := bulkOperationRequest{ + Group: "invalid-group-name-with-special-chars!@#", + } + + result, err := service.GetWorkloadNamesFromRequest(context.Background(), req) + + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "invalid group name") + }) + + t.Run("group does not exist", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGroupManager := groupsmocks.NewMockManager(ctrl) + mockGroupManager.EXPECT(). + Exists(gomock.Any(), "non-existent-group"). + Return(false, nil) + + service := &WorkloadService{ + groupManager: mockGroupManager, + } + + req := bulkOperationRequest{ + Group: "non-existent-group", + } + + result, err := service.GetWorkloadNamesFromRequest(context.Background(), req) + + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "group 'non-existent-group' does not exist") + }) + + t.Run("list workloads error", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGroupManager := groupsmocks.NewMockManager(ctrl) + mockGroupManager.EXPECT(). + Exists(gomock.Any(), "test-group"). + Return(true, nil) + + mockWorkloadManager := workloadsmocks.NewMockManager(ctrl) + mockWorkloadManager.EXPECT(). + ListWorkloadsInGroup(gomock.Any(), "test-group"). + Return(nil, errors.New("database error")) + + service := &WorkloadService{ + groupManager: mockGroupManager, + workloadManager: mockWorkloadManager, + } + + req := bulkOperationRequest{ + Group: "test-group", + } + + result, err := service.GetWorkloadNamesFromRequest(context.Background(), req) + + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "failed to list workloads in group") + }) +} + +func TestNewWorkloadService(t *testing.T) { + t.Parallel() + + service := NewWorkloadService(nil, nil, nil, nil, false) + require.NotNil(t, service) +} diff --git a/pkg/api/v1/workload_types.go b/pkg/api/v1/workload_types.go new file mode 100644 index 000000000..78ae850a7 --- /dev/null +++ b/pkg/api/v1/workload_types.go @@ -0,0 +1,226 @@ +package v1 + +import ( + "fmt" + + "github.com/stacklok/toolhive/pkg/container/runtime" + "github.com/stacklok/toolhive/pkg/core" + "github.com/stacklok/toolhive/pkg/permissions" + "github.com/stacklok/toolhive/pkg/registry" + "github.com/stacklok/toolhive/pkg/runner" + "github.com/stacklok/toolhive/pkg/secrets" +) + +// workloadListResponse represents the response for listing workloads +// +// @Description Response containing a list of workloads +type workloadListResponse struct { + // List of container information for each workload + Workloads []core.Workload `json:"workloads"` +} + +// workloadStatusResponse represents the response for getting workload status +// +// @Description Response containing workload status information +type workloadStatusResponse struct { + // Current status of the workload + Status runtime.WorkloadStatus `json:"status"` +} + +// updateRequest represents the request to update an existing workload +// +// @Description Request to update an existing workload (name cannot be changed) +type updateRequest struct { + // Docker image to use + Image string `json:"image"` + // Host to bind to + Host string `json:"host"` + // Command arguments to pass to the container + CmdArguments []string `json:"cmd_arguments"` + // Port to expose from the container + TargetPort int `json:"target_port"` + // Environment variables to set in the container + EnvVars map[string]string `json:"env_vars"` + // Secret parameters to inject + Secrets []secrets.SecretParameter `json:"secrets"` + // Volume mounts + Volumes []string `json:"volumes"` + // Transport configuration + Transport string `json:"transport"` + // Authorization configuration + AuthzConfig string `json:"authz_config"` + // OIDC configuration options + OIDC oidcOptions `json:"oidc"` + // Permission profile to apply + PermissionProfile *permissions.Profile `json:"permission_profile"` + // Proxy mode to use + ProxyMode string `json:"proxy_mode"` + // Whether network isolation is turned on. This applies the rules in the permission profile. + NetworkIsolation bool `json:"network_isolation"` + // Tools filter + ToolsFilter []string `json:"tools"` + // Group name this workload belongs to + Group string `json:"group,omitempty"` + + // Remote server specific fields + URL string `json:"url,omitempty"` + OAuthConfig remoteOAuthConfig `json:"oauth_config,omitempty"` + Headers []*registry.Header `json:"headers,omitempty"` +} + +// remoteOAuthConfig represents OAuth configuration for remote servers +// +// @Description OAuth configuration for remote server authentication +// +// @name remoteOAuthConfig +type remoteOAuthConfig struct { + // OAuth/OIDC issuer URL (e.g., https://accounts.google.com) + Issuer string `json:"issuer,omitempty"` + // OAuth authorization endpoint URL (alternative to issuer for non-OIDC OAuth) + AuthorizeURL string `json:"authorize_url,omitempty"` + // OAuth token endpoint URL (alternative to issuer for non-OIDC OAuth) + TokenURL string `json:"token_url,omitempty"` + // OAuth client ID for authentication + ClientID string `json:"client_id,omitempty"` + ClientSecret *secrets.SecretParameter `json:"client_secret,omitempty"` + + // OAuth scopes to request + Scopes []string `json:"scopes,omitempty"` + // Whether to use PKCE for the OAuth flow + UsePKCE bool `json:"use_pkce,omitempty"` + // Additional OAuth parameters for server-specific customization + OAuthParams map[string]string `json:"oauth_params,omitempty"` + // Specific port for OAuth callback server + CallbackPort int `json:"callback_port,omitempty"` + // Whether to skip opening browser for OAuth flow (defaults to false) + SkipBrowser bool `json:"skip_browser,omitempty"` +} + +// createRequest represents the request to create a new workload +// +// @Description Request to create a new workload +type createRequest struct { + updateRequest + // Name of the workload + Name string `json:"name"` +} + +// oidcOptions represents OIDC configuration options +// +// @Description OIDC configuration for workload authentication +type oidcOptions struct { + // OIDC issuer URL + Issuer string `json:"issuer"` + // Expected audience + Audience string `json:"audience"` + // JWKS URL for key verification + JwksURL string `json:"jwks_url"` + // Token introspection URL for OIDC + IntrospectionURL string `json:"introspection_url"` + // OAuth2 client ID + ClientID string `json:"client_id"` + // OAuth2 client secret + ClientSecret string `json:"client_secret"` +} + +// createWorkloadResponse represents the response for workload creation +// +// @Description Response after successfully creating a workload +type createWorkloadResponse struct { + // Name of the created workload + Name string `json:"name"` + // Port the workload is listening on + Port int `json:"port"` +} + +// bulkOperationRequest represents a request for bulk operations on workloads +type bulkOperationRequest struct { + // Names of the workloads to operate on + Names []string `json:"names"` + // Group name to operate on (mutually exclusive with names) + Group string `json:"group,omitempty"` +} + +// validateBulkOperationRequest validates the bulk operation request +func validateBulkOperationRequest(req bulkOperationRequest) error { + if len(req.Names) > 0 && req.Group != "" { + return fmt.Errorf("cannot specify both names and group") + } + if len(req.Names) == 0 && req.Group == "" { + return fmt.Errorf("must specify either names or group") + } + return nil +} + +// runConfigToCreateRequest converts a RunConfig to createRequest for API responses +func runConfigToCreateRequest(runConfig *runner.RunConfig) *createRequest { + if runConfig == nil { + return nil + } + + // Convert CLI secrets ([]string) back to SecretParameters + secretParams := make([]secrets.SecretParameter, 0, len(runConfig.Secrets)) + for _, secretStr := range runConfig.Secrets { + // Parse the CLI format: ",target=" + if secretParam, err := secrets.ParseSecretParameter(secretStr); err == nil { + secretParams = append(secretParams, secretParam) + } + // Ignore invalid secrets rather than failing the entire conversion + } + + // Get OIDC fields from RunConfig + var oidcConfig oidcOptions + if runConfig.OIDCConfig != nil { + oidcConfig = oidcOptions{ + Issuer: runConfig.OIDCConfig.Issuer, + Audience: runConfig.OIDCConfig.Audience, + JwksURL: runConfig.OIDCConfig.JWKSURL, + IntrospectionURL: runConfig.OIDCConfig.IntrospectionURL, + ClientID: runConfig.OIDCConfig.ClientID, + ClientSecret: runConfig.OIDCConfig.ClientSecret, + } + } + + // Get remote OAuth config from RunConfig + var oAuthConfig remoteOAuthConfig + var headers []*registry.Header + if runConfig.RemoteAuthConfig != nil { + oAuthConfig = remoteOAuthConfig{ + Issuer: runConfig.RemoteAuthConfig.Issuer, + AuthorizeURL: runConfig.RemoteAuthConfig.AuthorizeURL, + TokenURL: runConfig.RemoteAuthConfig.TokenURL, + ClientID: runConfig.RemoteAuthConfig.ClientID, + Scopes: runConfig.RemoteAuthConfig.Scopes, + OAuthParams: runConfig.RemoteAuthConfig.OAuthParams, + CallbackPort: runConfig.RemoteAuthConfig.CallbackPort, + SkipBrowser: runConfig.RemoteAuthConfig.SkipBrowser, + } + headers = runConfig.RemoteAuthConfig.Headers + } + + authzConfigPath := "" + + return &createRequest{ + updateRequest: updateRequest{ + Image: runConfig.Image, + Host: runConfig.Host, + CmdArguments: runConfig.CmdArgs, + TargetPort: runConfig.TargetPort, + EnvVars: runConfig.EnvVars, + Secrets: secretParams, + Volumes: runConfig.Volumes, + Transport: string(runConfig.Transport), + AuthzConfig: authzConfigPath, + OIDC: oidcConfig, + PermissionProfile: runConfig.PermissionProfile, + ProxyMode: string(runConfig.ProxyMode), + NetworkIsolation: runConfig.IsolateNetwork, + ToolsFilter: runConfig.ToolsFilter, + Group: runConfig.Group, + URL: runConfig.RemoteURL, + OAuthConfig: oAuthConfig, + Headers: headers, + }, + Name: runConfig.Name, + } +} diff --git a/pkg/api/v1/workloads.go b/pkg/api/v1/workloads.go index adaf4f46c..0733076af 100644 --- a/pkg/api/v1/workloads.go +++ b/pkg/api/v1/workloads.go @@ -1,7 +1,6 @@ package v1 import ( - "context" "encoding/json" "errors" "fmt" @@ -10,16 +9,12 @@ import ( "github.com/go-chi/chi/v5" "github.com/stacklok/toolhive/pkg/container/runtime" - "github.com/stacklok/toolhive/pkg/core" thverrors "github.com/stacklok/toolhive/pkg/errors" "github.com/stacklok/toolhive/pkg/groups" "github.com/stacklok/toolhive/pkg/logger" - "github.com/stacklok/toolhive/pkg/permissions" - "github.com/stacklok/toolhive/pkg/registry" "github.com/stacklok/toolhive/pkg/runner" "github.com/stacklok/toolhive/pkg/runner/retriever" "github.com/stacklok/toolhive/pkg/secrets" - "github.com/stacklok/toolhive/pkg/transport" "github.com/stacklok/toolhive/pkg/transport/types" "github.com/stacklok/toolhive/pkg/validation" "github.com/stacklok/toolhive/pkg/workloads" @@ -32,6 +27,8 @@ type WorkloadRoutes struct { containerRuntime runtime.Runtime debugMode bool groupManager groups.Manager + secretsProvider secrets.Provider + workloadService *WorkloadService } // @title ToolHive API @@ -45,13 +42,24 @@ func WorkloadRouter( workloadManager workloads.Manager, containerRuntime runtime.Runtime, groupManager groups.Manager, + secretsProvider secrets.Provider, debugMode bool, ) http.Handler { + workloadService := NewWorkloadService( + workloadManager, + groupManager, + secretsProvider, + containerRuntime, + debugMode, + ) + routes := WorkloadRoutes{ workloadManager: workloadManager, containerRuntime: containerRuntime, debugMode: debugMode, groupManager: groupManager, + secretsProvider: secretsProvider, + workloadService: workloadService, } r := chi.NewRouter() @@ -295,10 +303,10 @@ func (s *WorkloadRoutes) createWorkload(w http.ResponseWriter, r *http.Request) } // Create the workload using shared logic - runConfig, err := s.createWorkloadFromRequest(ctx, &req) + runConfig, err := s.workloadService.CreateWorkloadFromRequest(ctx, &req) if err != nil { // Error messages already logged in createWorkloadFromRequest - if errors.Is(err, retriever.ErrImageNotFound) || err.Error() == "MCP server image not found" { + if errors.Is(err, retriever.ErrImageNotFound) { http.Error(w, err.Error(), http.StatusNotFound) } else if errors.Is(err, retriever.ErrInvalidRunConfig) { http.Error(w, err.Error(), http.StatusBadRequest) @@ -374,7 +382,7 @@ func (s *WorkloadRoutes) updateWorkload(w http.ResponseWriter, r *http.Request) } // Create the new workload using shared logic - runConfig, err := s.createWorkloadFromRequest(ctx, &createReq) + runConfig, err := s.workloadService.CreateWorkloadFromRequest(ctx, &createReq) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -416,7 +424,7 @@ func (s *WorkloadRoutes) stopWorkloadsBulk(w http.ResponseWriter, r *http.Reques return } - workloadNames, err := s.getWorkloadNamesFromRequest(ctx, req) + workloadNames, err := s.workloadService.GetWorkloadNamesFromRequest(ctx, req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -461,7 +469,7 @@ func (s *WorkloadRoutes) restartWorkloadsBulk(w http.ResponseWriter, r *http.Req return } - workloadNames, err := s.getWorkloadNamesFromRequest(ctx, req) + workloadNames, err := s.workloadService.GetWorkloadNamesFromRequest(ctx, req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -507,7 +515,7 @@ func (s *WorkloadRoutes) deleteWorkloadsBulk(w http.ResponseWriter, r *http.Requ return } - workloadNames, err := s.getWorkloadNamesFromRequest(ctx, req) + workloadNames, err := s.workloadService.GetWorkloadNamesFromRequest(ctx, req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -636,269 +644,3 @@ func (*WorkloadRoutes) exportWorkload(w http.ResponseWriter, r *http.Request) { } // Response type definitions. - -// workloadListResponse represents the response for listing workloads -// -// @Description Response containing a list of workloads -type workloadListResponse struct { - // List of container information for each workload - Workloads []core.Workload `json:"workloads"` -} - -// workloadStatusResponse represents the response for getting workload status -// -// @Description Response containing workload status information -type workloadStatusResponse struct { - // Current status of the workload - Status runtime.WorkloadStatus `json:"status"` -} - -// updateRequest represents the request to update an existing workload -// -// @Description Request to update an existing workload (name cannot be changed) -type updateRequest struct { - // Docker image to use - Image string `json:"image"` - // Host to bind to - Host string `json:"host"` - // Command arguments to pass to the container - CmdArguments []string `json:"cmd_arguments"` - // Port to expose from the container - TargetPort int `json:"target_port"` - // Environment variables to set in the container - EnvVars map[string]string `json:"env_vars"` - // Secret parameters to inject - Secrets []secrets.SecretParameter `json:"secrets"` - // Volume mounts - Volumes []string `json:"volumes"` - // Transport configuration - Transport string `json:"transport"` - // Authorization configuration - AuthzConfig string `json:"authz_config"` - // OIDC configuration options - OIDC oidcOptions `json:"oidc"` - // Permission profile to apply - PermissionProfile *permissions.Profile `json:"permission_profile"` - // Proxy mode to use - ProxyMode string `json:"proxy_mode"` - // Whether network isolation is turned on. This applies the rules in the permission profile. - NetworkIsolation bool `json:"network_isolation"` - // Tools filter - ToolsFilter []string `json:"tools"` - // Group name this workload belongs to - Group string `json:"group,omitempty"` -} - -// createRequest represents the request to create a new workload -// -// @Description Request to create a new workload -type createRequest struct { - updateRequest - // Name of the workload - Name string `json:"name"` -} - -// oidcOptions represents OIDC configuration options -// -// @Description OIDC configuration for workload authentication -type oidcOptions struct { - // OIDC issuer URL - Issuer string `json:"issuer"` - // Expected audience - Audience string `json:"audience"` - // JWKS URL for key verification - JwksURL string `json:"jwks_url"` - // Token introspection URL for OIDC - IntrospectionURL string `json:"introspection_url"` - // OAuth2 client ID - ClientID string `json:"client_id"` - // OAuth2 client secret - ClientSecret string `json:"client_secret"` -} - -// createWorkloadResponse represents the response for workload creation -// -// @Description Response after successfully creating a workload -type createWorkloadResponse struct { - // Name of the created workload - Name string `json:"name"` - // Port the workload is listening on - Port int `json:"port"` -} - -// bulkOperationRequest represents a request for bulk operations on workloads -type bulkOperationRequest struct { - // Names of the workloads to operate on - Names []string `json:"names"` - // Group name to operate on (mutually exclusive with names) - Group string `json:"group,omitempty"` -} - -// validateBulkOperationRequest validates the bulk operation request -func validateBulkOperationRequest(req bulkOperationRequest) error { - if len(req.Names) > 0 && req.Group != "" { - return fmt.Errorf("cannot specify both names and group") - } - if len(req.Names) == 0 && req.Group == "" { - return fmt.Errorf("must specify either names or group") - } - return nil -} - -// getWorkloadNamesFromRequest gets workload names from either the names field or group -func (s *WorkloadRoutes) getWorkloadNamesFromRequest(ctx context.Context, req bulkOperationRequest) ([]string, error) { - if len(req.Names) > 0 { - return req.Names, nil - } - - if err := validation.ValidateGroupName(req.Group); err != nil { - return nil, fmt.Errorf("invalid group name: %w", err) - } - - // Check if the group exists - exists, err := s.groupManager.Exists(ctx, req.Group) - if err != nil { - return nil, fmt.Errorf("failed to check if group exists: %v", err) - } - if !exists { - return nil, fmt.Errorf("group '%s' does not exist", req.Group) - } - - // Get all workload names in the group - workloadNames, err := s.workloadManager.ListWorkloadsInGroup(ctx, req.Group) - if err != nil { - return nil, fmt.Errorf("failed to list workloads in group: %v", err) - } - - return workloadNames, nil -} - -// createWorkloadFromRequest creates a workload from a request -func (s *WorkloadRoutes) createWorkloadFromRequest(ctx context.Context, req *createRequest) (*runner.RunConfig, error) { - // Default group if not specified - groupName := req.Group - if groupName == "" { - groupName = groups.DefaultGroup - } - - // Validate that the group exists - exists, err := s.groupManager.Exists(ctx, groupName) - if err != nil { - return nil, fmt.Errorf("failed to check if group exists: %v", err) - } - if !exists { - return nil, fmt.Errorf("group '%s' does not exist", groupName) - } - - // Fetch or build the requested image - imageURL, serverMetadata, err := retriever.GetMCPServer( - ctx, - req.Image, - "", // We do not let the user specify a CA cert path here. - retriever.VerifyImageWarn, - ) - if err != nil { - if errors.Is(err, retriever.ErrImageNotFound) { - return nil, fmt.Errorf("MCP server image not found") - } - return nil, fmt.Errorf("failed to retrieve MCP server image: %v", err) - } - - // Build RunConfig - runSecrets := secrets.SecretParametersToCLI(req.Secrets) - - // Handle server metadata - API only supports container servers - imageMetadata, _ := serverMetadata.(*registry.ImageMetadata) - - runConfig, err := runner.NewRunConfigBuilder(). - WithRuntime(s.containerRuntime). - WithCmdArgs(req.CmdArguments). - WithName(req.Name). - WithGroup(groupName). - WithImage(imageURL). - WithHost(req.Host). - WithTargetHost(transport.LocalhostIPv4). - WithDebug(s.debugMode). - WithVolumes(req.Volumes). - WithSecrets(runSecrets). - WithAuthzConfigPath(req.AuthzConfig). - WithAuditConfigPath(""). - WithPermissionProfile(req.PermissionProfile). - WithNetworkIsolation(req.NetworkIsolation). - WithK8sPodPatch(""). - WithProxyMode(types.ProxyMode(req.ProxyMode)). - WithTransportAndPorts(req.Transport, 0, req.TargetPort). - WithAuditEnabled(false, ""). - WithOIDCConfig(req.OIDC.Issuer, req.OIDC.Audience, req.OIDC.JwksURL, req.OIDC.ClientID, - "", "", "", "", "", false). - WithTelemetryConfig("", false, "", 0.0, nil, false, nil). - WithToolsFilter(req.ToolsFilter). - Build(ctx, imageMetadata, req.EnvVars, &runner.DetachedEnvVarValidator{}) - if err != nil { - logger.Errorf("Failed to build run config: %v", err) - return nil, fmt.Errorf("%w: %v", retriever.ErrInvalidRunConfig, err) - } - - // Save the workload state - if err := runConfig.SaveState(ctx); err != nil { - logger.Errorf("Failed to save workload config: %v", err) - return nil, fmt.Errorf("failed to save workload config") - } - - // Start workload - if err := s.workloadManager.RunWorkloadDetached(ctx, runConfig); err != nil { - logger.Errorf("Failed to start workload: %v", err) - return nil, fmt.Errorf("failed to start workload") - } - - return runConfig, nil -} - -// runConfigToCreateRequest converts a RunConfig to createRequest for API responses -func runConfigToCreateRequest(runConfig *runner.RunConfig) *createRequest { - // Convert CLI secrets ([]string) back to SecretParameters - secretParams := make([]secrets.SecretParameter, 0, len(runConfig.Secrets)) - for _, secretStr := range runConfig.Secrets { - // Parse the CLI format: ",target=" - if secretParam, err := secrets.ParseSecretParameter(secretStr); err == nil { - secretParams = append(secretParams, secretParam) - } - // Ignore invalid secrets rather than failing the entire conversion - } - - // Get OIDC fields from RunConfig - var oidcConfig oidcOptions - if runConfig.OIDCConfig != nil { - oidcConfig = oidcOptions{ - Issuer: runConfig.OIDCConfig.Issuer, - Audience: runConfig.OIDCConfig.Audience, - JwksURL: runConfig.OIDCConfig.JWKSURL, - IntrospectionURL: runConfig.OIDCConfig.IntrospectionURL, - ClientID: runConfig.OIDCConfig.ClientID, - ClientSecret: runConfig.OIDCConfig.ClientSecret, - } - } - - authzConfigPath := "" - - return &createRequest{ - updateRequest: updateRequest{ - Image: runConfig.Image, - Host: runConfig.Host, - CmdArguments: runConfig.CmdArgs, - TargetPort: runConfig.TargetPort, - EnvVars: runConfig.EnvVars, - Secrets: secretParams, - Volumes: runConfig.Volumes, - Transport: string(runConfig.Transport), - AuthzConfig: authzConfigPath, - OIDC: oidcConfig, - PermissionProfile: runConfig.PermissionProfile, - ProxyMode: string(runConfig.ProxyMode), - NetworkIsolation: runConfig.IsolateNetwork, - ToolsFilter: runConfig.ToolsFilter, - Group: runConfig.Group, - }, - Name: runConfig.Name, - } -} diff --git a/pkg/api/v1/workloads_types_test.go b/pkg/api/v1/workloads_types_test.go new file mode 100644 index 000000000..6bfbe4d74 --- /dev/null +++ b/pkg/api/v1/workloads_types_test.go @@ -0,0 +1,216 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/auth" + "github.com/stacklok/toolhive/pkg/permissions" + "github.com/stacklok/toolhive/pkg/runner" + "github.com/stacklok/toolhive/pkg/transport/types" +) + +func TestValidateBulkOperationRequest(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + request bulkOperationRequest + wantErr bool + errMsg string + }{ + { + name: "valid with names only", + request: bulkOperationRequest{ + Names: []string{"workload1", "workload2"}, + }, + wantErr: false, + }, + { + name: "valid with group only", + request: bulkOperationRequest{ + Group: "test-group", + }, + wantErr: false, + }, + { + name: "invalid - both names and group", + request: bulkOperationRequest{ + Names: []string{"workload1"}, + Group: "test-group", + }, + wantErr: true, + errMsg: "cannot specify both names and group", + }, + { + name: "invalid - neither names nor group", + request: bulkOperationRequest{}, + wantErr: true, + errMsg: "must specify either names or group", + }, + { + name: "invalid - empty names array", + request: bulkOperationRequest{ + Names: []string{}, + }, + wantErr: true, + errMsg: "must specify either names or group", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := validateBulkOperationRequest(tt.request) + if tt.wantErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestRunConfigToCreateRequest(t *testing.T) { + t.Parallel() + + t.Run("basic conversion", func(t *testing.T) { + t.Parallel() + + runConfig := &runner.RunConfig{ + Name: "test-workload", + Image: "test-image:latest", + Host: "localhost", + CmdArgs: []string{"arg1", "arg2"}, + TargetPort: 8080, + EnvVars: map[string]string{"ENV1": "value1"}, + Secrets: []string{"secret1,target=/path1", "secret2,target=/path2"}, + Volumes: []string{"/host:/container"}, + Transport: types.TransportTypeSSE, + Group: "test-group", + ProxyMode: types.ProxyModeSSE, + IsolateNetwork: true, + ToolsFilter: []string{"tool1", "tool2"}, + } + + result := runConfigToCreateRequest(runConfig) + + require.NotNil(t, result) + assert.Equal(t, "test-workload", result.Name) + assert.Equal(t, "test-image:latest", result.Image) + assert.Equal(t, "localhost", result.Host) + assert.Equal(t, []string{"arg1", "arg2"}, result.CmdArguments) + assert.Equal(t, 8080, result.TargetPort) + assert.Equal(t, map[string]string{"ENV1": "value1"}, result.EnvVars) + require.Len(t, result.Secrets, 2) + assert.Equal(t, "secret1", result.Secrets[0].Name) + assert.Equal(t, "/path1", result.Secrets[0].Target) + assert.Equal(t, "secret2", result.Secrets[1].Name) + assert.Equal(t, "/path2", result.Secrets[1].Target) + assert.Equal(t, []string{"/host:/container"}, result.Volumes) + assert.Equal(t, "sse", result.Transport) + assert.Equal(t, "test-group", result.Group) + assert.Equal(t, "sse", result.ProxyMode) + assert.True(t, result.NetworkIsolation) + assert.Equal(t, []string{"tool1", "tool2"}, result.ToolsFilter) + }) + + t.Run("with OIDC config", func(t *testing.T) { + t.Parallel() + + runConfig := &runner.RunConfig{ + Name: "test-workload", + OIDCConfig: &auth.TokenValidatorConfig{ + Issuer: "https://oidc.example.com", + Audience: "test-audience", + JWKSURL: "https://oidc.example.com/jwks", + IntrospectionURL: "https://oidc.example.com/introspect", + ClientID: "test-client", + ClientSecret: "test-secret", + }, + } + + result := runConfigToCreateRequest(runConfig) + + require.NotNil(t, result) + assert.Equal(t, "https://oidc.example.com", result.OIDC.Issuer) + assert.Equal(t, "test-audience", result.OIDC.Audience) + assert.Equal(t, "https://oidc.example.com/jwks", result.OIDC.JwksURL) + assert.Equal(t, "https://oidc.example.com/introspect", result.OIDC.IntrospectionURL) + assert.Equal(t, "test-client", result.OIDC.ClientID) + assert.Equal(t, "test-secret", result.OIDC.ClientSecret) + }) + + t.Run("with remote OAuth config", func(t *testing.T) { + t.Parallel() + + runConfig := &runner.RunConfig{ + Name: "test-workload", + RemoteAuthConfig: &runner.RemoteAuthConfig{ + Issuer: "https://oauth.example.com", + AuthorizeURL: "https://oauth.example.com/auth", + TokenURL: "https://oauth.example.com/token", + ClientID: "test-client", + Scopes: []string{"read", "write"}, + OAuthParams: map[string]string{"custom": "param"}, + CallbackPort: 8081, + }, + } + + result := runConfigToCreateRequest(runConfig) + + require.NotNil(t, result) + require.NotNil(t, result.OAuthConfig) + assert.Equal(t, "https://oauth.example.com", result.OAuthConfig.Issuer) + assert.Equal(t, "https://oauth.example.com/auth", result.OAuthConfig.AuthorizeURL) + assert.Equal(t, "https://oauth.example.com/token", result.OAuthConfig.TokenURL) + assert.Equal(t, "test-client", result.OAuthConfig.ClientID) + assert.Equal(t, []string{"read", "write"}, result.OAuthConfig.Scopes) + assert.Equal(t, map[string]string{"custom": "param"}, result.OAuthConfig.OAuthParams) + assert.Equal(t, 8081, result.OAuthConfig.CallbackPort) + }) + + t.Run("with permission profile", func(t *testing.T) { + t.Parallel() + + profile := &permissions.Profile{ + Name: "test-profile", + } + + runConfig := &runner.RunConfig{ + Name: "test-workload", + PermissionProfile: profile, + } + + result := runConfigToCreateRequest(runConfig) + + require.NotNil(t, result) + assert.Equal(t, profile, result.PermissionProfile) + }) + + t.Run("with invalid secrets", func(t *testing.T) { + t.Parallel() + + runConfig := &runner.RunConfig{ + Name: "test-workload", + Secrets: []string{"invalid-secret-format", "another-invalid"}, + } + + result := runConfigToCreateRequest(runConfig) + + require.NotNil(t, result) + // Invalid secrets should be ignored, resulting in empty secrets array + assert.Empty(t, result.Secrets) + }) + + t.Run("nil runConfig", func(t *testing.T) { + t.Parallel() + + result := runConfigToCreateRequest(nil) + assert.Nil(t, result) + }) +} diff --git a/pkg/runner/config.go b/pkg/runner/config.go index 65141fc5d..eaa388ff3 100644 --- a/pkg/runner/config.go +++ b/pkg/runner/config.go @@ -383,6 +383,7 @@ func (c *RunConfig) WithContainerName() (*RunConfig, bool) { } else if c.RemoteURL != "" && c.Name != "" { // For remote servers, sanitize the provided name to ensure it's safe for file paths c.BaseName, wasModified = workloadtypes.SanitizeWorkloadName(c.Name) + c.ContainerName = c.Name } } return c, wasModified