From 2a22440b0e95fabbe59e2bbd9cddaadf38fd5a4f Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 29 Oct 2025 13:38:30 -0600 Subject: [PATCH 01/12] chore!: patch release v2.28 to remove `aibridge` experiment (#20544) Includes stack of PRs from https://github.com/coder/coder/pull/20520 --------- Signed-off-by: Danny Kopping --- .github/workflows/dogfood.yaml | 2 +- Makefile | 16 +- cli/testdata/coder_server_--help.golden | 35 + cli/testdata/server-config.yaml.golden | 3 +- coderd/apidoc/docs.go | 9 +- coderd/apidoc/swagger.json | 9 +- codersdk/aibridge.go | 4 +- codersdk/deployment.go | 16 +- docs/ai-coder/ai-bridge.md | 18 +- docs/manifest.json | 15 + docs/reference/api/aibridge.md | 4 +- docs/reference/api/schemas.md | 1 - docs/reference/cli/aibridge.md | 16 + docs/reference/cli/aibridge_interceptions.md | 16 + .../cli/aibridge_interceptions_list.md | 69 ++ docs/reference/cli/index.md | 1 + docs/reference/cli/server.md | 105 +++ dogfood/coder/main.tf | 2 +- enterprise/{x => }/aibridged/aibridged.go | 2 +- .../aibridged/aibridged_integration_test.go | 2 +- .../{x => }/aibridged/aibridged_test.go | 6 +- .../aibridged/aibridgedmock/clientmock.go | 6 +- .../{x => }/aibridged/aibridgedmock/doc.go | 4 +- .../aibridged/aibridgedmock/poolmock.go | 6 +- enterprise/{x => }/aibridged/client.go | 2 +- enterprise/{x => }/aibridged/http.go | 2 +- enterprise/{x => }/aibridged/mcp.go | 2 +- .../{x => }/aibridged/mcp_internal_test.go | 2 +- enterprise/{x => }/aibridged/pool.go | 0 enterprise/{x => }/aibridged/pool_test.go | 4 +- .../{x => }/aibridged/proto/aibridged.pb.go | 627 +++++++++--------- .../{x => }/aibridged/proto/aibridged.proto | 0 .../aibridged/proto/aibridged_drpc.pb.go | 62 +- enterprise/{x => }/aibridged/request.go | 0 enterprise/{x => }/aibridged/server.go | 2 +- enterprise/{x => }/aibridged/translator.go | 2 +- enterprise/{x => }/aibridged/utils_test.go | 0 .../aibridgedserver/aibridgedserver.go | 4 +- .../aibridgedserver_internal_test.go | 0 .../aibridgedserver/aibridgedserver_test.go | 6 +- .../cli/{exp_aibridge.go => aibridge.go} | 3 +- ...{exp_aibridge_test.go => aibridge_test.go} | 6 - enterprise/cli/aibridged.go | 2 +- enterprise/cli/root.go | 7 +- enterprise/cli/server.go | 34 +- enterprise/cli/testdata/coder_--help.golden | 1 + .../cli/testdata/coder_aibridge_--help.golden | 12 + ...coder_aibridge_interceptions_--help.golden | 12 + ..._aibridge_interceptions_list_--help.golden | 37 ++ .../cli/testdata/coder_server_--help.golden | 35 + enterprise/coderd/aibridge.go | 2 +- enterprise/coderd/aibridge_test.go | 37 +- enterprise/coderd/aibridged.go | 6 +- enterprise/coderd/coderd.go | 9 +- site/src/api/typesGenerated.ts | 2 - 55 files changed, 787 insertions(+), 500 deletions(-) create mode 100644 docs/reference/cli/aibridge.md create mode 100644 docs/reference/cli/aibridge_interceptions.md create mode 100644 docs/reference/cli/aibridge_interceptions_list.md rename enterprise/{x => }/aibridged/aibridged.go (97%) rename enterprise/{x => }/aibridged/aibridged_integration_test.go (99%) rename enterprise/{x => }/aibridged/aibridged_test.go (98%) rename enterprise/{x => }/aibridged/aibridgedmock/clientmock.go (97%) rename enterprise/{x => }/aibridged/aibridgedmock/doc.go (52%) rename enterprise/{x => }/aibridged/aibridgedmock/poolmock.go (91%) rename enterprise/{x => }/aibridged/client.go (90%) rename enterprise/{x => }/aibridged/http.go (98%) rename enterprise/{x => }/aibridged/mcp.go (99%) rename enterprise/{x => }/aibridged/mcp_internal_test.go (95%) rename enterprise/{x => }/aibridged/pool.go (100%) rename enterprise/{x => }/aibridged/pool_test.go (96%) rename enterprise/{x => }/aibridged/proto/aibridged.pb.go (60%) rename enterprise/{x => }/aibridged/proto/aibridged.proto (100%) rename enterprise/{x => }/aibridged/proto/aibridged_drpc.pb.go (84%) rename enterprise/{x => }/aibridged/request.go (100%) rename enterprise/{x => }/aibridged/server.go (68%) rename enterprise/{x => }/aibridged/translator.go (98%) rename enterprise/{x => }/aibridged/utils_test.go (100%) rename enterprise/{x => }/aibridgedserver/aibridgedserver.go (99%) rename enterprise/{x => }/aibridgedserver/aibridgedserver_internal_test.go (100%) rename enterprise/{x => }/aibridgedserver/aibridgedserver_test.go (99%) rename enterprise/cli/{exp_aibridge.go => aibridge.go} (97%) rename enterprise/cli/{exp_aibridge_test.go => aibridge_test.go} (96%) create mode 100644 enterprise/cli/testdata/coder_aibridge_--help.golden create mode 100644 enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden create mode 100644 enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml index 2f47132ae43f0..e1d4a7a22787a 100644 --- a/.github/workflows/dogfood.yaml +++ b/.github/workflows/dogfood.yaml @@ -40,7 +40,7 @@ jobs: with: # Pinning to 2.28 here, as Nix gets a "error: [json.exception.type_error.302] type must be array, but is string" # on version 2.29 and above. - nix_version: "2.28.4" + nix_version: "2.28.5" - uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3 with: diff --git a/Makefile b/Makefile index 7f21f1fa6da04..7ecb64975e548 100644 --- a/Makefile +++ b/Makefile @@ -636,8 +636,8 @@ TAILNETTEST_MOCKS := \ tailnet/tailnettest/subscriptionmock.go AIBRIDGED_MOCKS := \ - enterprise/x/aibridged/aibridgedmock/clientmock.go \ - enterprise/x/aibridged/aibridgedmock/poolmock.go + enterprise/aibridged/aibridgedmock/clientmock.go \ + enterprise/aibridged/aibridgedmock/poolmock.go GEN_FILES := \ tailnet/proto/tailnet.pb.go \ @@ -645,7 +645,7 @@ GEN_FILES := \ provisionersdk/proto/provisioner.pb.go \ provisionerd/proto/provisionerd.pb.go \ vpn/vpn.pb.go \ - enterprise/x/aibridged/proto/aibridged.pb.go \ + enterprise/aibridged/proto/aibridged.pb.go \ $(DB_GEN_FILES) \ $(SITE_GEN_FILES) \ coderd/rbac/object_gen.go \ @@ -697,7 +697,7 @@ gen/mark-fresh: provisionersdk/proto/provisioner.pb.go \ provisionerd/proto/provisionerd.pb.go \ vpn/vpn.pb.go \ - enterprise/x/aibridged/proto/aibridged.pb.go \ + enterprise/aibridged/proto/aibridged.pb.go \ coderd/database/dump.sql \ $(DB_GEN_FILES) \ site/src/api/typesGenerated.ts \ @@ -768,8 +768,8 @@ codersdk/workspacesdk/agentconnmock/agentconnmock.go: codersdk/workspacesdk/agen go generate ./codersdk/workspacesdk/agentconnmock/ touch "$@" -$(AIBRIDGED_MOCKS): enterprise/x/aibridged/client.go enterprise/x/aibridged/pool.go - go generate ./enterprise/x/aibridged/aibridgedmock/ +$(AIBRIDGED_MOCKS): enterprise/aibridged/client.go enterprise/aibridged/pool.go + go generate ./enterprise/aibridged/aibridgedmock/ touch "$@" agent/agentcontainers/dcspec/dcspec_gen.go: \ @@ -822,13 +822,13 @@ vpn/vpn.pb.go: vpn/vpn.proto --go_opt=paths=source_relative \ ./vpn/vpn.proto -enterprise/x/aibridged/proto/aibridged.pb.go: enterprise/x/aibridged/proto/aibridged.proto +enterprise/aibridged/proto/aibridged.pb.go: enterprise/aibridged/proto/aibridged.proto protoc \ --go_out=. \ --go_opt=paths=source_relative \ --go-drpc_out=. \ --go-drpc_opt=paths=source_relative \ - ./enterprise/x/aibridged/proto/aibridged.proto + ./enterprise/aibridged/proto/aibridged.proto site/src/api/typesGenerated.ts: site/node_modules/.installed $(wildcard scripts/apitypings/*) $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go') # -C sets the directory for the go run command diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 447ce1ae4fce2..7e7a7ece0d958 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -80,6 +80,41 @@ OPTIONS: Periodically check for new releases of Coder and inform the owner. The check is performed once per day. +AIBRIDGE OPTIONS: + --aibridge-anthropic-base-url string, $CODER_AIBRIDGE_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/) + The base URL of the Anthropic API. + + --aibridge-anthropic-key string, $CODER_AIBRIDGE_ANTHROPIC_KEY + The key to authenticate against the Anthropic API. + + --aibridge-bedrock-access-key string, $CODER_AIBRIDGE_BEDROCK_ACCESS_KEY + The access key to authenticate against the AWS Bedrock API. + + --aibridge-bedrock-access-key-secret string, $CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET + The access key secret to use with the access key to authenticate + against the AWS Bedrock API. + + --aibridge-bedrock-model string, $CODER_AIBRIDGE_BEDROCK_MODEL (default: global.anthropic.claude-sonnet-4-5-20250929-v1:0) + The model to use when making requests to the AWS Bedrock API. + + --aibridge-bedrock-region string, $CODER_AIBRIDGE_BEDROCK_REGION + The AWS Bedrock API region. + + --aibridge-bedrock-small-fastmodel string, $CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL (default: global.anthropic.claude-haiku-4-5-20251001-v1:0) + The small fast model to use when making requests to the AWS Bedrock + API. Claude Code uses Haiku-class models to perform background tasks. + See + https://docs.claude.com/en/docs/claude-code/settings#environment-variables. + + --aibridge-enabled bool, $CODER_AIBRIDGE_ENABLED (default: false) + Whether to start an in-memory aibridged instance. + + --aibridge-openai-base-url string, $CODER_AIBRIDGE_OPENAI_BASE_URL (default: https://api.openai.com/v1/) + The base URL of the OpenAI API. + + --aibridge-openai-key string, $CODER_AIBRIDGE_OPENAI_KEY + The key to authenticate against the OpenAI API. + CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index cbabf0474f291..225c240d9e761 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -714,8 +714,7 @@ workspace_prebuilds: # (default: 3, type: int) failure_hard_limit: 3 aibridge: - # Whether to start an in-memory aibridged instance ("aibridge" experiment must be - # enabled, too). + # Whether to start an in-memory aibridged instance. # (default: false, type: bool) enabled: false # The base URL of the OpenAI API. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 45b220eb9d255..344a0299f8405 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -85,7 +85,7 @@ const docTemplate = `{ } } }, - "/api/experimental/aibridge/interceptions": { + "/aibridge/interceptions": { "get": { "security": [ { @@ -14316,11 +14316,9 @@ const docTemplate = `{ "web-push", "oauth2", "mcp-server-http", - "workspace-sharing", - "aibridge" + "workspace-sharing" ], "x-enum-comments": { - "ExperimentAIBridge": "Enables AI Bridge functionality.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentExample": "This isn't used for anything.", "ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.", @@ -14338,8 +14336,7 @@ const docTemplate = `{ "ExperimentWebPush", "ExperimentOAuth2", "ExperimentMCPServerHTTP", - "ExperimentWorkspaceSharing", - "ExperimentAIBridge" + "ExperimentWorkspaceSharing" ] }, "codersdk.ExternalAPIKeyScopes": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8e6a0030cb83b..b472f2f4ef53f 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -65,7 +65,7 @@ } } }, - "/api/experimental/aibridge/interceptions": { + "/aibridge/interceptions": { "get": { "security": [ { @@ -12923,11 +12923,9 @@ "web-push", "oauth2", "mcp-server-http", - "workspace-sharing", - "aibridge" + "workspace-sharing" ], "x-enum-comments": { - "ExperimentAIBridge": "Enables AI Bridge functionality.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentExample": "This isn't used for anything.", "ExperimentMCPServerHTTP": "Enables the MCP HTTP server functionality.", @@ -12945,8 +12943,7 @@ "ExperimentWebPush", "ExperimentOAuth2", "ExperimentMCPServerHTTP", - "ExperimentWorkspaceSharing", - "ExperimentAIBridge" + "ExperimentWorkspaceSharing" ] }, "codersdk.ExternalAPIKeyScopes": { diff --git a/codersdk/aibridge.go b/codersdk/aibridge.go index b627f5e9d5ef7..a322187bb23c3 100644 --- a/codersdk/aibridge.go +++ b/codersdk/aibridge.go @@ -113,8 +113,8 @@ func (f AIBridgeListInterceptionsFilter) asRequestOption() RequestOption { // AIBridgeListInterceptions returns AIBridge interceptions with the given // filter. -func (c *ExperimentalClient) AIBridgeListInterceptions(ctx context.Context, filter AIBridgeListInterceptionsFilter) (AIBridgeListInterceptionsResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/experimental/aibridge/interceptions", nil, filter.asRequestOption(), filter.Pagination.asRequestOption(), filter.Pagination.asRequestOption()) +func (c *Client) AIBridgeListInterceptions(ctx context.Context, filter AIBridgeListInterceptionsFilter) (AIBridgeListInterceptionsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/aibridge/interceptions", nil, filter.asRequestOption(), filter.Pagination.asRequestOption(), filter.Pagination.asRequestOption()) if err != nil { return AIBridgeListInterceptionsResponse{}, err } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 97bbb13bedbc7..9425a3740f089 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3241,14 +3241,13 @@ Write out the current server config as YAML to stdout.`, // AIBridge Options { Name: "AIBridge Enabled", - Description: fmt.Sprintf("Whether to start an in-memory aibridged instance (%q experiment must be enabled, too).", ExperimentAIBridge), + Description: "Whether to start an in-memory aibridged instance.", Flag: "aibridge-enabled", Env: "CODER_AIBRIDGE_ENABLED", Value: &c.AI.BridgeConfig.Enabled, Default: "false", Group: &deploymentGroupAIBridge, YAML: "enabled", - Hidden: true, }, { Name: "AIBridge OpenAI Base URL", @@ -3259,7 +3258,6 @@ Write out the current server config as YAML to stdout.`, Default: "https://api.openai.com/v1/", Group: &deploymentGroupAIBridge, YAML: "openai_base_url", - Hidden: true, }, { Name: "AIBridge OpenAI Key", @@ -3270,7 +3268,6 @@ Write out the current server config as YAML to stdout.`, Default: "", Group: &deploymentGroupAIBridge, YAML: "openai_key", - Hidden: true, }, { Name: "AIBridge Anthropic Base URL", @@ -3281,7 +3278,6 @@ Write out the current server config as YAML to stdout.`, Default: "https://api.anthropic.com/", Group: &deploymentGroupAIBridge, YAML: "anthropic_base_url", - Hidden: true, }, { Name: "AIBridge Anthropic Key", @@ -3292,7 +3288,6 @@ Write out the current server config as YAML to stdout.`, Default: "", Group: &deploymentGroupAIBridge, YAML: "anthropic_key", - Hidden: true, }, { Name: "AIBridge Bedrock Region", @@ -3303,7 +3298,6 @@ Write out the current server config as YAML to stdout.`, Default: "", Group: &deploymentGroupAIBridge, YAML: "bedrock_region", - Hidden: true, }, { Name: "AIBridge Bedrock Access Key", @@ -3314,7 +3308,6 @@ Write out the current server config as YAML to stdout.`, Default: "", Group: &deploymentGroupAIBridge, YAML: "bedrock_access_key", - Hidden: true, }, { Name: "AIBridge Bedrock Access Key Secret", @@ -3325,7 +3318,6 @@ Write out the current server config as YAML to stdout.`, Default: "", Group: &deploymentGroupAIBridge, YAML: "bedrock_access_key_secret", - Hidden: true, }, { Name: "AIBridge Bedrock Model", @@ -3336,7 +3328,6 @@ Write out the current server config as YAML to stdout.`, Default: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", // See https://docs.claude.com/en/api/claude-on-amazon-bedrock#accessing-bedrock. Group: &deploymentGroupAIBridge, YAML: "bedrock_model", - Hidden: true, }, { Name: "AIBridge Bedrock Small Fast Model", @@ -3347,7 +3338,6 @@ Write out the current server config as YAML to stdout.`, Default: "global.anthropic.claude-haiku-4-5-20251001-v1:0", // See https://docs.claude.com/en/api/claude-on-amazon-bedrock#accessing-bedrock. Group: &deploymentGroupAIBridge, YAML: "bedrock_small_fast_model", - Hidden: true, }, { Name: "Enable Authorization Recordings", @@ -3645,7 +3635,6 @@ const ( ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality. ExperimentMCPServerHTTP Experiment = "mcp-server-http" // Enables the MCP HTTP server functionality. ExperimentWorkspaceSharing Experiment = "workspace-sharing" // Enables updating workspace ACLs for sharing with users and groups. - ExperimentAIBridge Experiment = "aibridge" // Enables AI Bridge functionality. ) func (e Experiment) DisplayName() string { @@ -3666,8 +3655,6 @@ func (e Experiment) DisplayName() string { return "MCP HTTP Server Functionality" case ExperimentWorkspaceSharing: return "Workspace Sharing" - case ExperimentAIBridge: - return "AI Bridge" default: // Split on hyphen and convert to title case // e.g. "web-push" -> "Web Push", "mcp-server-http" -> "Mcp Server Http" @@ -3686,7 +3673,6 @@ var ExperimentsKnown = Experiments{ ExperimentOAuth2, ExperimentMCPServerHTTP, ExperimentWorkspaceSharing, - ExperimentAIBridge, } // ExperimentsSafe should include all experiments that are safe for diff --git a/docs/ai-coder/ai-bridge.md b/docs/ai-coder/ai-bridge.md index c7cfbe7d85ea2..a993cee71319c 100644 --- a/docs/ai-coder/ai-bridge.md +++ b/docs/ai-coder/ai-bridge.md @@ -1,8 +1,5 @@ # AI Bridge -> [!NOTE] -> AI Bridge is currently an _experimental_ feature. - ![AI bridge diagram](../images/aibridge/aibridge_diagram.png) Bridge is a smart proxy for AI. It acts as a man-in-the-middle between your users' coding agents / IDEs @@ -45,17 +42,14 @@ Bridge runs inside the Coder control plane, requiring no separate compute to dep ### Activation -To enable this feature, activate the `aibridge` experiment using an environment variable or a CLI flag. -Additionally, you will need to enable Bridge explicitly: +You will need to enable AI Bridge explicitly: ```sh -CODER_EXPERIMENTS="aibridge" CODER_AIBRIDGE_ENABLED=true coder server +CODER_AIBRIDGE_ENABLED=true coder server # or -coder server --experiments=aibridge --aibridge-enabled=true +coder server --aibridge-enabled=true ``` -_If you have other experiments enabled, separate them by commas._ - ### Providers Bridge currently supports OpenAI and Anthropic APIs. @@ -89,8 +83,8 @@ Once AI Bridge is enabled on the server, your users need to configure their AI c The exact configuration method varies by client — some use environment variables, others use configuration files or UI settings: -- **OpenAI-compatible clients**: Set the base URL (commonly via the `OPENAI_BASE_URL` environment variable) to `https://coder.example.com/api/experimental/aibridge/openai/v1` -- **Anthropic-compatible clients**: Set the base URL (commonly via the `ANTHROPIC_BASE_URL` environment variable) to `https://coder.example.com/api/experimental/aibridge/anthropic` +- **OpenAI-compatible clients**: Set the base URL (commonly via the `OPENAI_BASE_URL` environment variable) to `https://coder.example.com/api/v2/aibridge/openai/v1` +- **Anthropic-compatible clients**: Set the base URL (commonly via the `ANTHROPIC_BASE_URL` environment variable) to `https://coder.example.com/api/v2/aibridge/anthropic` Replace `coder.example.com` with your actual Coder deployment URL. @@ -133,7 +127,7 @@ All of these records are associated to an "interception" record, which maps 1:1 These logs can be used to determine usage patterns, track costs, and evaluate tooling adoption. -This data is currently accessible through the API and CLI (experimental), which we advise administrators export to their observability platform of choice. We've configured a Grafana dashboard to display Claude Code usage internally which can be imported as a starting point for your tooling adoption metrics. +This data is currently accessible through the API and CLI, which we advise administrators export to their observability platform of choice. We've configured a Grafana dashboard to display Claude Code usage internally which can be imported as a starting point for your tooling adoption metrics. ![User Leaderboard](../images/aibridge/grafana_user_leaderboard.png) diff --git a/docs/manifest.json b/docs/manifest.json index 78a0d38ec949d..57711406c87d7 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1180,6 +1180,21 @@ "path": "./reference/cli/index.md", "icon_path": "./images/icons/terminal.svg", "children": [ + { + "title": "aibridge", + "description": "Manage AIBridge.", + "path": "reference/cli/aibridge.md" + }, + { + "title": "aibridge interceptions", + "description": "Manage AIBridge interceptions.", + "path": "reference/cli/aibridge_interceptions.md" + }, + { + "title": "aibridge interceptions list", + "description": "List AIBridge interceptions as JSON.", + "path": "reference/cli/aibridge_interceptions_list.md" + }, { "title": "autoupdate", "description": "Toggle auto-update policy for a workspace", diff --git a/docs/reference/api/aibridge.md b/docs/reference/api/aibridge.md index d2be736eb32b2..7e3a23fc5ec21 100644 --- a/docs/reference/api/aibridge.md +++ b/docs/reference/api/aibridge.md @@ -6,12 +6,12 @@ ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/api/experimental/aibridge/interceptions \ +curl -X GET http://coder-server:8080/api/v2/aibridge/interceptions \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`GET /api/experimental/aibridge/interceptions` +`GET /aibridge/interceptions` ### Parameters diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 796b4811cc4c1..037c9cfa109bb 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4059,7 +4059,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `oauth2` | | `mcp-server-http` | | `workspace-sharing` | -| `aibridge` | ## codersdk.ExternalAPIKeyScopes diff --git a/docs/reference/cli/aibridge.md b/docs/reference/cli/aibridge.md new file mode 100644 index 0000000000000..7b16c5cdc87a8 --- /dev/null +++ b/docs/reference/cli/aibridge.md @@ -0,0 +1,16 @@ + +# aibridge + +Manage AIBridge. + +## Usage + +```console +coder aibridge +``` + +## Subcommands + +| Name | Purpose | +|-----------------------------------------------------------|--------------------------------| +| [interceptions](./aibridge_interceptions.md) | Manage AIBridge interceptions. | diff --git a/docs/reference/cli/aibridge_interceptions.md b/docs/reference/cli/aibridge_interceptions.md new file mode 100644 index 0000000000000..9cfb3d45a74ea --- /dev/null +++ b/docs/reference/cli/aibridge_interceptions.md @@ -0,0 +1,16 @@ + +# aibridge interceptions + +Manage AIBridge interceptions. + +## Usage + +```console +coder aibridge interceptions +``` + +## Subcommands + +| Name | Purpose | +|-------------------------------------------------------|--------------------------------------| +| [list](./aibridge_interceptions_list.md) | List AIBridge interceptions as JSON. | diff --git a/docs/reference/cli/aibridge_interceptions_list.md b/docs/reference/cli/aibridge_interceptions_list.md new file mode 100644 index 0000000000000..7e86cd4968e33 --- /dev/null +++ b/docs/reference/cli/aibridge_interceptions_list.md @@ -0,0 +1,69 @@ + +# aibridge interceptions list + +List AIBridge interceptions as JSON. + +## Usage + +```console +coder aibridge interceptions list [flags] +``` + +## Options + +### --initiator + +| | | +|------|---------------------| +| Type | string | + +Only return interceptions initiated by this user. Accepts a user ID, username, or "me". + +### --started-before + +| | | +|------|---------------------| +| Type | string | + +Only return interceptions started before this time. Must be after 'started-after' if set. Accepts a time in the RFC 3339 format, e.g. "2006-01-02T15:04:05Z07:00". + +### --started-after + +| | | +|------|---------------------| +| Type | string | + +Only return interceptions started after this time. Must be before 'started-before' if set. Accepts a time in the RFC 3339 format, e.g. "2006-01-02T15:04:05Z07:00". + +### --provider + +| | | +|------|---------------------| +| Type | string | + +Only return interceptions from this provider. + +### --model + +| | | +|------|---------------------| +| Type | string | + +Only return interceptions from this model. + +### --after-id + +| | | +|------|---------------------| +| Type | string | + +The ID of the last result on the previous page to use as a pagination cursor. + +### --limit + +| | | +|---------|------------------| +| Type | int | +| Default | 100 | + +The limit of results to return. Must be between 1 and 1000. diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index c298f8bcb61a2..c1410b4599977 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -68,6 +68,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr | [groups](./groups.md) | Manage groups | | [prebuilds](./prebuilds.md) | Manage Coder prebuilds | | [external-workspaces](./external-workspaces.md) | Create or manage external workspaces | +| [aibridge](./aibridge.md) | Manage AIBridge. | ## Options diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index bdc424bdd7a8b..e689f7fa28336 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1647,3 +1647,108 @@ How often to reconcile workspace prebuilds state. | Default | false | Hide AI tasks from the dashboard. + +### --aibridge-enabled + +| | | +|-------------|--------------------------------------| +| Type | bool | +| Environment | $CODER_AIBRIDGE_ENABLED | +| YAML | aibridge.enabled | +| Default | false | + +Whether to start an in-memory aibridged instance. + +### --aibridge-openai-base-url + +| | | +|-------------|----------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_OPENAI_BASE_URL | +| YAML | aibridge.openai_base_url | +| Default | https://api.openai.com/v1/ | + +The base URL of the OpenAI API. + +### --aibridge-openai-key + +| | | +|-------------|-----------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_OPENAI_KEY | +| YAML | aibridge.openai_key | + +The key to authenticate against the OpenAI API. + +### --aibridge-anthropic-base-url + +| | | +|-------------|-------------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_ANTHROPIC_BASE_URL | +| YAML | aibridge.anthropic_base_url | +| Default | https://api.anthropic.com/ | + +The base URL of the Anthropic API. + +### --aibridge-anthropic-key + +| | | +|-------------|--------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_ANTHROPIC_KEY | +| YAML | aibridge.anthropic_key | + +The key to authenticate against the Anthropic API. + +### --aibridge-bedrock-region + +| | | +|-------------|---------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_BEDROCK_REGION | +| YAML | aibridge.bedrock_region | + +The AWS Bedrock API region. + +### --aibridge-bedrock-access-key + +| | | +|-------------|-------------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_BEDROCK_ACCESS_KEY | +| YAML | aibridge.bedrock_access_key | + +The access key to authenticate against the AWS Bedrock API. + +### --aibridge-bedrock-access-key-secret + +| | | +|-------------|--------------------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET | +| YAML | aibridge.bedrock_access_key_secret | + +The access key secret to use with the access key to authenticate against the AWS Bedrock API. + +### --aibridge-bedrock-model + +| | | +|-------------|---------------------------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_BEDROCK_MODEL | +| YAML | aibridge.bedrock_model | +| Default | global.anthropic.claude-sonnet-4-5-20250929-v1:0 | + +The model to use when making requests to the AWS Bedrock API. + +### --aibridge-bedrock-small-fastmodel + +| | | +|-------------|--------------------------------------------------------------| +| Type | string | +| Environment | $CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL | +| YAML | aibridge.bedrock_small_fast_model | +| Default | global.anthropic.claude-haiku-4-5-20251001-v1:0 | + +The small fast model to use when making requests to the AWS Bedrock API. Claude Code uses Haiku-class models to perform background tasks. See https://docs.claude.com/en/docs/claude-code/settings#environment-variables. diff --git a/dogfood/coder/main.tf b/dogfood/coder/main.tf index 05ec4a6a2e975..37a53349bb903 100644 --- a/dogfood/coder/main.tf +++ b/dogfood/coder/main.tf @@ -479,7 +479,7 @@ resource "coder_agent" "dev" { dir = local.repo_dir env = { OIDC_TOKEN : data.coder_workspace_owner.me.oidc_access_token, - ANTHROPIC_BASE_URL : "https://dev.coder.com/api/experimental/aibridge/anthropic", + ANTHROPIC_BASE_URL : "https://dev.coder.com/api/v2/aibridge/anthropic", ANTHROPIC_AUTH_TOKEN : data.coder_workspace_owner.me.session_token } startup_script_behavior = "blocking" diff --git a/enterprise/x/aibridged/aibridged.go b/enterprise/aibridged/aibridged.go similarity index 97% rename from enterprise/x/aibridged/aibridged.go rename to enterprise/aibridged/aibridged.go index a1fa4022ff960..fcec1629b8701 100644 --- a/enterprise/x/aibridged/aibridged.go +++ b/enterprise/aibridged/aibridged.go @@ -19,7 +19,7 @@ var _ io.Closer = &Server{} // Server provides the AI Bridge functionality. // It is responsible for: -// - receiving requests on /api/experimental/aibridged/* // TODO: update endpoint once out of experimental +// - receiving requests on /api/v2/aibridged/* // - manipulating the requests // - relaying requests to upstream AI services and relaying responses to caller // diff --git a/enterprise/x/aibridged/aibridged_integration_test.go b/enterprise/aibridged/aibridged_integration_test.go similarity index 99% rename from enterprise/x/aibridged/aibridged_integration_test.go rename to enterprise/aibridged/aibridged_integration_test.go index 45d47bd1b3507..88fa21377f5a2 100644 --- a/enterprise/x/aibridged/aibridged_integration_test.go +++ b/enterprise/aibridged/aibridged_integration_test.go @@ -19,8 +19,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/aibridged" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" - "github.com/coder/coder/v2/enterprise/x/aibridged" "github.com/coder/coder/v2/testutil" ) diff --git a/enterprise/x/aibridged/aibridged_test.go b/enterprise/aibridged/aibridged_test.go similarity index 98% rename from enterprise/x/aibridged/aibridged_test.go rename to enterprise/aibridged/aibridged_test.go index 967e9aac2bce3..5d38b7f54d18c 100644 --- a/enterprise/x/aibridged/aibridged_test.go +++ b/enterprise/aibridged/aibridged_test.go @@ -18,9 +18,9 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/aibridge" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/enterprise/x/aibridged" - mock "github.com/coder/coder/v2/enterprise/x/aibridged/aibridgedmock" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged" + mock "github.com/coder/coder/v2/enterprise/aibridged/aibridgedmock" + "github.com/coder/coder/v2/enterprise/aibridged/proto" "github.com/coder/coder/v2/testutil" ) diff --git a/enterprise/x/aibridged/aibridgedmock/clientmock.go b/enterprise/aibridged/aibridgedmock/clientmock.go similarity index 97% rename from enterprise/x/aibridged/aibridgedmock/clientmock.go rename to enterprise/aibridged/aibridgedmock/clientmock.go index c49a385451a8e..2bb7083e10924 100644 --- a/enterprise/x/aibridged/aibridgedmock/clientmock.go +++ b/enterprise/aibridged/aibridgedmock/clientmock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/coder/coder/v2/enterprise/x/aibridged (interfaces: DRPCClient) +// Source: github.com/coder/coder/v2/enterprise/aibridged (interfaces: DRPCClient) // // Generated by this command: // -// mockgen -destination ./clientmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/x/aibridged DRPCClient +// mockgen -destination ./clientmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/aibridged DRPCClient // // Package aibridgedmock is a generated GoMock package. @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - proto "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + proto "github.com/coder/coder/v2/enterprise/aibridged/proto" gomock "go.uber.org/mock/gomock" drpc "storj.io/drpc" ) diff --git a/enterprise/x/aibridged/aibridgedmock/doc.go b/enterprise/aibridged/aibridgedmock/doc.go similarity index 52% rename from enterprise/x/aibridged/aibridgedmock/doc.go rename to enterprise/aibridged/aibridgedmock/doc.go index 3d3f56c05574d..9c9c644570463 100644 --- a/enterprise/x/aibridged/aibridgedmock/doc.go +++ b/enterprise/aibridged/aibridgedmock/doc.go @@ -1,4 +1,4 @@ package aibridgedmock -//go:generate mockgen -destination ./clientmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/x/aibridged DRPCClient -//go:generate mockgen -destination ./poolmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/x/aibridged Pooler +//go:generate mockgen -destination ./clientmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/aibridged DRPCClient +//go:generate mockgen -destination ./poolmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/aibridged Pooler diff --git a/enterprise/x/aibridged/aibridgedmock/poolmock.go b/enterprise/aibridged/aibridgedmock/poolmock.go similarity index 91% rename from enterprise/x/aibridged/aibridgedmock/poolmock.go rename to enterprise/aibridged/aibridgedmock/poolmock.go index bf3b39ed2a879..fcd941fc7c989 100644 --- a/enterprise/x/aibridged/aibridgedmock/poolmock.go +++ b/enterprise/aibridged/aibridgedmock/poolmock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/coder/coder/v2/enterprise/x/aibridged (interfaces: Pooler) +// Source: github.com/coder/coder/v2/enterprise/aibridged (interfaces: Pooler) // // Generated by this command: // -// mockgen -destination ./poolmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/x/aibridged Pooler +// mockgen -destination ./poolmock.go -package aibridgedmock github.com/coder/coder/v2/enterprise/aibridged Pooler // // Package aibridgedmock is a generated GoMock package. @@ -14,7 +14,7 @@ import ( http "net/http" reflect "reflect" - aibridged "github.com/coder/coder/v2/enterprise/x/aibridged" + aibridged "github.com/coder/coder/v2/enterprise/aibridged" gomock "go.uber.org/mock/gomock" ) diff --git a/enterprise/x/aibridged/client.go b/enterprise/aibridged/client.go similarity index 90% rename from enterprise/x/aibridged/client.go rename to enterprise/aibridged/client.go index 3004a84df9626..60650bf994f28 100644 --- a/enterprise/x/aibridged/client.go +++ b/enterprise/aibridged/client.go @@ -5,7 +5,7 @@ import ( "storj.io/drpc" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged/proto" ) type Dialer func(ctx context.Context) (DRPCClient, error) diff --git a/enterprise/x/aibridged/http.go b/enterprise/aibridged/http.go similarity index 98% rename from enterprise/x/aibridged/http.go rename to enterprise/aibridged/http.go index 43f4ba7670671..e87238cc7bbc0 100644 --- a/enterprise/x/aibridged/http.go +++ b/enterprise/aibridged/http.go @@ -9,7 +9,7 @@ import ( "cdr.dev/slog" "github.com/coder/aibridge" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged/proto" ) var _ http.Handler = &Server{} diff --git a/enterprise/x/aibridged/mcp.go b/enterprise/aibridged/mcp.go similarity index 99% rename from enterprise/x/aibridged/mcp.go rename to enterprise/aibridged/mcp.go index 4b42287e02899..ab6d1d0031d37 100644 --- a/enterprise/x/aibridged/mcp.go +++ b/enterprise/aibridged/mcp.go @@ -10,7 +10,7 @@ import ( "cdr.dev/slog" "github.com/coder/aibridge/mcp" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged/proto" ) var ( diff --git a/enterprise/x/aibridged/mcp_internal_test.go b/enterprise/aibridged/mcp_internal_test.go similarity index 95% rename from enterprise/x/aibridged/mcp_internal_test.go rename to enterprise/aibridged/mcp_internal_test.go index 20edf79d06bf5..37fb6fe2c25d2 100644 --- a/enterprise/x/aibridged/mcp_internal_test.go +++ b/enterprise/aibridged/mcp_internal_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged/proto" "github.com/coder/coder/v2/testutil" ) diff --git a/enterprise/x/aibridged/pool.go b/enterprise/aibridged/pool.go similarity index 100% rename from enterprise/x/aibridged/pool.go rename to enterprise/aibridged/pool.go diff --git a/enterprise/x/aibridged/pool_test.go b/enterprise/aibridged/pool_test.go similarity index 96% rename from enterprise/x/aibridged/pool_test.go rename to enterprise/aibridged/pool_test.go index 38cae85da9d92..e3609144f0d59 100644 --- a/enterprise/x/aibridged/pool_test.go +++ b/enterprise/aibridged/pool_test.go @@ -13,8 +13,8 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/aibridge/mcp" "github.com/coder/aibridge/mcpmock" - "github.com/coder/coder/v2/enterprise/x/aibridged" - mock "github.com/coder/coder/v2/enterprise/x/aibridged/aibridgedmock" + "github.com/coder/coder/v2/enterprise/aibridged" + mock "github.com/coder/coder/v2/enterprise/aibridged/aibridgedmock" ) // TestPool validates the published behavior of [aibridged.CachedBridgePool]. diff --git a/enterprise/x/aibridged/proto/aibridged.pb.go b/enterprise/aibridged/proto/aibridged.pb.go similarity index 60% rename from enterprise/x/aibridged/proto/aibridged.pb.go rename to enterprise/aibridged/proto/aibridged.pb.go index 41d31563b4043..a13a39ed95245 100644 --- a/enterprise/x/aibridged/proto/aibridged.pb.go +++ b/enterprise/aibridged/proto/aibridged.pb.go @@ -2,7 +2,7 @@ // versions: // protoc-gen-go v1.30.0 // protoc v4.23.4 -// source: enterprise/x/aibridged/proto/aibridged.proto +// source: enterprise/aibridged/proto/aibridged.proto package proto @@ -38,7 +38,7 @@ type RecordInterceptionRequest struct { func (x *RecordInterceptionRequest) Reset() { *x = RecordInterceptionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[0] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -51,7 +51,7 @@ func (x *RecordInterceptionRequest) String() string { func (*RecordInterceptionRequest) ProtoMessage() {} func (x *RecordInterceptionRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[0] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -64,7 +64,7 @@ func (x *RecordInterceptionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordInterceptionRequest.ProtoReflect.Descriptor instead. func (*RecordInterceptionRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{0} } func (x *RecordInterceptionRequest) GetId() string { @@ -118,7 +118,7 @@ type RecordInterceptionResponse struct { func (x *RecordInterceptionResponse) Reset() { *x = RecordInterceptionResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[1] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -131,7 +131,7 @@ func (x *RecordInterceptionResponse) String() string { func (*RecordInterceptionResponse) ProtoMessage() {} func (x *RecordInterceptionResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[1] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -144,7 +144,7 @@ func (x *RecordInterceptionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordInterceptionResponse.ProtoReflect.Descriptor instead. func (*RecordInterceptionResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{1} } type RecordInterceptionEndedRequest struct { @@ -159,7 +159,7 @@ type RecordInterceptionEndedRequest struct { func (x *RecordInterceptionEndedRequest) Reset() { *x = RecordInterceptionEndedRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[2] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -172,7 +172,7 @@ func (x *RecordInterceptionEndedRequest) String() string { func (*RecordInterceptionEndedRequest) ProtoMessage() {} func (x *RecordInterceptionEndedRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[2] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -185,7 +185,7 @@ func (x *RecordInterceptionEndedRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordInterceptionEndedRequest.ProtoReflect.Descriptor instead. func (*RecordInterceptionEndedRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{2} } func (x *RecordInterceptionEndedRequest) GetId() string { @@ -211,7 +211,7 @@ type RecordInterceptionEndedResponse struct { func (x *RecordInterceptionEndedResponse) Reset() { *x = RecordInterceptionEndedResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[3] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -224,7 +224,7 @@ func (x *RecordInterceptionEndedResponse) String() string { func (*RecordInterceptionEndedResponse) ProtoMessage() {} func (x *RecordInterceptionEndedResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[3] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -237,7 +237,7 @@ func (x *RecordInterceptionEndedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordInterceptionEndedResponse.ProtoReflect.Descriptor instead. func (*RecordInterceptionEndedResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{3} } type RecordTokenUsageRequest struct { @@ -256,7 +256,7 @@ type RecordTokenUsageRequest struct { func (x *RecordTokenUsageRequest) Reset() { *x = RecordTokenUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[4] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -269,7 +269,7 @@ func (x *RecordTokenUsageRequest) String() string { func (*RecordTokenUsageRequest) ProtoMessage() {} func (x *RecordTokenUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[4] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -282,7 +282,7 @@ func (x *RecordTokenUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordTokenUsageRequest.ProtoReflect.Descriptor instead. func (*RecordTokenUsageRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{4} } func (x *RecordTokenUsageRequest) GetInterceptionId() string { @@ -336,7 +336,7 @@ type RecordTokenUsageResponse struct { func (x *RecordTokenUsageResponse) Reset() { *x = RecordTokenUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[5] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -349,7 +349,7 @@ func (x *RecordTokenUsageResponse) String() string { func (*RecordTokenUsageResponse) ProtoMessage() {} func (x *RecordTokenUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[5] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -362,7 +362,7 @@ func (x *RecordTokenUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordTokenUsageResponse.ProtoReflect.Descriptor instead. func (*RecordTokenUsageResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{5} } type RecordPromptUsageRequest struct { @@ -380,7 +380,7 @@ type RecordPromptUsageRequest struct { func (x *RecordPromptUsageRequest) Reset() { *x = RecordPromptUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[6] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -393,7 +393,7 @@ func (x *RecordPromptUsageRequest) String() string { func (*RecordPromptUsageRequest) ProtoMessage() {} func (x *RecordPromptUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[6] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -406,7 +406,7 @@ func (x *RecordPromptUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordPromptUsageRequest.ProtoReflect.Descriptor instead. func (*RecordPromptUsageRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{6} } func (x *RecordPromptUsageRequest) GetInterceptionId() string { @@ -453,7 +453,7 @@ type RecordPromptUsageResponse struct { func (x *RecordPromptUsageResponse) Reset() { *x = RecordPromptUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[7] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -466,7 +466,7 @@ func (x *RecordPromptUsageResponse) String() string { func (*RecordPromptUsageResponse) ProtoMessage() {} func (x *RecordPromptUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[7] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -479,7 +479,7 @@ func (x *RecordPromptUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordPromptUsageResponse.ProtoReflect.Descriptor instead. func (*RecordPromptUsageResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{7} } type RecordToolUsageRequest struct { @@ -501,7 +501,7 @@ type RecordToolUsageRequest struct { func (x *RecordToolUsageRequest) Reset() { *x = RecordToolUsageRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[8] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -514,7 +514,7 @@ func (x *RecordToolUsageRequest) String() string { func (*RecordToolUsageRequest) ProtoMessage() {} func (x *RecordToolUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[8] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -527,7 +527,7 @@ func (x *RecordToolUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordToolUsageRequest.ProtoReflect.Descriptor instead. func (*RecordToolUsageRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{8} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{8} } func (x *RecordToolUsageRequest) GetInterceptionId() string { @@ -602,7 +602,7 @@ type RecordToolUsageResponse struct { func (x *RecordToolUsageResponse) Reset() { *x = RecordToolUsageResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[9] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -615,7 +615,7 @@ func (x *RecordToolUsageResponse) String() string { func (*RecordToolUsageResponse) ProtoMessage() {} func (x *RecordToolUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[9] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -628,7 +628,7 @@ func (x *RecordToolUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RecordToolUsageResponse.ProtoReflect.Descriptor instead. func (*RecordToolUsageResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{9} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{9} } type GetMCPServerConfigsRequest struct { @@ -642,7 +642,7 @@ type GetMCPServerConfigsRequest struct { func (x *GetMCPServerConfigsRequest) Reset() { *x = GetMCPServerConfigsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[10] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -655,7 +655,7 @@ func (x *GetMCPServerConfigsRequest) String() string { func (*GetMCPServerConfigsRequest) ProtoMessage() {} func (x *GetMCPServerConfigsRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[10] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -668,7 +668,7 @@ func (x *GetMCPServerConfigsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMCPServerConfigsRequest.ProtoReflect.Descriptor instead. func (*GetMCPServerConfigsRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{10} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{10} } func (x *GetMCPServerConfigsRequest) GetUserId() string { @@ -690,7 +690,7 @@ type GetMCPServerConfigsResponse struct { func (x *GetMCPServerConfigsResponse) Reset() { *x = GetMCPServerConfigsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[11] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -703,7 +703,7 @@ func (x *GetMCPServerConfigsResponse) String() string { func (*GetMCPServerConfigsResponse) ProtoMessage() {} func (x *GetMCPServerConfigsResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[11] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -716,7 +716,7 @@ func (x *GetMCPServerConfigsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMCPServerConfigsResponse.ProtoReflect.Descriptor instead. func (*GetMCPServerConfigsResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{11} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{11} } func (x *GetMCPServerConfigsResponse) GetCoderMcpConfig() *MCPServerConfig { @@ -747,7 +747,7 @@ type MCPServerConfig struct { func (x *MCPServerConfig) Reset() { *x = MCPServerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[12] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -760,7 +760,7 @@ func (x *MCPServerConfig) String() string { func (*MCPServerConfig) ProtoMessage() {} func (x *MCPServerConfig) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[12] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -773,7 +773,7 @@ func (x *MCPServerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use MCPServerConfig.ProtoReflect.Descriptor instead. func (*MCPServerConfig) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{12} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{12} } func (x *MCPServerConfig) GetId() string { @@ -816,7 +816,7 @@ type GetMCPServerAccessTokensBatchRequest struct { func (x *GetMCPServerAccessTokensBatchRequest) Reset() { *x = GetMCPServerAccessTokensBatchRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[13] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -829,7 +829,7 @@ func (x *GetMCPServerAccessTokensBatchRequest) String() string { func (*GetMCPServerAccessTokensBatchRequest) ProtoMessage() {} func (x *GetMCPServerAccessTokensBatchRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[13] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -842,7 +842,7 @@ func (x *GetMCPServerAccessTokensBatchRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetMCPServerAccessTokensBatchRequest.ProtoReflect.Descriptor instead. func (*GetMCPServerAccessTokensBatchRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{13} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{13} } func (x *GetMCPServerAccessTokensBatchRequest) GetUserId() string { @@ -873,7 +873,7 @@ type GetMCPServerAccessTokensBatchResponse struct { func (x *GetMCPServerAccessTokensBatchResponse) Reset() { *x = GetMCPServerAccessTokensBatchResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[14] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -886,7 +886,7 @@ func (x *GetMCPServerAccessTokensBatchResponse) String() string { func (*GetMCPServerAccessTokensBatchResponse) ProtoMessage() {} func (x *GetMCPServerAccessTokensBatchResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[14] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -899,7 +899,7 @@ func (x *GetMCPServerAccessTokensBatchResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetMCPServerAccessTokensBatchResponse.ProtoReflect.Descriptor instead. func (*GetMCPServerAccessTokensBatchResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{14} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{14} } func (x *GetMCPServerAccessTokensBatchResponse) GetAccessTokens() map[string]string { @@ -927,7 +927,7 @@ type IsAuthorizedRequest struct { func (x *IsAuthorizedRequest) Reset() { *x = IsAuthorizedRequest{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[15] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -940,7 +940,7 @@ func (x *IsAuthorizedRequest) String() string { func (*IsAuthorizedRequest) ProtoMessage() {} func (x *IsAuthorizedRequest) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[15] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -953,7 +953,7 @@ func (x *IsAuthorizedRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use IsAuthorizedRequest.ProtoReflect.Descriptor instead. func (*IsAuthorizedRequest) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{15} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{15} } func (x *IsAuthorizedRequest) GetKey() string { @@ -974,7 +974,7 @@ type IsAuthorizedResponse struct { func (x *IsAuthorizedResponse) Reset() { *x = IsAuthorizedResponse{} if protoimpl.UnsafeEnabled { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[16] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -987,7 +987,7 @@ func (x *IsAuthorizedResponse) String() string { func (*IsAuthorizedResponse) ProtoMessage() {} func (x *IsAuthorizedResponse) ProtoReflect() protoreflect.Message { - mi := &file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[16] + mi := &file_enterprise_aibridged_proto_aibridged_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1000,7 +1000,7 @@ func (x *IsAuthorizedResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use IsAuthorizedResponse.ProtoReflect.Descriptor instead. func (*IsAuthorizedResponse) Descriptor() ([]byte, []int) { - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{16} + return file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{16} } func (x *IsAuthorizedResponse) GetOwnerId() string { @@ -1010,257 +1010,256 @@ func (x *IsAuthorizedResponse) GetOwnerId() string { return "" } -var File_enterprise_x_aibridged_proto_aibridged_proto protoreflect.FileDescriptor - -var file_enterprise_x_aibridged_proto_aibridged_proto_rawDesc = []byte{ - 0x0a, 0x2c, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x2f, 0x78, 0x2f, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, - 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0xda, 0x02, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, - 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x14, - 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1c, - 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x67, 0x0a, 0x1e, +var File_enterprise_aibridged_proto_aibridged_proto protoreflect.FileDescriptor + +var file_enterprise_aibridged_proto_aibridged_proto_rawDesc = []byte{ + 0x0a, 0x2a, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x2f, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x69, 0x62, + 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0xda, 0x02, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, + 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, + 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x12, 0x4a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, + 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, + 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1c, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, - 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, - 0x64, 0x65, 0x64, 0x41, 0x74, 0x22, 0x21, 0x0a, 0x1f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xf9, 0x02, 0x0a, 0x17, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x67, 0x0a, 0x1e, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, + 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x65, + 0x64, 0x41, 0x74, 0x22, 0x21, 0x0a, 0x1f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, + 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xf9, 0x02, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, + 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, + 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x48, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, + 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xcb, + 0x02, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x6f, + 0x6d, 0x70, 0x74, 0x12, 0x49, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, + 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, + 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1b, 0x0a, 0x19, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xed, 0x03, 0x0a, 0x16, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, - 0x73, 0x67, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, - 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x48, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0xcb, 0x02, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, - 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, - 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, - 0x06, 0x70, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x12, 0x49, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1b, - 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xed, 0x03, 0x0a, 0x16, - 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, - 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, - 0x15, 0x0a, 0x06, 0x6d, 0x73, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6d, 0x73, 0x67, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, - 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, - 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, - 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, - 0x12, 0x2e, 0x0a, 0x10, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0f, 0x69, 0x6e, - 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x88, 0x01, 0x01, - 0x12, 0x47, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x52, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb2, 0x01, - 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, - 0x10, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x51, 0x0a, 0x19, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, - 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x43, 0x50, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x16, 0x65, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x73, 0x22, 0x85, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x6f, 0x6c, - 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6f, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x67, - 0x65, 0x78, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x64, 0x65, 0x6e, 0x79, 0x5f, - 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x6f, 0x6f, - 0x6c, 0x44, 0x65, 0x6e, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x22, 0x72, 0x0a, 0x24, 0x47, 0x65, - 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x67, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x55, 0x72, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x6f, 0x6f, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x6f, 0x6f, 0x6c, 0x12, 0x14, 0x0a, 0x05, + 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x2e, + 0x0a, 0x10, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0f, 0x69, 0x6e, 0x76, 0x6f, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x12, 0x47, + 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, + 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x1a, 0x51, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x5f, 0x75, 0x72, 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x19, 0x0a, 0x17, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x15, 0x6d, - 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x6d, 0x63, 0x70, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x73, 0x22, 0xda, - 0x02, 0x0a, 0x25, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x3e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x50, 0x0a, - 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb2, 0x01, 0x0a, 0x1b, + 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x10, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x43, + 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x51, 0x0a, + 0x19, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, + 0x63, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x16, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x63, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, + 0x22, 0x85, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x74, 0x6f, 0x6f, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x67, 0x65, 0x78, + 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x6f, 0x6f, 0x6c, 0x5f, 0x64, 0x65, 0x6e, 0x79, 0x5f, 0x72, 0x65, + 0x67, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x6f, 0x6f, 0x6c, 0x44, + 0x65, 0x6e, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x22, 0x72, 0x0a, 0x24, 0x47, 0x65, 0x74, 0x4d, + 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x15, 0x6d, 0x63, 0x70, + 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x69, + 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x73, 0x22, 0xda, 0x02, 0x0a, + 0x25, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, - 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, - 0x3f, 0x0a, 0x11, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x1a, 0x39, 0x0a, 0x0b, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x27, 0x0a, 0x13, 0x49, - 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x22, 0x31, 0x0a, 0x14, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, - 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x32, 0xce, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x65, 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x68, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, - 0x0a, 0x11, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, - 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, - 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, - 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, - 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, - 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, + 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x41, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x50, 0x0a, 0x06, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x55, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x3f, 0x0a, + 0x11, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x39, + 0x0a, 0x0b, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x27, 0x0a, 0x13, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, - 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x22, 0x31, 0x0a, 0x14, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x49, 0x64, 0x32, 0xce, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x65, 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, + 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, + 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, + 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, + 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, + 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, + 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x55, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, + 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( - file_enterprise_x_aibridged_proto_aibridged_proto_rawDescOnce sync.Once - file_enterprise_x_aibridged_proto_aibridged_proto_rawDescData = file_enterprise_x_aibridged_proto_aibridged_proto_rawDesc + file_enterprise_aibridged_proto_aibridged_proto_rawDescOnce sync.Once + file_enterprise_aibridged_proto_aibridged_proto_rawDescData = file_enterprise_aibridged_proto_aibridged_proto_rawDesc ) -func file_enterprise_x_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { - file_enterprise_x_aibridged_proto_aibridged_proto_rawDescOnce.Do(func() { - file_enterprise_x_aibridged_proto_aibridged_proto_rawDescData = protoimpl.X.CompressGZIP(file_enterprise_x_aibridged_proto_aibridged_proto_rawDescData) +func file_enterprise_aibridged_proto_aibridged_proto_rawDescGZIP() []byte { + file_enterprise_aibridged_proto_aibridged_proto_rawDescOnce.Do(func() { + file_enterprise_aibridged_proto_aibridged_proto_rawDescData = protoimpl.X.CompressGZIP(file_enterprise_aibridged_proto_aibridged_proto_rawDescData) }) - return file_enterprise_x_aibridged_proto_aibridged_proto_rawDescData + return file_enterprise_aibridged_proto_aibridged_proto_rawDescData } -var file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 23) -var file_enterprise_x_aibridged_proto_aibridged_proto_goTypes = []interface{}{ +var file_enterprise_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_enterprise_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*RecordInterceptionRequest)(nil), // 0: proto.RecordInterceptionRequest (*RecordInterceptionResponse)(nil), // 1: proto.RecordInterceptionResponse (*RecordInterceptionEndedRequest)(nil), // 2: proto.RecordInterceptionEndedRequest @@ -1287,7 +1286,7 @@ var file_enterprise_x_aibridged_proto_aibridged_proto_goTypes = []interface{}{ (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp (*anypb.Any)(nil), // 24: google.protobuf.Any } -var file_enterprise_x_aibridged_proto_aibridged_proto_depIdxs = []int32{ +var file_enterprise_aibridged_proto_aibridged_proto_depIdxs = []int32{ 17, // 0: proto.RecordInterceptionRequest.metadata:type_name -> proto.RecordInterceptionRequest.MetadataEntry 23, // 1: proto.RecordInterceptionRequest.started_at:type_name -> google.protobuf.Timestamp 23, // 2: proto.RecordInterceptionEndedRequest.ended_at:type_name -> google.protobuf.Timestamp @@ -1328,13 +1327,13 @@ var file_enterprise_x_aibridged_proto_aibridged_proto_depIdxs = []int32{ 0, // [0:17] is the sub-list for field type_name } -func init() { file_enterprise_x_aibridged_proto_aibridged_proto_init() } -func file_enterprise_x_aibridged_proto_aibridged_proto_init() { - if File_enterprise_x_aibridged_proto_aibridged_proto != nil { +func init() { file_enterprise_aibridged_proto_aibridged_proto_init() } +func file_enterprise_aibridged_proto_aibridged_proto_init() { + if File_enterprise_aibridged_proto_aibridged_proto != nil { return } if !protoimpl.UnsafeEnabled { - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordInterceptionRequest); i { case 0: return &v.state @@ -1346,7 +1345,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordInterceptionResponse); i { case 0: return &v.state @@ -1358,7 +1357,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordInterceptionEndedRequest); i { case 0: return &v.state @@ -1370,7 +1369,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordInterceptionEndedResponse); i { case 0: return &v.state @@ -1382,7 +1381,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordTokenUsageRequest); i { case 0: return &v.state @@ -1394,7 +1393,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordTokenUsageResponse); i { case 0: return &v.state @@ -1406,7 +1405,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordPromptUsageRequest); i { case 0: return &v.state @@ -1418,7 +1417,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordPromptUsageResponse); i { case 0: return &v.state @@ -1430,7 +1429,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordToolUsageRequest); i { case 0: return &v.state @@ -1442,7 +1441,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RecordToolUsageResponse); i { case 0: return &v.state @@ -1454,7 +1453,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetMCPServerConfigsRequest); i { case 0: return &v.state @@ -1466,7 +1465,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetMCPServerConfigsResponse); i { case 0: return &v.state @@ -1478,7 +1477,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*MCPServerConfig); i { case 0: return &v.state @@ -1490,7 +1489,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetMCPServerAccessTokensBatchRequest); i { case 0: return &v.state @@ -1502,7 +1501,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetMCPServerAccessTokensBatchResponse); i { case 0: return &v.state @@ -1514,7 +1513,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IsAuthorizedRequest); i { case 0: return &v.state @@ -1526,7 +1525,7 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { return nil } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IsAuthorizedResponse); i { case 0: return &v.state @@ -1539,23 +1538,23 @@ func file_enterprise_x_aibridged_proto_aibridged_proto_init() { } } } - file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes[8].OneofWrappers = []interface{}{} + file_enterprise_aibridged_proto_aibridged_proto_msgTypes[8].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_enterprise_x_aibridged_proto_aibridged_proto_rawDesc, + RawDescriptor: file_enterprise_aibridged_proto_aibridged_proto_rawDesc, NumEnums: 0, NumMessages: 23, NumExtensions: 0, NumServices: 3, }, - GoTypes: file_enterprise_x_aibridged_proto_aibridged_proto_goTypes, - DependencyIndexes: file_enterprise_x_aibridged_proto_aibridged_proto_depIdxs, - MessageInfos: file_enterprise_x_aibridged_proto_aibridged_proto_msgTypes, + GoTypes: file_enterprise_aibridged_proto_aibridged_proto_goTypes, + DependencyIndexes: file_enterprise_aibridged_proto_aibridged_proto_depIdxs, + MessageInfos: file_enterprise_aibridged_proto_aibridged_proto_msgTypes, }.Build() - File_enterprise_x_aibridged_proto_aibridged_proto = out.File - file_enterprise_x_aibridged_proto_aibridged_proto_rawDesc = nil - file_enterprise_x_aibridged_proto_aibridged_proto_goTypes = nil - file_enterprise_x_aibridged_proto_aibridged_proto_depIdxs = nil + File_enterprise_aibridged_proto_aibridged_proto = out.File + file_enterprise_aibridged_proto_aibridged_proto_rawDesc = nil + file_enterprise_aibridged_proto_aibridged_proto_goTypes = nil + file_enterprise_aibridged_proto_aibridged_proto_depIdxs = nil } diff --git a/enterprise/x/aibridged/proto/aibridged.proto b/enterprise/aibridged/proto/aibridged.proto similarity index 100% rename from enterprise/x/aibridged/proto/aibridged.proto rename to enterprise/aibridged/proto/aibridged.proto diff --git a/enterprise/x/aibridged/proto/aibridged_drpc.pb.go b/enterprise/aibridged/proto/aibridged_drpc.pb.go similarity index 84% rename from enterprise/x/aibridged/proto/aibridged_drpc.pb.go rename to enterprise/aibridged/proto/aibridged_drpc.pb.go index 4c7cb3c190764..1309957d153d5 100644 --- a/enterprise/x/aibridged/proto/aibridged_drpc.pb.go +++ b/enterprise/aibridged/proto/aibridged_drpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-drpc. DO NOT EDIT. // protoc-gen-go-drpc version: v0.0.34 -// source: enterprise/x/aibridged/proto/aibridged.proto +// source: enterprise/aibridged/proto/aibridged.proto package proto @@ -13,25 +13,25 @@ import ( drpcerr "storj.io/drpc/drpcerr" ) -type drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto struct{} +type drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto struct{} -func (drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto) Marshal(msg drpc.Message) ([]byte, error) { +func (drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto) Marshal(msg drpc.Message) ([]byte, error) { return proto.Marshal(msg.(proto.Message)) } -func (drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) { +func (drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) { return proto.MarshalOptions{}.MarshalAppend(buf, msg.(proto.Message)) } -func (drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto) Unmarshal(buf []byte, msg drpc.Message) error { +func (drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto) Unmarshal(buf []byte, msg drpc.Message) error { return proto.Unmarshal(buf, msg.(proto.Message)) } -func (drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto) JSONMarshal(msg drpc.Message) ([]byte, error) { +func (drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto) JSONMarshal(msg drpc.Message) ([]byte, error) { return protojson.Marshal(msg.(proto.Message)) } -func (drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error { +func (drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error { return protojson.Unmarshal(buf, msg.(proto.Message)) } @@ -57,7 +57,7 @@ func (c *drpcRecorderClient) DRPCConn() drpc.Conn { return c.cc } func (c *drpcRecorderClient) RecordInterception(ctx context.Context, in *RecordInterceptionRequest) (*RecordInterceptionResponse, error) { out := new(RecordInterceptionResponse) - err := c.cc.Invoke(ctx, "/proto.Recorder/RecordInterception", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.Recorder/RecordInterception", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func (c *drpcRecorderClient) RecordInterception(ctx context.Context, in *RecordI func (c *drpcRecorderClient) RecordInterceptionEnded(ctx context.Context, in *RecordInterceptionEndedRequest) (*RecordInterceptionEndedResponse, error) { out := new(RecordInterceptionEndedResponse) - err := c.cc.Invoke(ctx, "/proto.Recorder/RecordInterceptionEnded", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.Recorder/RecordInterceptionEnded", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -75,7 +75,7 @@ func (c *drpcRecorderClient) RecordInterceptionEnded(ctx context.Context, in *Re func (c *drpcRecorderClient) RecordTokenUsage(ctx context.Context, in *RecordTokenUsageRequest) (*RecordTokenUsageResponse, error) { out := new(RecordTokenUsageResponse) - err := c.cc.Invoke(ctx, "/proto.Recorder/RecordTokenUsage", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.Recorder/RecordTokenUsage", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func (c *drpcRecorderClient) RecordTokenUsage(ctx context.Context, in *RecordTok func (c *drpcRecorderClient) RecordPromptUsage(ctx context.Context, in *RecordPromptUsageRequest) (*RecordPromptUsageResponse, error) { out := new(RecordPromptUsageResponse) - err := c.cc.Invoke(ctx, "/proto.Recorder/RecordPromptUsage", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.Recorder/RecordPromptUsage", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -93,7 +93,7 @@ func (c *drpcRecorderClient) RecordPromptUsage(ctx context.Context, in *RecordPr func (c *drpcRecorderClient) RecordToolUsage(ctx context.Context, in *RecordToolUsageRequest) (*RecordToolUsageResponse, error) { out := new(RecordToolUsageResponse) - err := c.cc.Invoke(ctx, "/proto.Recorder/RecordToolUsage", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.Recorder/RecordToolUsage", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -137,7 +137,7 @@ func (DRPCRecorderDescription) NumMethods() int { return 5 } func (DRPCRecorderDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { case 0: - return "/proto.Recorder/RecordInterception", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.Recorder/RecordInterception", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCRecorderServer). RecordInterception( @@ -146,7 +146,7 @@ func (DRPCRecorderDescription) Method(n int) (string, drpc.Encoding, drpc.Receiv ) }, DRPCRecorderServer.RecordInterception, true case 1: - return "/proto.Recorder/RecordInterceptionEnded", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.Recorder/RecordInterceptionEnded", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCRecorderServer). RecordInterceptionEnded( @@ -155,7 +155,7 @@ func (DRPCRecorderDescription) Method(n int) (string, drpc.Encoding, drpc.Receiv ) }, DRPCRecorderServer.RecordInterceptionEnded, true case 2: - return "/proto.Recorder/RecordTokenUsage", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.Recorder/RecordTokenUsage", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCRecorderServer). RecordTokenUsage( @@ -164,7 +164,7 @@ func (DRPCRecorderDescription) Method(n int) (string, drpc.Encoding, drpc.Receiv ) }, DRPCRecorderServer.RecordTokenUsage, true case 3: - return "/proto.Recorder/RecordPromptUsage", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.Recorder/RecordPromptUsage", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCRecorderServer). RecordPromptUsage( @@ -173,7 +173,7 @@ func (DRPCRecorderDescription) Method(n int) (string, drpc.Encoding, drpc.Receiv ) }, DRPCRecorderServer.RecordPromptUsage, true case 4: - return "/proto.Recorder/RecordToolUsage", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.Recorder/RecordToolUsage", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCRecorderServer). RecordToolUsage( @@ -200,7 +200,7 @@ type drpcRecorder_RecordInterceptionStream struct { } func (x *drpcRecorder_RecordInterceptionStream) SendAndClose(m *RecordInterceptionResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -216,7 +216,7 @@ type drpcRecorder_RecordInterceptionEndedStream struct { } func (x *drpcRecorder_RecordInterceptionEndedStream) SendAndClose(m *RecordInterceptionEndedResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -232,7 +232,7 @@ type drpcRecorder_RecordTokenUsageStream struct { } func (x *drpcRecorder_RecordTokenUsageStream) SendAndClose(m *RecordTokenUsageResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -248,7 +248,7 @@ type drpcRecorder_RecordPromptUsageStream struct { } func (x *drpcRecorder_RecordPromptUsageStream) SendAndClose(m *RecordPromptUsageResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -264,7 +264,7 @@ type drpcRecorder_RecordToolUsageStream struct { } func (x *drpcRecorder_RecordToolUsageStream) SendAndClose(m *RecordToolUsageResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -289,7 +289,7 @@ func (c *drpcMCPConfiguratorClient) DRPCConn() drpc.Conn { return c.cc } func (c *drpcMCPConfiguratorClient) GetMCPServerConfigs(ctx context.Context, in *GetMCPServerConfigsRequest) (*GetMCPServerConfigsResponse, error) { out := new(GetMCPServerConfigsResponse) - err := c.cc.Invoke(ctx, "/proto.MCPConfigurator/GetMCPServerConfigs", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.MCPConfigurator/GetMCPServerConfigs", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -298,7 +298,7 @@ func (c *drpcMCPConfiguratorClient) GetMCPServerConfigs(ctx context.Context, in func (c *drpcMCPConfiguratorClient) GetMCPServerAccessTokensBatch(ctx context.Context, in *GetMCPServerAccessTokensBatchRequest) (*GetMCPServerAccessTokensBatchResponse, error) { out := new(GetMCPServerAccessTokensBatchResponse) - err := c.cc.Invoke(ctx, "/proto.MCPConfigurator/GetMCPServerAccessTokensBatch", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.MCPConfigurator/GetMCPServerAccessTokensBatch", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -327,7 +327,7 @@ func (DRPCMCPConfiguratorDescription) NumMethods() int { return 2 } func (DRPCMCPConfiguratorDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { case 0: - return "/proto.MCPConfigurator/GetMCPServerConfigs", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.MCPConfigurator/GetMCPServerConfigs", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCMCPConfiguratorServer). GetMCPServerConfigs( @@ -336,7 +336,7 @@ func (DRPCMCPConfiguratorDescription) Method(n int) (string, drpc.Encoding, drpc ) }, DRPCMCPConfiguratorServer.GetMCPServerConfigs, true case 1: - return "/proto.MCPConfigurator/GetMCPServerAccessTokensBatch", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.MCPConfigurator/GetMCPServerAccessTokensBatch", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCMCPConfiguratorServer). GetMCPServerAccessTokensBatch( @@ -363,7 +363,7 @@ type drpcMCPConfigurator_GetMCPServerConfigsStream struct { } func (x *drpcMCPConfigurator_GetMCPServerConfigsStream) SendAndClose(m *GetMCPServerConfigsResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -379,7 +379,7 @@ type drpcMCPConfigurator_GetMCPServerAccessTokensBatchStream struct { } func (x *drpcMCPConfigurator_GetMCPServerAccessTokensBatchStream) SendAndClose(m *GetMCPServerAccessTokensBatchResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() @@ -403,7 +403,7 @@ func (c *drpcAuthorizerClient) DRPCConn() drpc.Conn { return c.cc } func (c *drpcAuthorizerClient) IsAuthorized(ctx context.Context, in *IsAuthorizedRequest) (*IsAuthorizedResponse, error) { out := new(IsAuthorizedResponse) - err := c.cc.Invoke(ctx, "/proto.Authorizer/IsAuthorized", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, in, out) + err := c.cc.Invoke(ctx, "/proto.Authorizer/IsAuthorized", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, in, out) if err != nil { return nil, err } @@ -427,7 +427,7 @@ func (DRPCAuthorizerDescription) NumMethods() int { return 1 } func (DRPCAuthorizerDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { case 0: - return "/proto.Authorizer/IsAuthorized", drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}, + return "/proto.Authorizer/IsAuthorized", drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAuthorizerServer). IsAuthorized( @@ -454,7 +454,7 @@ type drpcAuthorizer_IsAuthorizedStream struct { } func (x *drpcAuthorizer_IsAuthorizedStream) SendAndClose(m *IsAuthorizedResponse) error { - if err := x.MsgSend(m, drpcEncoding_File_enterprise_x_aibridged_proto_aibridged_proto{}); err != nil { + if err := x.MsgSend(m, drpcEncoding_File_enterprise_aibridged_proto_aibridged_proto{}); err != nil { return err } return x.CloseSend() diff --git a/enterprise/x/aibridged/request.go b/enterprise/aibridged/request.go similarity index 100% rename from enterprise/x/aibridged/request.go rename to enterprise/aibridged/request.go diff --git a/enterprise/x/aibridged/server.go b/enterprise/aibridged/server.go similarity index 68% rename from enterprise/x/aibridged/server.go rename to enterprise/aibridged/server.go index 713ea2a0cd126..052c94dad4a9e 100644 --- a/enterprise/x/aibridged/server.go +++ b/enterprise/aibridged/server.go @@ -1,6 +1,6 @@ package aibridged -import "github.com/coder/coder/v2/enterprise/x/aibridged/proto" +import "github.com/coder/coder/v2/enterprise/aibridged/proto" type DRPCServer interface { proto.DRPCRecorderServer diff --git a/enterprise/x/aibridged/translator.go b/enterprise/aibridged/translator.go similarity index 98% rename from enterprise/x/aibridged/translator.go rename to enterprise/aibridged/translator.go index bfc39d834ad2c..f36185715a745 100644 --- a/enterprise/x/aibridged/translator.go +++ b/enterprise/aibridged/translator.go @@ -11,7 +11,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/coder/coder/v2/coderd/util/ptr" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged/proto" "github.com/coder/aibridge" ) diff --git a/enterprise/x/aibridged/utils_test.go b/enterprise/aibridged/utils_test.go similarity index 100% rename from enterprise/x/aibridged/utils_test.go rename to enterprise/aibridged/utils_test.go diff --git a/enterprise/x/aibridgedserver/aibridgedserver.go b/enterprise/aibridgedserver/aibridgedserver.go similarity index 99% rename from enterprise/x/aibridgedserver/aibridgedserver.go rename to enterprise/aibridgedserver/aibridgedserver.go index 2c5e3ff71c072..6adf7b793c1d2 100644 --- a/enterprise/x/aibridgedserver/aibridgedserver.go +++ b/enterprise/aibridgedserver/aibridgedserver.go @@ -24,8 +24,8 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" codermcp "github.com/coder/coder/v2/coderd/mcp" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/enterprise/x/aibridged" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridged" + "github.com/coder/coder/v2/enterprise/aibridged/proto" ) var ( diff --git a/enterprise/x/aibridgedserver/aibridgedserver_internal_test.go b/enterprise/aibridgedserver/aibridgedserver_internal_test.go similarity index 100% rename from enterprise/x/aibridgedserver/aibridgedserver_internal_test.go rename to enterprise/aibridgedserver/aibridgedserver_internal_test.go diff --git a/enterprise/x/aibridgedserver/aibridgedserver_test.go b/enterprise/aibridgedserver/aibridgedserver_test.go similarity index 99% rename from enterprise/x/aibridgedserver/aibridgedserver_test.go rename to enterprise/aibridgedserver/aibridgedserver_test.go index 4f9f892bc886a..27598c79857f1 100644 --- a/enterprise/x/aibridgedserver/aibridgedserver_test.go +++ b/enterprise/aibridgedserver/aibridgedserver_test.go @@ -28,9 +28,9 @@ import ( codermcp "github.com/coder/coder/v2/coderd/mcp" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" - "github.com/coder/coder/v2/enterprise/x/aibridged" - "github.com/coder/coder/v2/enterprise/x/aibridged/proto" - "github.com/coder/coder/v2/enterprise/x/aibridgedserver" + "github.com/coder/coder/v2/enterprise/aibridged" + "github.com/coder/coder/v2/enterprise/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridgedserver" "github.com/coder/coder/v2/testutil" ) diff --git a/enterprise/cli/exp_aibridge.go b/enterprise/cli/aibridge.go similarity index 97% rename from enterprise/cli/exp_aibridge.go rename to enterprise/cli/aibridge.go index 722f7bf239223..90953b6aa2bf2 100644 --- a/enterprise/cli/exp_aibridge.go +++ b/enterprise/cli/aibridge.go @@ -134,8 +134,7 @@ func (r *RootCmd) aibridgeInterceptionsList() *serpent.Command { return xerrors.Errorf("limit value must be between 1 and %d", maxInterceptionsLimit) } - expCli := codersdk.NewExperimentalClient(client) - resp, err := expCli.AIBridgeListInterceptions(inv.Context(), codersdk.AIBridgeListInterceptionsFilter{ + resp, err := client.AIBridgeListInterceptions(inv.Context(), codersdk.AIBridgeListInterceptionsFilter{ Pagination: codersdk.Pagination{ AfterID: afterID, // #nosec G115 - Checked above. diff --git a/enterprise/cli/exp_aibridge_test.go b/enterprise/cli/aibridge_test.go similarity index 96% rename from enterprise/cli/exp_aibridge_test.go rename to enterprise/cli/aibridge_test.go index 466d6b3df8246..a5b48a14e1c38 100644 --- a/enterprise/cli/exp_aibridge_test.go +++ b/enterprise/cli/aibridge_test.go @@ -27,7 +27,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -55,7 +54,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, nil) args := []string{ - "exp", "aibridge", "interceptions", "list", @@ -78,7 +76,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -137,7 +134,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, nil) args := []string{ - "exp", "aibridge", "interceptions", "list", @@ -164,7 +160,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -192,7 +187,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, nil) args := []string{ - "exp", "aibridge", "interceptions", "list", diff --git a/enterprise/cli/aibridged.go b/enterprise/cli/aibridged.go index 17bb5ebe681fa..b2dc3d7725b93 100644 --- a/enterprise/cli/aibridged.go +++ b/enterprise/cli/aibridged.go @@ -9,8 +9,8 @@ import ( "github.com/coder/aibridge" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/aibridged" "github.com/coder/coder/v2/enterprise/coderd" - "github.com/coder/coder/v2/enterprise/x/aibridged" ) func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) { diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go index 3cec11970369e..78858ef48da7b 100644 --- a/enterprise/cli/root.go +++ b/enterprise/cli/root.go @@ -25,13 +25,12 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command { r.prebuilds(), r.provisionerd(), r.externalWorkspaces(), + r.aibridge(), } } -func (r *RootCmd) enterpriseExperimental() []*serpent.Command { - return []*serpent.Command{ - r.aibridge(), - } +func (*RootCmd) enterpriseExperimental() []*serpent.Command { + return []*serpent.Command{} } func (r *RootCmd) EnterpriseSubcommands() []*serpent.Command { diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go index ea9f2d3e93825..bc77bc54ba522 100644 --- a/enterprise/cli/server.go +++ b/enterprise/cli/server.go @@ -7,7 +7,6 @@ import ( "database/sql" "encoding/base64" "errors" - "fmt" "io" "net/url" @@ -16,8 +15,8 @@ import ( "tailscale.com/types/key" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" + "github.com/coder/coder/v2/enterprise/aibridged" "github.com/coder/coder/v2/enterprise/audit" "github.com/coder/coder/v2/enterprise/audit/backends" "github.com/coder/coder/v2/enterprise/coderd" @@ -25,7 +24,6 @@ import ( "github.com/coder/coder/v2/enterprise/coderd/usage" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/enterprise/trialer" - "github.com/coder/coder/v2/enterprise/x/aibridged" "github.com/coder/coder/v2/tailnet" "github.com/coder/quartz" "github.com/coder/serpent" @@ -146,8 +144,6 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { } closers.Add(publisher) - experiments := agplcoderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value()) - // In-memory aibridge daemon. // TODO(@deansheather): the lifecycle of the aibridged server is // probably better managed by the enterprise API type itself. Managing @@ -155,26 +151,18 @@ func (r *RootCmd) Server(_ func()) *serpent.Command { // is not entitled to the feature. var aibridgeDaemon *aibridged.Server if options.DeploymentValues.AI.BridgeConfig.Enabled { - if experiments.Enabled(codersdk.ExperimentAIBridge) { - aibridgeDaemon, err = newAIBridgeDaemon(api) - if err != nil { - return nil, nil, xerrors.Errorf("create aibridged: %w", err) - } + aibridgeDaemon, err = newAIBridgeDaemon(api) + if err != nil { + return nil, nil, xerrors.Errorf("create aibridged: %w", err) + } - api.RegisterInMemoryAIBridgedHTTPHandler(aibridgeDaemon) + api.RegisterInMemoryAIBridgedHTTPHandler(aibridgeDaemon) - // When running as an in-memory daemon, the HTTP handler is wired into the - // coderd API and therefore is subject to its context. Calling Close() on - // aibridged will NOT affect in-flight requests but those will be closed once - // the API server is itself shutdown. - closers.Add(aibridgeDaemon) - } else { - api.Logger.Warn(ctx, fmt.Sprintf("CODER_AIBRIDGE_ENABLED=true but experiment %q not enabled", codersdk.ExperimentAIBridge)) - } - } else { - if experiments.Enabled(codersdk.ExperimentAIBridge) { - api.Logger.Warn(ctx, "aibridge experiment enabled but CODER_AIBRIDGE_ENABLED=false") - } + // When running as an in-memory daemon, the HTTP handler is wired into the + // coderd API and therefore is subject to its context. Calling Close() on + // aibridged will NOT affect in-flight requests but those will be closed once + // the API server is itself shutdown. + closers.Add(aibridgeDaemon) } return api.AGPL, closers, nil diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden index ddb44f78ae524..8424ccac923a2 100644 --- a/enterprise/cli/testdata/coder_--help.golden +++ b/enterprise/cli/testdata/coder_--help.golden @@ -14,6 +14,7 @@ USAGE: $ coder templates init SUBCOMMANDS: + aibridge Manage AIBridge. external-workspaces Create or manage external workspaces features List Enterprise features groups Manage groups diff --git a/enterprise/cli/testdata/coder_aibridge_--help.golden b/enterprise/cli/testdata/coder_aibridge_--help.golden new file mode 100644 index 0000000000000..d005ae429ad50 --- /dev/null +++ b/enterprise/cli/testdata/coder_aibridge_--help.golden @@ -0,0 +1,12 @@ +coder v0.0.0-devel + +USAGE: + coder aibridge + + Manage AIBridge. + +SUBCOMMANDS: + interceptions Manage AIBridge interceptions. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden b/enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden new file mode 100644 index 0000000000000..1f3b3af5ad3d3 --- /dev/null +++ b/enterprise/cli/testdata/coder_aibridge_interceptions_--help.golden @@ -0,0 +1,12 @@ +coder v0.0.0-devel + +USAGE: + coder aibridge interceptions + + Manage AIBridge interceptions. + +SUBCOMMANDS: + list List AIBridge interceptions as JSON. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden b/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden new file mode 100644 index 0000000000000..c98fd0019a45a --- /dev/null +++ b/enterprise/cli/testdata/coder_aibridge_interceptions_list_--help.golden @@ -0,0 +1,37 @@ +coder v0.0.0-devel + +USAGE: + coder aibridge interceptions list [flags] + + List AIBridge interceptions as JSON. + +OPTIONS: + --after-id string + The ID of the last result on the previous page to use as a pagination + cursor. + + --initiator string + Only return interceptions initiated by this user. Accepts a user ID, + username, or "me". + + --limit int (default: 100) + The limit of results to return. Must be between 1 and 1000. + + --model string + Only return interceptions from this model. + + --provider string + Only return interceptions from this provider. + + --started-after string + Only return interceptions started after this time. Must be before + 'started-before' if set. Accepts a time in the RFC 3339 format, e.g. + "====[timestamp]=====07:00". + + --started-before string + Only return interceptions started before this time. Must be after + 'started-after' if set. Accepts a time in the RFC 3339 format, e.g. + "====[timestamp]=====07:00". + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 162d4214ccc6a..492306c55882d 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -81,6 +81,41 @@ OPTIONS: Periodically check for new releases of Coder and inform the owner. The check is performed once per day. +AIBRIDGE OPTIONS: + --aibridge-anthropic-base-url string, $CODER_AIBRIDGE_ANTHROPIC_BASE_URL (default: https://api.anthropic.com/) + The base URL of the Anthropic API. + + --aibridge-anthropic-key string, $CODER_AIBRIDGE_ANTHROPIC_KEY + The key to authenticate against the Anthropic API. + + --aibridge-bedrock-access-key string, $CODER_AIBRIDGE_BEDROCK_ACCESS_KEY + The access key to authenticate against the AWS Bedrock API. + + --aibridge-bedrock-access-key-secret string, $CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET + The access key secret to use with the access key to authenticate + against the AWS Bedrock API. + + --aibridge-bedrock-model string, $CODER_AIBRIDGE_BEDROCK_MODEL (default: global.anthropic.claude-sonnet-4-5-20250929-v1:0) + The model to use when making requests to the AWS Bedrock API. + + --aibridge-bedrock-region string, $CODER_AIBRIDGE_BEDROCK_REGION + The AWS Bedrock API region. + + --aibridge-bedrock-small-fastmodel string, $CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL (default: global.anthropic.claude-haiku-4-5-20251001-v1:0) + The small fast model to use when making requests to the AWS Bedrock + API. Claude Code uses Haiku-class models to perform background tasks. + See + https://docs.claude.com/en/docs/claude-code/settings#environment-variables. + + --aibridge-enabled bool, $CODER_AIBRIDGE_ENABLED (default: false) + Whether to start an in-memory aibridged instance. + + --aibridge-openai-base-url string, $CODER_AIBRIDGE_OPENAI_BASE_URL (default: https://api.openai.com/v1/) + The base URL of the OpenAI API. + + --aibridge-openai-key string, $CODER_AIBRIDGE_OPENAI_KEY + The key to authenticate against the OpenAI API. + CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. diff --git a/enterprise/coderd/aibridge.go b/enterprise/coderd/aibridge.go index dab93d8992a79..bdd2a99166910 100644 --- a/enterprise/coderd/aibridge.go +++ b/enterprise/coderd/aibridge.go @@ -36,7 +36,7 @@ const ( // @Param after_id query string false "Cursor pagination after ID (cannot be used with offset)" // @Param offset query int false "Offset pagination (cannot be used with after_id)" // @Success 200 {object} codersdk.AIBridgeListInterceptionsResponse -// @Router /api/experimental/aibridge/interceptions [get] +// @Router /aibridge/interceptions [get] func (api *API) aiBridgeListInterceptions(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index abaf82dbe85f8..17e5df56fb65d 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -27,7 +27,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -37,10 +36,10 @@ func TestAIBridgeListInterceptions(t *testing.T) { Features: license.Features{}, }, }) - experimentalClient := codersdk.NewExperimentalClient(client) ctx := testutil.Context(t, testutil.WaitLong) - _, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) + //nolint:gocritic // Owner role is irrelevant here. + _, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) var sdkErr *codersdk.Error require.ErrorAs(t, err, &sdkErr) require.Equal(t, http.StatusForbidden, sdkErr.StatusCode()) @@ -50,7 +49,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run("EmptyDB", func(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -61,9 +59,9 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }, }) - experimentalClient := codersdk.NewExperimentalClient(client) ctx := testutil.Context(t, testutil.WaitLong) - res, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) + //nolint:gocritic // Owner role is irrelevant here. + res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) require.NoError(t, err) require.Empty(t, res.Results) }) @@ -71,7 +69,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run("OK", func(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -82,7 +79,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }, }) - experimentalClient := codersdk.NewExperimentalClient(client) ctx := testutil.Context(t, testutil.WaitLong) user1, err := client.User(ctx, codersdk.Me) @@ -143,7 +139,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { i1SDK := db2sdk.AIBridgeInterception(i1, user1Visible, []database.AIBridgeTokenUsage{i1tok2, i1tok1}, []database.AIBridgeUserPrompt{i1up2, i1up1}, []database.AIBridgeToolUsage{i1tool2, i1tool1}) i2SDK := db2sdk.AIBridgeInterception(i2, user2Visible, nil, nil, nil) - res, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) + res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) require.NoError(t, err) require.Len(t, res.Results, 2) require.Equal(t, i2SDK.ID, res.Results[0].ID) @@ -183,7 +179,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -194,7 +189,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }, }) - experimentalClient := codersdk.NewExperimentalClient(client) ctx := testutil.Context(t, testutil.WaitLong) allInterceptionIDs := make([]uuid.UUID, 0, 20) @@ -225,7 +219,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { } // Try to fetch with an invalid limit. - res, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ + res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ Pagination: codersdk.Pagination{ Limit: 1001, }, @@ -236,7 +230,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { require.Empty(t, res.Results) // Try to fetch with both after_id and offset pagination. - res, err = experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ + res, err = client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ Pagination: codersdk.Pagination{ AfterID: allInterceptionIDs[0], Offset: 1, @@ -269,7 +263,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { } else { pagination.Offset = len(interceptionIDs) } - res, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ + res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ Pagination: pagination, }) require.NoError(t, err) @@ -299,7 +293,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run("Authorized", func(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} adminClient, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -310,11 +303,9 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }, }) - adminExperimentalClient := codersdk.NewExperimentalClient(adminClient) ctx := testutil.Context(t, testutil.WaitLong) secondUserClient, secondUser := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) - secondUserExperimentalClient := codersdk.NewExperimentalClient(secondUserClient) now := dbtime.Now() i1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ @@ -327,7 +318,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, &now) // Admin can see all interceptions. - res, err := adminExperimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) + res, err := adminClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) require.NoError(t, err) require.EqualValues(t, 2, res.Count) require.Len(t, res.Results, 2) @@ -335,7 +326,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { require.Equal(t, i2.ID, res.Results[1].ID) // Second user can only see their own interceptions. - res, err = secondUserExperimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) + res, err = secondUserClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{}) require.NoError(t, err) require.EqualValues(t, 1, res.Count) require.Len(t, res.Results, 1) @@ -345,7 +336,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run("Filter", func(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -356,7 +346,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }, }) - experimentalClient := codersdk.NewExperimentalClient(client) ctx := testutil.Context(t, testutil.WaitLong) user1, err := client.User(ctx, codersdk.Me) @@ -506,7 +495,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - res, err := experimentalClient.AIBridgeListInterceptions(ctx, tc.filter) + res, err := client.AIBridgeListInterceptions(ctx, tc.filter) require.NoError(t, err) require.EqualValues(t, len(tc.want), res.Count) // We just compare UUID strings for the sake of this test. @@ -526,7 +515,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run("FilterErrors", func(t *testing.T) { t.Parallel() dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{string(codersdk.ExperimentAIBridge)} client, _ := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ DeploymentValues: dv, @@ -537,7 +525,6 @@ func TestAIBridgeListInterceptions(t *testing.T) { }, }, }) - experimentalClient := codersdk.NewExperimentalClient(client) // No need to insert any test data, we're just testing the filter // errors. @@ -594,7 +581,7 @@ func TestAIBridgeListInterceptions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - res, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ + res, err := client.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{ FilterQuery: tc.q, }) var sdkErr *codersdk.Error diff --git a/enterprise/coderd/aibridged.go b/enterprise/coderd/aibridged.go index bf991103b1f52..285575df33862 100644 --- a/enterprise/coderd/aibridged.go +++ b/enterprise/coderd/aibridged.go @@ -14,9 +14,9 @@ import ( "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/codersdk/drpcsdk" - "github.com/coder/coder/v2/enterprise/x/aibridged" - aibridgedproto "github.com/coder/coder/v2/enterprise/x/aibridged/proto" - "github.com/coder/coder/v2/enterprise/x/aibridgedserver" + "github.com/coder/coder/v2/enterprise/aibridged" + aibridgedproto "github.com/coder/coder/v2/enterprise/aibridged/proto" + "github.com/coder/coder/v2/enterprise/aibridgedserver" ) // RegisterInMemoryAIBridgedHTTPHandler mounts [aibridged.Server]'s HTTP router onto diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 7666e8f957fc2..a4adb0479b96b 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -226,12 +226,9 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { return api.refreshEntitlements(ctx) } - api.AGPL.ExperimentalHandler.Group(func(r chi.Router) { + api.AGPL.APIHandler.Group(func(r chi.Router) { r.Route("/aibridge", func(r chi.Router) { - r.Use( - api.RequireFeatureMW(codersdk.FeatureAIBridge), - httpmw.RequireExperimentWithDevBypass(api.AGPL.Experiments, codersdk.ExperimentAIBridge), - ) + r.Use(api.RequireFeatureMW(codersdk.FeatureAIBridge)) r.Group(func(r chi.Router) { r.Use(apiKeyMiddleware) r.Get("/interceptions", api.aiBridgeListInterceptions) @@ -246,7 +243,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { }) return } - http.StripPrefix("/api/experimental/aibridge", api.aibridgedHandler).ServeHTTP(rw, r) + http.StripPrefix("/api/v2/aibridge", api.aibridgedHandler).ServeHTTP(rw, r) }) }) }) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d671cc2bb117e..b964cf4d05f28 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1894,7 +1894,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning"; // From codersdk/deployment.go export type Experiment = - | "aibridge" | "auto-fill-parameters" | "example" | "mcp-server-http" @@ -1905,7 +1904,6 @@ export type Experiment = | "workspace-usage"; export const Experiments: Experiment[] = [ - "aibridge", "auto-fill-parameters", "example", "mcp-server-http", From d0a2e6d603c18deccd0c8ba8dcd318e12ad6097a Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 29 Oct 2025 21:08:26 +0000 Subject: [PATCH 02/12] fix: ensure lifecycle executor has sufficient task permissions (#20539) (#20560) We recently made a change to the `wsbuilder` to handle task related logic. Our test coverage for the lifecycle executor didn't handle this scenario and so we missed that it had insufficient permissions. This PR adds `Update` and `Read` permissions for `Task`s in the lifecycle executor, as well as an autostart/autostop test tailored to task workspaces to verify the change. --- This is cherry picked from https://github.com/coder/coder/commit/06dbadab11760fe5fbf88c5bfcac2c48e11f7862 https://github.com/coder/coder/pull/20539 --- coderd/autobuild/lifecycle_executor_test.go | 172 ++++++++++++++++++++ coderd/database/dbauthz/dbauthz.go | 1 + 2 files changed, 173 insertions(+) diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 263a9e7e13c77..466c8c40525e1 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -1764,3 +1764,175 @@ func TestExecutorAutostartSkipsWhenNoProvisionersAvailable(t *testing.T) { assert.Len(t, stats.Transitions, 1, "should create builds when provisioners are available") } + +func TestExecutorTaskWorkspace(t *testing.T) { + t.Parallel() + + createTaskTemplate := func(t *testing.T, client *codersdk.Client, orgID uuid.UUID, ctx context.Context, defaultTTL time.Duration) codersdk.Template { + t.Helper() + + taskAppID := uuid.New() + version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{HasAiTasks: true}, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Agents: []*proto.Agent{ + { + Id: uuid.NewString(), + Name: "dev", + Auth: &proto.Agent_Token{ + Token: uuid.NewString(), + }, + Apps: []*proto.App{ + { + Id: taskAppID.String(), + Slug: "task-app", + }, + }, + }, + }, + }, + }, + AiTasks: []*proto.AITask{ + { + AppId: taskAppID.String(), + }, + }, + }, + }, + }, + }, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, orgID, version.ID) + + if defaultTTL > 0 { + _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + DefaultTTLMillis: defaultTTL.Milliseconds(), + }) + require.NoError(t, err) + } + + return template + } + + createTaskWorkspace := func(t *testing.T, client *codersdk.Client, template codersdk.Template, ctx context.Context, input string) codersdk.Workspace { + t.Helper() + + exp := codersdk.NewExperimentalClient(client) + task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Input: input, + }) + require.NoError(t, err) + require.True(t, task.WorkspaceID.Valid, "task should have a workspace") + + workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + return workspace + } + + t.Run("Autostart", func(t *testing.T) { + t.Parallel() + + var ( + ctx = testutil.Context(t, testutil.WaitShort) + sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *") + tickCh = make(chan time.Time) + statsCh = make(chan autobuild.Stats) + client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + }) + admin = coderdtest.CreateFirstUser(t, client) + ) + + // Given: A task workspace + template := createTaskTemplate(t, client, admin.OrganizationID, ctx, 0) + workspace := createTaskWorkspace(t, client, template, ctx, "test task for autostart") + + // Given: The task workspace has an autostart schedule + err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{ + Schedule: ptr.Ref(sched.String()), + }) + require.NoError(t, err) + + // Given: That the workspace is in a stopped state. + workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop) + + p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{}) + require.NoError(t, err) + + // When: the autobuild executor ticks after the scheduled time + go func() { + tickTime := sched.Next(workspace.LatestBuild.CreatedAt) + coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) + tickCh <- tickTime + close(tickCh) + }() + + // Then: We expect to see a start transition + stats := <-statsCh + require.Len(t, stats.Transitions, 1, "lifecycle executor should transition the task workspace") + assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions") + assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID], "should autostart the workspace") + require.Empty(t, stats.Errors, "should have no errors when managing task workspaces") + }) + + t.Run("Autostop", func(t *testing.T) { + t.Parallel() + + var ( + ctx = testutil.Context(t, testutil.WaitShort) + tickCh = make(chan time.Time) + statsCh = make(chan autobuild.Stats) + client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + }) + admin = coderdtest.CreateFirstUser(t, client) + ) + + // Given: A task workspace with an 8 hour deadline + template := createTaskTemplate(t, client, admin.OrganizationID, ctx, 8*time.Hour) + workspace := createTaskWorkspace(t, client, template, ctx, "test task for autostop") + + // Given: The workspace is currently running + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition) + require.NotZero(t, workspace.LatestBuild.Deadline, "workspace should have a deadline for autostop") + + p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{}) + require.NoError(t, err) + + // When: the autobuild executor ticks after the deadline + go func() { + tickTime := workspace.LatestBuild.Deadline.Time.Add(time.Minute) + coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) + tickCh <- tickTime + close(tickCh) + }() + + // Then: We expect to see a stop transition + stats := <-statsCh + require.Len(t, stats.Transitions, 1, "lifecycle executor should transition the task workspace") + assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions") + assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID], "should autostop the workspace") + require.Empty(t, stats.Errors, "should have no errors when managing task workspaces") + }) +} diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 01d3c4f26ae85..81873efb72d2d 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -254,6 +254,7 @@ var ( rbac.ResourceFile.Type: {policy.ActionRead}, // Required to read terraform files rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead}, rbac.ResourceSystem.Type: {policy.WildcardSymbol}, + rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, rbac.ResourceUser.Type: {policy.ActionRead}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, From ba14acf4e8b955fefb0933f6a3a2164c7b2c7a52 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 29 Oct 2025 21:29:54 +0000 Subject: [PATCH 03/12] fix(site): fix disappearing preset selector when switching task template (#20514) (#20564) Ensure we set `selectedPresetId` to `undefined` when we change `selectedTemplateId` to ensure we don't end up breaking the ` setSelectedTemplateId(value)} + onValueChange={(value) => { + setSelectedTemplateId(value); + if (value !== selectedTemplateId) { + setSelectedPresetId(undefined); + } + }} defaultValue={templates[0].id} required > From 0b5542f9338e1569a11c78a4c2ff3c45ba3ce605 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 29 Oct 2025 21:45:43 +0000 Subject: [PATCH 04/12] fix: update task link AppStatus using task_id (#20543) (#20551) Fixes https://github.com/coder/coder/issues/20515 Alternative to https://github.com/coder/coder/pull/20519 Adds `task_id` to `workspaces_expanded` view and updates the "View Task" link in `AppStatuses` component. NOTE: this contains a migration (cherry picked from commit 1ebc2176248c4a77e1a3743938a3b9085a4e750e) --- cli/testdata/coder_list_--output_json.golden | 3 +- coderd/aitasks_test.go | 12 ++++++ coderd/apidoc/docs.go | 8 ++++ coderd/apidoc/swagger.json | 8 ++++ coderd/database/dump.sql | 8 ++-- ...00393_workspaces_expanded_task_id.down.sql | 39 +++++++++++++++++ .../000393_workspaces_expanded_task_id.up.sql | 42 +++++++++++++++++++ coderd/database/modelqueries.go | 1 + coderd/database/models.go | 1 + coderd/database/queries.sql.go | 26 ++++++++---- coderd/database/queries/workspaces.sql | 1 + coderd/workspaces.go | 1 + codersdk/workspaces.go | 2 + docs/reference/api/schemas.md | 9 ++++ docs/reference/api/workspaces.md | 24 +++++++++++ site/src/api/typesGenerated.ts | 4 ++ .../WorkspacePage/AppStatuses.stories.tsx | 21 +++++++++- site/src/pages/WorkspacePage/AppStatuses.tsx | 16 ++++--- .../pages/WorkspacePage/Workspace.stories.tsx | 4 +- site/src/testHelpers/entities.ts | 5 +++ 20 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 coderd/database/migrations/000393_workspaces_expanded_task_id.down.sql create mode 100644 coderd/database/migrations/000393_workspaces_expanded_task_id.up.sql diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 66afcf563dfbd..8da57536338f8 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -90,6 +90,7 @@ "allow_renames": false, "favorite": false, "next_start_at": "====[timestamp]=====", - "is_prebuild": false + "is_prebuild": false, + "task_id": null } ] diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 80af3e993e97a..d3b5e240d8301 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -259,6 +259,9 @@ func TestTasks(t *testing.T) { // Wait for the workspace to be built. workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID) require.NoError(t, err) + if assert.True(t, workspace.TaskID.Valid, "task id should be set on workspace") { + assert.Equal(t, task.ID, workspace.TaskID.UUID, "workspace task id should match") + } coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) // List tasks via experimental API and verify the prompt and status mapping. @@ -297,6 +300,9 @@ func TestTasks(t *testing.T) { // Get the workspace and wait for it to be ready. ws, err := client.Workspace(ctx, task.WorkspaceID.UUID) require.NoError(t, err) + if assert.True(t, ws.TaskID.Valid, "task id should be set on workspace") { + assert.Equal(t, task.ID, ws.TaskID.UUID, "workspace task id should match") + } coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) ws = coderdtest.MustWorkspace(t, client, task.WorkspaceID.UUID) // Assert invariant: the workspace has exactly one resource with one agent with one app. @@ -371,6 +377,9 @@ func TestTasks(t *testing.T) { require.True(t, task.WorkspaceID.Valid, "task should have a workspace ID") ws, err := client.Workspace(ctx, task.WorkspaceID.UUID) require.NoError(t, err) + if assert.True(t, ws.TaskID.Valid, "task id should be set on workspace") { + assert.Equal(t, task.ID, ws.TaskID.UUID, "workspace task id should match") + } coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) err = exp.DeleteTask(ctx, "me", task.ID) @@ -417,6 +426,9 @@ func TestTasks(t *testing.T) { coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) ws := coderdtest.CreateWorkspace(t, client, template.ID) + if assert.False(t, ws.TaskID.Valid, "task id should not be set on non-task workspace") { + assert.Zero(t, ws.TaskID, "non-task workspace task id should be empty") + } coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) exp := codersdk.NewExperimentalClient(client) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 344a0299f8405..e459b94b3fdaf 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -19712,6 +19712,14 @@ const docTemplate = `{ "description": "OwnerName is the username of the owner of the workspace.", "type": "string" }, + "task_id": { + "description": "TaskID, if set, indicates that the workspace is relevant to the given codersdk.Task.", + "allOf": [ + { + "$ref": "#/definitions/uuid.NullUUID" + } + ] + }, "template_active_version_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index b472f2f4ef53f..8eef9dfb8e0d7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -18098,6 +18098,14 @@ "description": "OwnerName is the username of the owner of the workspace.", "type": "string" }, + "task_id": { + "description": "TaskID, if set, indicates that the workspace is relevant to the given codersdk.Task.", + "allOf": [ + { + "$ref": "#/definitions/uuid.NullUUID" + } + ] + }, "template_active_version_id": { "type": "string", "format": "uuid" diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 837c657402bf2..8790bd27df693 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2922,11 +2922,13 @@ CREATE VIEW workspaces_expanded AS templates.name AS template_name, templates.display_name AS template_display_name, templates.icon AS template_icon, - templates.description AS template_description - FROM (((workspaces + templates.description AS template_description, + tasks.id AS task_id + FROM ((((workspaces JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) JOIN organizations ON ((workspaces.organization_id = organizations.id))) - JOIN templates ON ((workspaces.template_id = templates.id))); + JOIN templates ON ((workspaces.template_id = templates.id))) + LEFT JOIN tasks ON ((workspaces.id = tasks.workspace_id))); COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/migrations/000393_workspaces_expanded_task_id.down.sql b/coderd/database/migrations/000393_workspaces_expanded_task_id.down.sql new file mode 100644 index 0000000000000..ed30e6a0f64f3 --- /dev/null +++ b/coderd/database/migrations/000393_workspaces_expanded_task_id.down.sql @@ -0,0 +1,39 @@ +DROP VIEW workspaces_expanded; + +-- Recreate the view from 000354_workspace_acl.up.sql +CREATE VIEW workspaces_expanded AS + SELECT workspaces.id, + workspaces.created_at, + workspaces.updated_at, + workspaces.owner_id, + workspaces.organization_id, + workspaces.template_id, + workspaces.deleted, + workspaces.name, + workspaces.autostart_schedule, + workspaces.ttl, + workspaces.last_used_at, + workspaces.dormant_at, + workspaces.deleting_at, + workspaces.automatic_updates, + workspaces.favorite, + workspaces.next_start_at, + workspaces.group_acl, + workspaces.user_acl, + visible_users.avatar_url AS owner_avatar_url, + visible_users.username AS owner_username, + visible_users.name AS owner_name, + organizations.name AS organization_name, + organizations.display_name AS organization_display_name, + organizations.icon AS organization_icon, + organizations.description AS organization_description, + templates.name AS template_name, + templates.display_name AS template_display_name, + templates.icon AS template_icon, + templates.description AS template_description + FROM (((workspaces + JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) + JOIN organizations ON ((workspaces.organization_id = organizations.id))) + JOIN templates ON ((workspaces.template_id = templates.id))); + +COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/migrations/000393_workspaces_expanded_task_id.up.sql b/coderd/database/migrations/000393_workspaces_expanded_task_id.up.sql new file mode 100644 index 0000000000000..f01354e65bd50 --- /dev/null +++ b/coderd/database/migrations/000393_workspaces_expanded_task_id.up.sql @@ -0,0 +1,42 @@ +DROP VIEW workspaces_expanded; + +-- Add nullable task_id to workspaces_expanded view +CREATE VIEW workspaces_expanded AS + SELECT workspaces.id, + workspaces.created_at, + workspaces.updated_at, + workspaces.owner_id, + workspaces.organization_id, + workspaces.template_id, + workspaces.deleted, + workspaces.name, + workspaces.autostart_schedule, + workspaces.ttl, + workspaces.last_used_at, + workspaces.dormant_at, + workspaces.deleting_at, + workspaces.automatic_updates, + workspaces.favorite, + workspaces.next_start_at, + workspaces.group_acl, + workspaces.user_acl, + visible_users.avatar_url AS owner_avatar_url, + visible_users.username AS owner_username, + visible_users.name AS owner_name, + organizations.name AS organization_name, + organizations.display_name AS organization_display_name, + organizations.icon AS organization_icon, + organizations.description AS organization_description, + templates.name AS template_name, + templates.display_name AS template_display_name, + templates.icon AS template_icon, + templates.description AS template_description, + tasks.id AS task_id + FROM ((((workspaces + JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) + JOIN organizations ON ((workspaces.organization_id = organizations.id))) + JOIN templates ON ((workspaces.template_id = templates.id))) + LEFT JOIN tasks ON ((workspaces.id = tasks.workspace_id))); + +COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; + diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index c9c7879627684..f9b058a40986e 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -321,6 +321,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, &i.TemplateVersionID, &i.TemplateVersionName, &i.LatestBuildCompletedAt, diff --git a/coderd/database/models.go b/coderd/database/models.go index e55f3a553721b..ade3348ba3c69 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -4663,6 +4663,7 @@ type Workspace struct { TemplateDisplayName string `db:"template_display_name" json:"template_display_name"` TemplateIcon string `db:"template_icon" json:"template_icon"` TemplateDescription string `db:"template_description" json:"template_description"` + TaskID uuid.NullUUID `db:"task_id" json:"task_id"` } type WorkspaceAgent struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2005f98347c6c..0edb4fc709a1a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -21826,7 +21826,7 @@ func (q *sqlQuerier) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (Get const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id FROM workspaces_expanded as workspaces WHERE @@ -21887,13 +21887,14 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, ) return i, err } const getWorkspaceByID = `-- name: GetWorkspaceByID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id FROM workspaces_expanded WHERE @@ -21935,13 +21936,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, ) return i, err } const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id FROM workspaces_expanded as workspaces WHERE @@ -21990,13 +21992,14 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, ) return i, err } const getWorkspaceByResourceID = `-- name: GetWorkspaceByResourceID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id FROM workspaces_expanded as workspaces WHERE @@ -22052,13 +22055,14 @@ func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uu &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, ) return i, err } const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id FROM workspaces_expanded as workspaces WHERE @@ -22126,6 +22130,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, ) return i, err } @@ -22175,7 +22180,7 @@ SELECT ), filtered_workspaces AS ( SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, workspaces.task_id, latest_build.template_version_id, latest_build.template_version_name, latest_build.completed_at as latest_build_completed_at, @@ -22466,7 +22471,7 @@ WHERE -- @authorize_filter ), filtered_workspaces_order AS ( SELECT - fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_ai_task, fw.latest_build_has_external_agent + fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_ai_task, fw.latest_build_has_external_agent FROM filtered_workspaces fw ORDER BY @@ -22487,7 +22492,7 @@ WHERE $25 ), filtered_workspaces_order_with_summary AS ( SELECT - fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_ai_task, fwo.latest_build_has_external_agent + fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_ai_task, fwo.latest_build_has_external_agent FROM filtered_workspaces_order fwo -- Return a technical summary row with total count of workspaces. @@ -22523,6 +22528,7 @@ WHERE '', -- template_display_name '', -- template_icon '', -- template_description + '00000000-0000-0000-0000-000000000000'::uuid, -- task_id -- Extra columns added to ` + "`" + `filtered_workspaces` + "`" + ` '00000000-0000-0000-0000-000000000000'::uuid, -- template_version_id '', -- template_version_name @@ -22542,7 +22548,7 @@ WHERE filtered_workspaces ) SELECT - fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_ai_task, fwos.latest_build_has_external_agent, + fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_ai_task, fwos.latest_build_has_external_agent, tc.count FROM filtered_workspaces_order_with_summary fwos @@ -22610,6 +22616,7 @@ type GetWorkspacesRow struct { TemplateDisplayName string `db:"template_display_name" json:"template_display_name"` TemplateIcon string `db:"template_icon" json:"template_icon"` TemplateDescription string `db:"template_description" json:"template_description"` + TaskID uuid.NullUUID `db:"task_id" json:"task_id"` TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"` LatestBuildCompletedAt sql.NullTime `db:"latest_build_completed_at" json:"latest_build_completed_at"` @@ -22692,6 +22699,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, + &i.TaskID, &i.TemplateVersionID, &i.TemplateVersionName, &i.LatestBuildCompletedAt, diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 8ccc69b9a813c..d48285bb7de9c 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -457,6 +457,7 @@ WHERE '', -- template_display_name '', -- template_icon '', -- template_description + '00000000-0000-0000-0000-000000000000'::uuid, -- task_id -- Extra columns added to `filtered_workspaces` '00000000-0000-0000-0000-000000000000'::uuid, -- template_version_id '', -- template_version_name diff --git a/coderd/workspaces.go b/coderd/workspaces.go index e8b7ff51530c3..3519442c3e6bf 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2654,6 +2654,7 @@ func convertWorkspace( Favorite: requesterFavorite, NextStartAt: nextStartAt, IsPrebuild: workspace.IsPrebuild(), + TaskID: workspace.TaskID, }, nil } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index f190d58be6bfb..709c9257c8350 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -72,6 +72,8 @@ type Workspace struct { // Once a prebuilt workspace is claimed by a user, it transitions to a regular workspace, // and IsPrebuild returns false. IsPrebuild bool `json:"is_prebuild"` + // TaskID, if set, indicates that the workspace is relevant to the given codersdk.Task. + TaskID uuid.NullUUID `json:"task_id,omitempty"` } func (w Workspace) FullName() string { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 037c9cfa109bb..4317324f002be 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -10184,6 +10184,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", @@ -10222,6 +10226,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `owner_avatar_url` | string | false | | | | `owner_id` | string | false | | | | `owner_name` | string | false | | Owner name is the username of the owner of the workspace. | +| `task_id` | [uuid.NullUUID](#uuidnulluuid) | false | | Task ID if set, indicates that the workspace is relevant to the given codersdk.Task. | | `template_active_version_id` | string | false | | | | `template_allow_user_cancel_workspace_jobs` | boolean | false | | | | `template_display_name` | string | false | | | @@ -12178,6 +12183,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 91ab23f9260e9..3e52d9e0a2d60 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -297,6 +297,10 @@ of the template will be used. "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", @@ -589,6 +593,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", @@ -906,6 +914,10 @@ of the template will be used. "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", @@ -1184,6 +1196,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", @@ -1477,6 +1493,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", @@ -2029,6 +2049,10 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "owner_avatar_url": "string", "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05", "owner_name": "string", + "task_id": { + "uuid": "string", + "valid": true + }, "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c", "template_allow_user_cancel_workspace_jobs": true, "template_display_name": "string", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b964cf4d05f28..6d703cbcfe2d6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -5875,6 +5875,10 @@ export interface Workspace { * and IsPrebuild returns false. */ readonly is_prebuild: boolean; + /** + * TaskID, if set, indicates that the workspace is relevant to the given codersdk.Task. + */ + readonly task_id?: string; } // From codersdk/workspaces.go diff --git a/site/src/pages/WorkspacePage/AppStatuses.stories.tsx b/site/src/pages/WorkspacePage/AppStatuses.stories.tsx index 2e8324aef2e0b..d27e64ae7096f 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.stories.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.stories.tsx @@ -1,5 +1,6 @@ import { createTimestamp, + MockTaskWorkspace, MockWorkspace, MockWorkspaceAgent, MockWorkspaceApp, @@ -18,7 +19,7 @@ const meta: Meta = { args: { referenceDate: new Date("2024-03-26T15:15:00Z"), agent: mockAgent(MockWorkspaceAppStatuses), - workspace: MockWorkspace, + workspace: MockTaskWorkspace, }, decorators: [withProxyProvider()], }; @@ -148,6 +149,24 @@ export const MultipleStatuses: Story = { }, }; +export const NoTaskWorkspace: Story = { + args: { + agent: mockAgent([ + { + ...MockWorkspaceAppStatus, + id: "status-9", + icon: "", + message: "status updated via curl", + created_at: createTimestamp(5, 15), + uri: "", + state: "complete" as const, + }, + ...MockWorkspaceAppStatuses, + ]), + workspace: MockWorkspace, + }, +}; + function mockAgent(statuses: WorkspaceAppStatus[]) { return { ...MockWorkspaceAgent, diff --git a/site/src/pages/WorkspacePage/AppStatuses.tsx b/site/src/pages/WorkspacePage/AppStatuses.tsx index 26f239b627101..2f1845db37031 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.tsx @@ -121,12 +121,16 @@ export const AppStatuses: FC = ({ ))} - + {workspace.task_id && ( + + )} diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 5a49e0fa57091..8ae0c1b3095d0 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -112,9 +112,9 @@ export const RunningWithChildAgent: Story = { export const RunningWithAppStatuses: Story = { args: { workspace: { - ...Mocks.MockWorkspace, + ...Mocks.MockTaskWorkspace, latest_build: { - ...Mocks.MockWorkspace.latest_build, + ...Mocks.MockTaskWorkspace.latest_build, resources: [ { ...Mocks.MockWorkspaceResource, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a12d91cf51a8e..b0ce392fa1feb 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5027,6 +5027,11 @@ export const MockTask = { updated_at: "2022-05-17T17:39:01.382927298Z", } satisfies TypesGen.Task; +export const MockTaskWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + task_id: MockTask.id, +}; + export const MockTasks = [ MockTask, { From a7b3efb540830f502c14a2fde82cdfe681371810 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Thu, 30 Oct 2025 10:38:14 +0000 Subject: [PATCH 05/12] feat: delete pending canceled prebuilds (#20499) (#20554) Related to PR: https://github.com/coder/coder/pull/20499 (cherry picked from commit c3e3bb58f2821c94b31bbe8bb750849514b49a5b) --- coderd/database/dbauthz/dbauthz.go | 4 +- coderd/database/dbauthz/dbauthz_test.go | 9 +- coderd/database/dbmetrics/querymetrics.go | 2 +- coderd/database/dbmock/dbmock.go | 4 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 38 +++-- coderd/database/queries/prebuilds.sql | 16 +- enterprise/coderd/prebuilds/reconcile.go | 144 +++++++++++++----- enterprise/coderd/prebuilds/reconcile_test.go | 95 ++++++++---- 9 files changed, 219 insertions(+), 95 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 81873efb72d2d..f169f7eeb13c1 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -4934,10 +4934,10 @@ func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg databas return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID) } -func (q *querier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]uuid.UUID, error) { +func (q *querier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]database.UpdatePrebuildProvisionerJobWithCancelRow, error) { // Prebuild operation for canceling pending prebuild jobs from non-active template versions if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourcePrebuiltWorkspace); err != nil { - return []uuid.UUID{}, err + return []database.UpdatePrebuildProvisionerJobWithCancelRow{}, err } return q.db.UpdatePrebuildProvisionerJobWithCancel(ctx, arg) } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 8cf622a4347f3..63226271f7fa0 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -646,10 +646,13 @@ func (s *MethodTestSuite) TestProvisionerJob() { PresetID: uuid.NullUUID{UUID: uuid.New(), Valid: true}, Now: dbtime.Now(), } - jobIDs := []uuid.UUID{uuid.New(), uuid.New()} + canceledJobs := []database.UpdatePrebuildProvisionerJobWithCancelRow{ + {ID: uuid.New(), WorkspaceID: uuid.New(), TemplateID: uuid.New(), TemplateVersionPresetID: uuid.NullUUID{UUID: uuid.New(), Valid: true}}, + {ID: uuid.New(), WorkspaceID: uuid.New(), TemplateID: uuid.New(), TemplateVersionPresetID: uuid.NullUUID{UUID: uuid.New(), Valid: true}}, + } - dbm.EXPECT().UpdatePrebuildProvisionerJobWithCancel(gomock.Any(), arg).Return(jobIDs, nil).AnyTimes() - check.Args(arg).Asserts(rbac.ResourcePrebuiltWorkspace, policy.ActionUpdate).Returns(jobIDs) + dbm.EXPECT().UpdatePrebuildProvisionerJobWithCancel(gomock.Any(), arg).Return(canceledJobs, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourcePrebuiltWorkspace, policy.ActionUpdate).Returns(canceledJobs) })) s.Run("GetProvisionerJobsByIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { org := testutil.Fake(s.T(), faker, database.Organization{}) diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 1bd8fda62470a..1b36a3fa987f2 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -3042,7 +3042,7 @@ func (m queryMetricsStore) UpdateOrganizationDeletedByID(ctx context.Context, ar return r0 } -func (m queryMetricsStore) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]uuid.UUID, error) { +func (m queryMetricsStore) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]database.UpdatePrebuildProvisionerJobWithCancelRow, error) { start := time.Now() r0, r1 := m.s.UpdatePrebuildProvisionerJobWithCancel(ctx, arg) m.queryLatencies.WithLabelValues("UpdatePrebuildProvisionerJobWithCancel").Observe(time.Since(start).Seconds()) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 1983092aa53f0..0ecd0191cbce1 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -6540,10 +6540,10 @@ func (mr *MockStoreMockRecorder) UpdateOrganizationDeletedByID(ctx, arg any) *go } // UpdatePrebuildProvisionerJobWithCancel mocks base method. -func (m *MockStore) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]uuid.UUID, error) { +func (m *MockStore) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg database.UpdatePrebuildProvisionerJobWithCancelParams) ([]database.UpdatePrebuildProvisionerJobWithCancelRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdatePrebuildProvisionerJobWithCancel", ctx, arg) - ret0, _ := ret[0].([]uuid.UUID) + ret0, _ := ret[0].([]database.UpdatePrebuildProvisionerJobWithCancelRow) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 2b96823028f61..2aa1cb8650051 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -667,7 +667,7 @@ type sqlcQuerier interface { // Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an // inactive template version. // This is an optimization to clean up stale pending jobs. - UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]uuid.UUID, error) + UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0edb4fc709a1a..9a9c7d2629dd0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8687,12 +8687,8 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa } const updatePrebuildProvisionerJobWithCancel = `-- name: UpdatePrebuildProvisionerJobWithCancel :many -UPDATE provisioner_jobs -SET - canceled_at = $1::timestamptz, - completed_at = $1::timestamptz -WHERE id IN ( - SELECT pj.id +WITH jobs_to_cancel AS ( + SELECT pj.id, w.id AS workspace_id, w.template_id, wpb.template_version_preset_id FROM provisioner_jobs pj INNER JOIN workspace_prebuild_builds wpb ON wpb.job_id = pj.id INNER JOIN workspaces w ON w.id = wpb.workspace_id @@ -8711,7 +8707,13 @@ WHERE id IN ( AND pj.canceled_at IS NULL AND pj.completed_at IS NULL ) -RETURNING id +UPDATE provisioner_jobs +SET + canceled_at = $1::timestamptz, + completed_at = $1::timestamptz +FROM jobs_to_cancel +WHERE provisioner_jobs.id = jobs_to_cancel.id +RETURNING jobs_to_cancel.id, jobs_to_cancel.workspace_id, jobs_to_cancel.template_id, jobs_to_cancel.template_version_preset_id ` type UpdatePrebuildProvisionerJobWithCancelParams struct { @@ -8719,22 +8721,34 @@ type UpdatePrebuildProvisionerJobWithCancelParams struct { PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"` } +type UpdatePrebuildProvisionerJobWithCancelRow struct { + ID uuid.UUID `db:"id" json:"id"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"` +} + // Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an // inactive template version. // This is an optimization to clean up stale pending jobs. -func (q *sqlQuerier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]uuid.UUID, error) { +func (q *sqlQuerier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error) { rows, err := q.db.QueryContext(ctx, updatePrebuildProvisionerJobWithCancel, arg.Now, arg.PresetID) if err != nil { return nil, err } defer rows.Close() - var items []uuid.UUID + var items []UpdatePrebuildProvisionerJobWithCancelRow for rows.Next() { - var id uuid.UUID - if err := rows.Scan(&id); err != nil { + var i UpdatePrebuildProvisionerJobWithCancelRow + if err := rows.Scan( + &i.ID, + &i.WorkspaceID, + &i.TemplateID, + &i.TemplateVersionPresetID, + ); err != nil { return nil, err } - items = append(items, id) + items = append(items, i) } if err := rows.Close(); err != nil { return nil, err diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index 6c5520c9da7e1..7c060971efba5 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -300,12 +300,8 @@ GROUP BY wpb.template_version_preset_id; -- Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an -- inactive template version. -- This is an optimization to clean up stale pending jobs. -UPDATE provisioner_jobs -SET - canceled_at = @now::timestamptz, - completed_at = @now::timestamptz -WHERE id IN ( - SELECT pj.id +WITH jobs_to_cancel AS ( + SELECT pj.id, w.id AS workspace_id, w.template_id, wpb.template_version_preset_id FROM provisioner_jobs pj INNER JOIN workspace_prebuild_builds wpb ON wpb.job_id = pj.id INNER JOIN workspaces w ON w.id = wpb.workspace_id @@ -324,4 +320,10 @@ WHERE id IN ( AND pj.canceled_at IS NULL AND pj.completed_at IS NULL ) -RETURNING id; +UPDATE provisioner_jobs +SET + canceled_at = @now::timestamptz, + completed_at = @now::timestamptz +FROM jobs_to_cancel +WHERE provisioner_jobs.id = jobs_to_cancel.id +RETURNING jobs_to_cancel.id, jobs_to_cancel.workspace_id, jobs_to_cancel.template_id, jobs_to_cancel.template_version_preset_id; diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index ceb16061bd7a7..5e5eec68ab382 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -57,6 +57,24 @@ type StoreReconciler struct { var _ prebuilds.ReconciliationOrchestrator = &StoreReconciler{} +type DeprovisionMode int + +const ( + DeprovisionModeNormal DeprovisionMode = iota + DeprovisionModeOrphan +) + +func (d DeprovisionMode) String() string { + switch d { + case DeprovisionModeOrphan: + return "orphan" + case DeprovisionModeNormal: + return "normal" + default: + return "unknown" + } +} + func NewStoreReconciler(store database.Store, ps pubsub.Pubsub, fileCache *files.Cache, @@ -642,34 +660,7 @@ func (c *StoreReconciler) executeReconciliationAction(ctx context.Context, logge return multiErr.ErrorOrNil() case prebuilds.ActionTypeCancelPending: - // Cancel pending prebuild jobs from non-active template versions to avoid - // provisioning obsolete workspaces that would immediately be deprovisioned. - // This uses a criteria-based update to ensure only jobs that are still pending - // at execution time are canceled, avoiding race conditions where jobs may have - // transitioned to running status between query and update. - canceledJobs, err := c.store.UpdatePrebuildProvisionerJobWithCancel( - ctx, - database.UpdatePrebuildProvisionerJobWithCancelParams{ - Now: c.clock.Now(), - PresetID: uuid.NullUUID{ - UUID: ps.Preset.ID, - Valid: true, - }, - }) - if err != nil { - logger.Error(ctx, "failed to cancel pending prebuild jobs", - slog.F("template_version_id", ps.Preset.TemplateVersionID.String()), - slog.F("preset_id", ps.Preset.ID), - slog.Error(err)) - return err - } - if len(canceledJobs) > 0 { - logger.Info(ctx, "canceled pending prebuild jobs for inactive version", - slog.F("template_version_id", ps.Preset.TemplateVersionID.String()), - slog.F("preset_id", ps.Preset.ID), - slog.F("count", len(canceledJobs))) - } - return nil + return c.cancelAndOrphanDeletePendingPrebuilds(ctx, ps.Preset.TemplateID, ps.Preset.TemplateVersionID, ps.Preset.ID) default: return xerrors.Errorf("unknown action type: %v", action.ActionType) @@ -717,33 +708,100 @@ func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltW c.logger.Info(ctx, "attempting to create prebuild", slog.F("name", name), slog.F("workspace_id", prebuiltWorkspaceID.String()), slog.F("preset_id", presetID.String())) - return c.provision(ctx, db, prebuiltWorkspaceID, template, presetID, database.WorkspaceTransitionStart, workspace) + return c.provision(ctx, db, prebuiltWorkspaceID, template, presetID, database.WorkspaceTransitionStart, workspace, DeprovisionModeNormal) }, &database.TxOptions{ Isolation: sql.LevelRepeatableRead, ReadOnly: false, }) } -func (c *StoreReconciler) deletePrebuiltWorkspace(ctx context.Context, prebuiltWorkspaceID uuid.UUID, templateID uuid.UUID, presetID uuid.UUID) error { +// provisionDelete provisions a delete transition for a prebuilt workspace. +// +// If mode is DeprovisionModeOrphan, the builder will not send Terraform state to the provisioner. +// This allows the workspace to be deleted even when no provisioners are available, and is safe +// when no Terraform resources were actually created (e.g., for pending prebuilds that were canceled +// before provisioning started). +// +// IMPORTANT: This function must be called within a database transaction. It does not create its own transaction. +// The caller is responsible for managing the transaction boundary via db.InTx(). +func (c *StoreReconciler) provisionDelete(ctx context.Context, db database.Store, workspaceID uuid.UUID, templateID uuid.UUID, presetID uuid.UUID, mode DeprovisionMode) error { + workspace, err := db.GetWorkspaceByID(ctx, workspaceID) + if err != nil { + return xerrors.Errorf("get workspace by ID: %w", err) + } + + template, err := db.GetTemplateByID(ctx, templateID) + if err != nil { + return xerrors.Errorf("failed to get template: %w", err) + } + + if workspace.OwnerID != database.PrebuildsSystemUserID { + return xerrors.Errorf("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed") + } + + c.logger.Info(ctx, "attempting to delete prebuild", slog.F("orphan", mode.String()), + slog.F("name", workspace.Name), slog.F("workspace_id", workspaceID.String()), slog.F("preset_id", presetID.String())) + + return c.provision(ctx, db, workspaceID, template, presetID, + database.WorkspaceTransitionDelete, workspace, mode) +} + +// cancelAndOrphanDeletePendingPrebuilds cancels pending prebuild jobs from inactive template versions +// and orphan-deletes their associated workspaces. +// +// The cancel operation uses a criteria-based update to ensure only jobs that are still pending at +// execution time are canceled, avoiding race conditions where jobs may have transitioned to running. +// +// Since these jobs were never processed by a provisioner, no Terraform resources were created, +// making it safe to orphan-delete the workspaces (skipping Terraform destroy). +func (c *StoreReconciler) cancelAndOrphanDeletePendingPrebuilds(ctx context.Context, templateID uuid.UUID, templateVersionID uuid.UUID, presetID uuid.UUID) error { return c.store.InTx(func(db database.Store) error { - workspace, err := db.GetWorkspaceByID(ctx, prebuiltWorkspaceID) + canceledJobs, err := db.UpdatePrebuildProvisionerJobWithCancel( + ctx, + database.UpdatePrebuildProvisionerJobWithCancelParams{ + Now: c.clock.Now(), + PresetID: uuid.NullUUID{ + UUID: presetID, + Valid: true, + }, + }) if err != nil { - return xerrors.Errorf("get workspace by ID: %w", err) + c.logger.Error(ctx, "failed to cancel pending prebuild jobs", + slog.F("template_id", templateID.String()), + slog.F("template_version_id", templateVersionID.String()), + slog.F("preset_id", presetID.String()), + slog.Error(err)) + return err } - template, err := db.GetTemplateByID(ctx, templateID) - if err != nil { - return xerrors.Errorf("failed to get template: %w", err) + if len(canceledJobs) > 0 { + c.logger.Info(ctx, "canceled pending prebuild jobs for inactive version", + slog.F("template_id", templateID.String()), + slog.F("template_version_id", templateVersionID.String()), + slog.F("preset_id", presetID.String()), + slog.F("count", len(canceledJobs))) } - if workspace.OwnerID != database.PrebuildsSystemUserID { - return xerrors.Errorf("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed") + var multiErr multierror.Error + for _, job := range canceledJobs { + err = c.provisionDelete(ctx, db, job.WorkspaceID, job.TemplateID, presetID, DeprovisionModeOrphan) + if err != nil { + c.logger.Error(ctx, "failed to orphan delete canceled prebuild", + slog.F("workspace_id", job.WorkspaceID.String()), slog.Error(err)) + multiErr.Errors = append(multiErr.Errors, err) + } } - c.logger.Info(ctx, "attempting to delete prebuild", - slog.F("workspace_id", prebuiltWorkspaceID.String()), slog.F("preset_id", presetID.String())) + return multiErr.ErrorOrNil() + }, &database.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: false, + }) +} - return c.provision(ctx, db, prebuiltWorkspaceID, template, presetID, database.WorkspaceTransitionDelete, workspace) +func (c *StoreReconciler) deletePrebuiltWorkspace(ctx context.Context, prebuiltWorkspaceID uuid.UUID, templateID uuid.UUID, presetID uuid.UUID) error { + return c.store.InTx(func(db database.Store) error { + return c.provisionDelete(ctx, db, prebuiltWorkspaceID, templateID, presetID, DeprovisionModeNormal) }, &database.TxOptions{ Isolation: sql.LevelRepeatableRead, ReadOnly: false, @@ -758,6 +816,7 @@ func (c *StoreReconciler) provision( presetID uuid.UUID, transition database.WorkspaceTransition, workspace database.Workspace, + mode DeprovisionMode, ) error { tvp, err := db.GetPresetParametersByTemplateVersionID(ctx, template.ActiveVersionID) if err != nil { @@ -795,6 +854,11 @@ func (c *StoreReconciler) provision( builder = builder.RichParameterValues(params) } + // Use orphan mode for deletes when no Terraform resources exist + if transition == database.WorkspaceTransitionDelete && mode == DeprovisionModeOrphan { + builder = builder.Orphan() + } + _, provisionerJob, _, err := builder.Build( ctx, db, diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 33b99145d8e12..4b359ba9df429 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -204,7 +204,10 @@ func TestPrebuildReconciliation(t *testing.T) { templateDeleted: []bool{false}, }, { - name: "never attempt to interfere with active builds", + // TODO(ssncferreira): Investigate why the GetRunningPrebuiltWorkspaces query is returning 0 rows. + // When a template version is inactive (templateVersionActive = false), any prebuilds in the + // database.ProvisionerJobStatusRunning state should be deleted. + name: "never attempt to interfere with prebuilds from an active template version", // The workspace builder does not allow scheduling a new build if there is already a build // pending, running, or canceling. As such, we should never attempt to start, stop or delete // such prebuilds. Rather, we should wait for the existing build to complete and reconcile @@ -215,7 +218,7 @@ func TestPrebuildReconciliation(t *testing.T) { database.ProvisionerJobStatusRunning, database.ProvisionerJobStatusCanceling, }, - templateVersionActive: []bool{true, false}, + templateVersionActive: []bool{true}, shouldDeleteOldPrebuild: ptr.To(false), templateDeleted: []bool{false}, }, @@ -2121,16 +2124,16 @@ func TestCancelPendingPrebuilds(t *testing.T) { }, }).SkipCreateTemplate().Do() - var workspace dbfake.WorkspaceResponse + var pendingWorkspace dbfake.WorkspaceResponse if tt.activeTemplateVersion { // Given: a prebuilt workspace, workspace build and respective provisioner job from an // active template version - workspace = tt.setupBuild(t, db, client, + pendingWorkspace = tt.setupBuild(t, db, client, owner.OrganizationID, templateID, activeTemplateVersion.TemplateVersion.ID, activePresetID) } else { // Given: a prebuilt workspace, workspace build and respective provisioner job from a // non-active template version - workspace = tt.setupBuild(t, db, client, + pendingWorkspace = tt.setupBuild(t, db, client, owner.OrganizationID, templateID, nonActiveTemplateVersion.TemplateVersion.ID, nonActivePresetID) } @@ -2145,15 +2148,28 @@ func TestCancelPendingPrebuilds(t *testing.T) { require.NoError(t, reconciler.ReconcileAll(ctx)) if tt.shouldCancel { - // Then: the prebuild related jobs from non-active version should be canceled - cancelledJob, err := db.GetProvisionerJobByID(ctx, workspace.Build.JobID) + // Then: the pending prebuild job from non-active version should be canceled + cancelledJob, err := db.GetProvisionerJobByID(ctx, pendingWorkspace.Build.JobID) require.NoError(t, err) require.Equal(t, clock.Now().UTC(), cancelledJob.CanceledAt.Time.UTC()) require.Equal(t, clock.Now().UTC(), cancelledJob.CompletedAt.Time.UTC()) require.Equal(t, database.ProvisionerJobStatusCanceled, cancelledJob.JobStatus) + + // Then: the workspace should be deleted + deletedWorkspace, err := db.GetWorkspaceByID(ctx, pendingWorkspace.Workspace.ID) + require.NoError(t, err) + require.True(t, deletedWorkspace.Deleted) + latestBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, deletedWorkspace.ID) + require.NoError(t, err) + require.Equal(t, database.WorkspaceTransitionDelete, latestBuild.Transition) + deleteJob, err := db.GetProvisionerJobByID(ctx, latestBuild.JobID) + require.NoError(t, err) + require.True(t, deleteJob.CompletedAt.Valid) + require.False(t, deleteJob.WorkerID.Valid) + require.Equal(t, database.ProvisionerJobStatusSucceeded, deleteJob.JobStatus) } else { - // Then: the provisioner job should not be canceled - job, err := db.GetProvisionerJobByID(ctx, workspace.Build.JobID) + // Then: the pending prebuild job should not be canceled + job, err := db.GetProvisionerJobByID(ctx, pendingWorkspace.Build.JobID) require.NoError(t, err) if !tt.previouslyCanceled { require.Zero(t, job.CanceledAt.Time.UTC()) @@ -2162,6 +2178,11 @@ func TestCancelPendingPrebuilds(t *testing.T) { if !tt.previouslyCompleted { require.Zero(t, job.CompletedAt.Time.UTC()) } + + // Then: the workspace should not be deleted + workspace, err := db.GetWorkspaceByID(ctx, pendingWorkspace.Workspace.ID) + require.NoError(t, err) + require.False(t, workspace.Deleted) } }) } @@ -2235,25 +2256,45 @@ func TestCancelPendingPrebuilds(t *testing.T) { return prebuilds } - checkIfJobCanceled := func( + checkIfJobCanceledAndDeleted := func( t *testing.T, clock *quartz.Mock, ctx context.Context, db database.Store, - shouldBeCanceled bool, + shouldBeCanceledAndDeleted bool, prebuilds []dbfake.WorkspaceResponse, ) { for _, prebuild := range prebuilds { - job, err := db.GetProvisionerJobByID(ctx, prebuild.Build.JobID) + pendingJob, err := db.GetProvisionerJobByID(ctx, prebuild.Build.JobID) require.NoError(t, err) - if shouldBeCanceled { - require.Equal(t, database.ProvisionerJobStatusCanceled, job.JobStatus) - require.Equal(t, clock.Now().UTC(), job.CanceledAt.Time.UTC()) - require.Equal(t, clock.Now().UTC(), job.CompletedAt.Time.UTC()) + if shouldBeCanceledAndDeleted { + // Pending job should be canceled + require.Equal(t, database.ProvisionerJobStatusCanceled, pendingJob.JobStatus) + require.Equal(t, clock.Now().UTC(), pendingJob.CanceledAt.Time.UTC()) + require.Equal(t, clock.Now().UTC(), pendingJob.CompletedAt.Time.UTC()) + + // Workspace should be deleted + deletedWorkspace, err := db.GetWorkspaceByID(ctx, prebuild.Workspace.ID) + require.NoError(t, err) + require.True(t, deletedWorkspace.Deleted) + latestBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, deletedWorkspace.ID) + require.NoError(t, err) + require.Equal(t, database.WorkspaceTransitionDelete, latestBuild.Transition) + deleteJob, err := db.GetProvisionerJobByID(ctx, latestBuild.JobID) + require.NoError(t, err) + require.True(t, deleteJob.CompletedAt.Valid) + require.False(t, deleteJob.WorkerID.Valid) + require.Equal(t, database.ProvisionerJobStatusSucceeded, deleteJob.JobStatus) } else { - require.NotEqual(t, database.ProvisionerJobStatusCanceled, job.JobStatus) - require.Zero(t, job.CanceledAt.Time.UTC()) + // Pending job should not be canceled + require.NotEqual(t, database.ProvisionerJobStatusCanceled, pendingJob.JobStatus) + require.Zero(t, pendingJob.CanceledAt.Time.UTC()) + + // Workspace should not be deleted + workspace, err := db.GetWorkspaceByID(ctx, prebuild.Workspace.ID) + require.NoError(t, err) + require.False(t, workspace.Deleted) } } } @@ -2309,22 +2350,22 @@ func TestCancelPendingPrebuilds(t *testing.T) { require.NoError(t, reconciler.ReconcileAll(ctx)) // Then: template A version 1 running workspaces should not be canceled - checkIfJobCanceled(t, clock, ctx, db, false, templateAVersion1Running) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateAVersion1Running) // Then: template A version 1 pending workspaces should be canceled - checkIfJobCanceled(t, clock, ctx, db, true, templateAVersion1Pending) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, true, templateAVersion1Pending) // Then: template A version 2 running and pending workspaces should not be canceled - checkIfJobCanceled(t, clock, ctx, db, false, templateAVersion2Running) - checkIfJobCanceled(t, clock, ctx, db, false, templateAVersion2Pending) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateAVersion2Running) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateAVersion2Pending) // Then: template B version 1 running workspaces should not be canceled - checkIfJobCanceled(t, clock, ctx, db, false, templateBVersion1Running) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateBVersion1Running) // Then: template B version 1 pending workspaces should be canceled - checkIfJobCanceled(t, clock, ctx, db, true, templateBVersion1Pending) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, true, templateBVersion1Pending) // Then: template B version 2 pending workspaces should be canceled - checkIfJobCanceled(t, clock, ctx, db, true, templateBVersion2Pending) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, true, templateBVersion2Pending) // Then: template B version 3 running and pending workspaces should not be canceled - checkIfJobCanceled(t, clock, ctx, db, false, templateBVersion3Running) - checkIfJobCanceled(t, clock, ctx, db, false, templateBVersion3Pending) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateBVersion3Running) + checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateBVersion3Pending) }) } From 2cf4b5c5a2d5757e5a0986084789586b5322583c Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Thu, 30 Oct 2025 10:50:38 +0000 Subject: [PATCH 06/12] perf: optimize prebuilds membership reconciliation to check orgs not presets (#20493) (#20555) Related to PR: https://github.com/coder/coder/pull/20493 (cherry picked from commit 7e8fcb4b0f3a7413ff10941e0e35b1f807566a04) --- coderd/database/dbauthz/dbauthz.go | 7 + coderd/database/dbauthz/dbauthz_test.go | 8 + coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 + coderd/database/querier.go | 3 + coderd/database/queries.sql.go | 87 ++++++ coderd/database/queries/prebuilds.sql | 44 +++ enterprise/coderd/prebuilds/membership.go | 148 +++++----- .../coderd/prebuilds/membership_test.go | 278 +++++++++--------- enterprise/coderd/prebuilds/reconcile.go | 12 +- 10 files changed, 397 insertions(+), 212 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index f169f7eeb13c1..1b2a6a5d97590 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2649,6 +2649,13 @@ func (q *querier) GetOrganizationsByUserID(ctx context.Context, userID database. return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetOrganizationsByUserID)(ctx, userID) } +func (q *querier) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg database.GetOrganizationsWithPrebuildStatusParams) ([]database.GetOrganizationsWithPrebuildStatusRow, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOrganization.All()); err != nil { + return nil, err + } + return q.db.GetOrganizationsWithPrebuildStatus(ctx, arg) +} + func (q *querier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { version, err := q.db.GetTemplateVersionByJobID(ctx, jobID) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 63226271f7fa0..32c951fb5c20b 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3759,6 +3759,14 @@ func (s *MethodTestSuite) TestPrebuilds() { dbm.EXPECT().GetPrebuildMetrics(gomock.Any()).Return([]database.GetPrebuildMetricsRow{}, nil).AnyTimes() check.Args().Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead) })) + s.Run("GetOrganizationsWithPrebuildStatus", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { + arg := database.GetOrganizationsWithPrebuildStatusParams{ + UserID: uuid.New(), + GroupName: "test", + } + dbm.EXPECT().GetOrganizationsWithPrebuildStatus(gomock.Any(), arg).Return([]database.GetOrganizationsWithPrebuildStatusRow{}, nil).AnyTimes() + check.Args(arg).Asserts(rbac.ResourceOrganization.All(), policy.ActionRead) + })) s.Run("GetPrebuildsSettings", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { dbm.EXPECT().GetPrebuildsSettings(gomock.Any()).Return("{}", nil).AnyTimes() check.Args().Asserts() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 1b36a3fa987f2..252f6f9b5ad09 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1243,6 +1243,13 @@ func (m queryMetricsStore) GetOrganizationsByUserID(ctx context.Context, userID return organizations, err } +func (m queryMetricsStore) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg database.GetOrganizationsWithPrebuildStatusParams) ([]database.GetOrganizationsWithPrebuildStatusRow, error) { + start := time.Now() + r0, r1 := m.s.GetOrganizationsWithPrebuildStatus(ctx, arg) + m.queryLatencies.WithLabelValues("GetOrganizationsWithPrebuildStatus").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { start := time.Now() schemas, err := m.s.GetParameterSchemasByJobID(ctx, jobID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 0ecd0191cbce1..af89a987a3203 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2622,6 +2622,21 @@ func (mr *MockStoreMockRecorder) GetOrganizationsByUserID(ctx, arg any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationsByUserID", reflect.TypeOf((*MockStore)(nil).GetOrganizationsByUserID), ctx, arg) } +// GetOrganizationsWithPrebuildStatus mocks base method. +func (m *MockStore) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg database.GetOrganizationsWithPrebuildStatusParams) ([]database.GetOrganizationsWithPrebuildStatusRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrganizationsWithPrebuildStatus", ctx, arg) + ret0, _ := ret[0].([]database.GetOrganizationsWithPrebuildStatusRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrganizationsWithPrebuildStatus indicates an expected call of GetOrganizationsWithPrebuildStatus. +func (mr *MockStoreMockRecorder) GetOrganizationsWithPrebuildStatus(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationsWithPrebuildStatus", reflect.TypeOf((*MockStore)(nil).GetOrganizationsWithPrebuildStatus), ctx, arg) +} + // GetParameterSchemasByJobID mocks base method. func (m *MockStore) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ParameterSchema, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 2aa1cb8650051..b1a450939834d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -269,6 +269,9 @@ type sqlcQuerier interface { GetOrganizationResourceCountByID(ctx context.Context, organizationID uuid.UUID) (GetOrganizationResourceCountByIDRow, error) GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) + // GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their + // membership status for the prebuilds system user (org membership, group existence, group membership). + GetOrganizationsWithPrebuildStatus(ctx context.Context, arg GetOrganizationsWithPrebuildStatusParams) ([]GetOrganizationsWithPrebuildStatusRow, error) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error) GetPrebuildsSettings(ctx context.Context) (string, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9a9c7d2629dd0..ff32a1126792d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -8285,6 +8285,93 @@ func (q *sqlQuerier) FindMatchingPresetID(ctx context.Context, arg FindMatchingP return template_version_preset_id, err } +const getOrganizationsWithPrebuildStatus = `-- name: GetOrganizationsWithPrebuildStatus :many +WITH orgs_with_prebuilds AS ( + -- Get unique organizations that have presets with prebuilds configured + SELECT DISTINCT o.id, o.name + FROM organizations o + INNER JOIN templates t ON t.organization_id = o.id + INNER JOIN template_versions tv ON tv.template_id = t.id + INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id + WHERE tvp.desired_instances IS NOT NULL +), +prebuild_user_membership AS ( + -- Check if the user is a member of the organizations + SELECT om.organization_id + FROM organization_members om + INNER JOIN orgs_with_prebuilds owp ON owp.id = om.organization_id + WHERE om.user_id = $1::uuid +), +prebuild_groups AS ( + -- Check if the organizations have the prebuilds group + SELECT g.organization_id, g.id as group_id + FROM groups g + INNER JOIN orgs_with_prebuilds owp ON owp.id = g.organization_id + WHERE g.name = $2::text +), +prebuild_group_membership AS ( + -- Check if the user is in the prebuilds group + SELECT pg.organization_id + FROM prebuild_groups pg + INNER JOIN group_members gm ON gm.group_id = pg.group_id + WHERE gm.user_id = $1::uuid +) +SELECT + owp.id AS organization_id, + owp.name AS organization_name, + (pum.organization_id IS NOT NULL)::boolean AS has_prebuild_user, + pg.group_id AS prebuilds_group_id, + (pgm.organization_id IS NOT NULL)::boolean AS has_prebuild_user_in_group +FROM orgs_with_prebuilds owp +LEFT JOIN prebuild_groups pg ON pg.organization_id = owp.id +LEFT JOIN prebuild_user_membership pum ON pum.organization_id = owp.id +LEFT JOIN prebuild_group_membership pgm ON pgm.organization_id = owp.id +` + +type GetOrganizationsWithPrebuildStatusParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + GroupName string `db:"group_name" json:"group_name"` +} + +type GetOrganizationsWithPrebuildStatusRow struct { + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + OrganizationName string `db:"organization_name" json:"organization_name"` + HasPrebuildUser bool `db:"has_prebuild_user" json:"has_prebuild_user"` + PrebuildsGroupID uuid.NullUUID `db:"prebuilds_group_id" json:"prebuilds_group_id"` + HasPrebuildUserInGroup bool `db:"has_prebuild_user_in_group" json:"has_prebuild_user_in_group"` +} + +// GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their +// membership status for the prebuilds system user (org membership, group existence, group membership). +func (q *sqlQuerier) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg GetOrganizationsWithPrebuildStatusParams) ([]GetOrganizationsWithPrebuildStatusRow, error) { + rows, err := q.db.QueryContext(ctx, getOrganizationsWithPrebuildStatus, arg.UserID, arg.GroupName) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrganizationsWithPrebuildStatusRow + for rows.Next() { + var i GetOrganizationsWithPrebuildStatusRow + if err := rows.Scan( + &i.OrganizationID, + &i.OrganizationName, + &i.HasPrebuildUser, + &i.PrebuildsGroupID, + &i.HasPrebuildUserInGroup, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getPrebuildMetrics = `-- name: GetPrebuildMetrics :many SELECT t.name as template_name, diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index 7c060971efba5..ae70593b269d9 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -327,3 +327,47 @@ SET FROM jobs_to_cancel WHERE provisioner_jobs.id = jobs_to_cancel.id RETURNING jobs_to_cancel.id, jobs_to_cancel.workspace_id, jobs_to_cancel.template_id, jobs_to_cancel.template_version_preset_id; + +-- name: GetOrganizationsWithPrebuildStatus :many +-- GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their +-- membership status for the prebuilds system user (org membership, group existence, group membership). +WITH orgs_with_prebuilds AS ( + -- Get unique organizations that have presets with prebuilds configured + SELECT DISTINCT o.id, o.name + FROM organizations o + INNER JOIN templates t ON t.organization_id = o.id + INNER JOIN template_versions tv ON tv.template_id = t.id + INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id + WHERE tvp.desired_instances IS NOT NULL +), +prebuild_user_membership AS ( + -- Check if the user is a member of the organizations + SELECT om.organization_id + FROM organization_members om + INNER JOIN orgs_with_prebuilds owp ON owp.id = om.organization_id + WHERE om.user_id = @user_id::uuid +), +prebuild_groups AS ( + -- Check if the organizations have the prebuilds group + SELECT g.organization_id, g.id as group_id + FROM groups g + INNER JOIN orgs_with_prebuilds owp ON owp.id = g.organization_id + WHERE g.name = @group_name::text +), +prebuild_group_membership AS ( + -- Check if the user is in the prebuilds group + SELECT pg.organization_id + FROM prebuild_groups pg + INNER JOIN group_members gm ON gm.group_id = pg.group_id + WHERE gm.user_id = @user_id::uuid +) +SELECT + owp.id AS organization_id, + owp.name AS organization_name, + (pum.organization_id IS NOT NULL)::boolean AS has_prebuild_user, + pg.group_id AS prebuilds_group_id, + (pgm.organization_id IS NOT NULL)::boolean AS has_prebuild_user_in_group +FROM orgs_with_prebuilds owp +LEFT JOIN prebuild_groups pg ON pg.organization_id = owp.id +LEFT JOIN prebuild_user_membership pum ON pum.organization_id = owp.id +LEFT JOIN prebuild_group_membership pgm ON pgm.organization_id = owp.id; diff --git a/enterprise/coderd/prebuilds/membership.go b/enterprise/coderd/prebuilds/membership.go index f843d33f7f106..9436f68737d4a 100644 --- a/enterprise/coderd/prebuilds/membership.go +++ b/enterprise/coderd/prebuilds/membership.go @@ -2,12 +2,13 @@ package prebuilds import ( "context" - "database/sql" "errors" "github.com/google/uuid" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/quartz" ) @@ -21,114 +22,117 @@ const ( // organizations for which prebuilt workspaces are requested. This is necessary because our data model requires that such // prebuilt workspaces belong to a member of the organization of their eventual claimant. type StoreMembershipReconciler struct { - store database.Store - clock quartz.Clock + store database.Store + clock quartz.Clock + logger slog.Logger } -func NewStoreMembershipReconciler(store database.Store, clock quartz.Clock) StoreMembershipReconciler { +func NewStoreMembershipReconciler(store database.Store, clock quartz.Clock, logger slog.Logger) StoreMembershipReconciler { return StoreMembershipReconciler{ - store: store, - clock: clock, + store: store, + clock: clock, + logger: logger, } } -// ReconcileAll compares the current organization and group memberships of a user to the memberships required -// in order to create prebuilt workspaces. If the user in question is not yet a member of an organization that -// needs prebuilt workspaces, ReconcileAll will create the membership required. +// ReconcileAll ensures the prebuilds system user has the necessary memberships to create prebuilt workspaces. +// For each organization with prebuilds configured, it ensures: +// * The user is a member of the organization +// * A group exists with quota 0 +// * The user is a member of that group // -// To facilitate quota management, ReconcileAll will ensure: -// * the existence of a group (defined by PrebuiltWorkspacesGroupName) in each organization that needs prebuilt workspaces -// * that the prebuilds system user belongs to the group in each organization that needs prebuilt workspaces -// * that the group has a quota of 0 by default, which users can adjust based on their needs. +// Unique constraint violations are safely ignored (concurrent creation). // // ReconcileAll does not have an opinion on transaction or lock management. These responsibilities are left to the caller. -func (s StoreMembershipReconciler) ReconcileAll(ctx context.Context, userID uuid.UUID, presets []database.GetTemplatePresetsWithPrebuildsRow) error { - organizationMemberships, err := s.store.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ - UserID: userID, - Deleted: sql.NullBool{ - Bool: false, - Valid: true, - }, +func (s StoreMembershipReconciler) ReconcileAll(ctx context.Context, userID uuid.UUID, groupName string) error { + orgStatuses, err := s.store.GetOrganizationsWithPrebuildStatus(ctx, database.GetOrganizationsWithPrebuildStatusParams{ + UserID: userID, + GroupName: groupName, }) if err != nil { - return xerrors.Errorf("determine prebuild organization membership: %w", err) - } - - orgMemberships := make(map[uuid.UUID]struct{}, 0) - defaultOrg, err := s.store.GetDefaultOrganization(ctx) - if err != nil { - return xerrors.Errorf("get default organization: %w", err) - } - orgMemberships[defaultOrg.ID] = struct{}{} - for _, o := range organizationMemberships { - orgMemberships[o.ID] = struct{}{} + return xerrors.Errorf("get organizations with prebuild status: %w", err) } var membershipInsertionErrors error - for _, preset := range presets { - _, alreadyOrgMember := orgMemberships[preset.OrganizationID] - if !alreadyOrgMember { - // Add the organization to our list of memberships regardless of potential failure below - // to avoid a retry that will probably be doomed anyway. - orgMemberships[preset.OrganizationID] = struct{}{} + for _, orgStatus := range orgStatuses { + s.logger.Debug(ctx, "organization prebuild status", + slog.F("organization_id", orgStatus.OrganizationID), + slog.F("organization_name", orgStatus.OrganizationName), + slog.F("has_prebuild_user", orgStatus.HasPrebuildUser), + slog.F("has_prebuild_group", orgStatus.PrebuildsGroupID.Valid), + slog.F("has_prebuild_user_in_group", orgStatus.HasPrebuildUserInGroup)) - // Insert the missing membership + // Add user to org if needed + if !orgStatus.HasPrebuildUser { _, err = s.store.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ - OrganizationID: preset.OrganizationID, + OrganizationID: orgStatus.OrganizationID, UserID: userID, CreatedAt: s.clock.Now(), UpdatedAt: s.clock.Now(), Roles: []string{}, }) - if err != nil { - membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("insert membership for prebuilt workspaces: %w", err)) + // Unique violation means organization membership was created after status check, safe to ignore. + if err != nil && !database.IsUniqueViolation(err) { + membershipInsertionErrors = errors.Join(membershipInsertionErrors, err) continue } - } - - // determine whether the org already has a prebuilds group - prebuildsGroupExists := true - prebuildsGroup, err := s.store.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{ - OrganizationID: preset.OrganizationID, - Name: PrebuiltWorkspacesGroupName, - }) - if err != nil { - if !xerrors.Is(err, sql.ErrNoRows) { - membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("get prebuilds group: %w", err)) - continue + if err == nil { + s.logger.Info(ctx, "added prebuilds user to organization", + slog.F("organization_id", orgStatus.OrganizationID), + slog.F("organization_name", orgStatus.OrganizationName), + slog.F("prebuilds_user", userID.String())) } - prebuildsGroupExists = false } - // if the prebuilds group does not exist, create it - if !prebuildsGroupExists { - // create a "prebuilds" group in the organization and add the system user to it - // this group will have a quota of 0 by default, which users can adjust based on their needs - prebuildsGroup, err = s.store.InsertGroup(ctx, database.InsertGroupParams{ + // Create group if it doesn't exist + var groupID uuid.UUID + if !orgStatus.PrebuildsGroupID.Valid { + // Group doesn't exist, create it + group, err := s.store.InsertGroup(ctx, database.InsertGroupParams{ ID: uuid.New(), Name: PrebuiltWorkspacesGroupName, DisplayName: PrebuiltWorkspacesGroupDisplayName, - OrganizationID: preset.OrganizationID, + OrganizationID: orgStatus.OrganizationID, AvatarURL: "", - QuotaAllowance: 0, // Default quota of 0, users should set this based on their needs + QuotaAllowance: 0, }) - if err != nil { - membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("create prebuilds group: %w", err)) + // Unique violation means group was created after status check, safe to ignore. + if err != nil && !database.IsUniqueViolation(err) { + membershipInsertionErrors = errors.Join(membershipInsertionErrors, err) continue } + if err == nil { + s.logger.Info(ctx, "created prebuilds group in organization", + slog.F("organization_id", orgStatus.OrganizationID), + slog.F("organization_name", orgStatus.OrganizationName), + slog.F("prebuilds_group", group.ID.String())) + } + groupID = group.ID + } else { + // Group exists + groupID = orgStatus.PrebuildsGroupID.UUID } - // add the system user to the prebuilds group - err = s.store.InsertGroupMember(ctx, database.InsertGroupMemberParams{ - GroupID: prebuildsGroup.ID, - UserID: userID, - }) - if err != nil { - // ignore unique violation errors as the user might already be in the group - if !database.IsUniqueViolation(err) { - membershipInsertionErrors = errors.Join(membershipInsertionErrors, xerrors.Errorf("add system user to prebuilds group: %w", err)) + // Add user to group if needed + if !orgStatus.HasPrebuildUserInGroup { + err = s.store.InsertGroupMember(ctx, database.InsertGroupMemberParams{ + GroupID: groupID, + UserID: userID, + }) + // Unique violation means group membership was created after status check, safe to ignore. + if err != nil && !database.IsUniqueViolation(err) { + membershipInsertionErrors = errors.Join(membershipInsertionErrors, err) + continue + } + if err == nil { + s.logger.Info(ctx, "added prebuilds user to prebuilds group", + slog.F("organization_id", orgStatus.OrganizationID), + slog.F("organization_name", orgStatus.OrganizationName), + slog.F("prebuilds_user", userID.String()), + slog.F("prebuilds_group", groupID.String())) } } } + return membershipInsertionErrors } diff --git a/enterprise/coderd/prebuilds/membership_test.go b/enterprise/coderd/prebuilds/membership_test.go index 55d6557b12495..fe4ec26259889 100644 --- a/enterprise/coderd/prebuilds/membership_test.go +++ b/enterprise/coderd/prebuilds/membership_test.go @@ -7,16 +7,17 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" - "tailscale.com/types/ptr" - "github.com/coder/quartz" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/enterprise/coderd/prebuilds" "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" ) // TestReconcileAll verifies that StoreMembershipReconciler correctly updates membership @@ -26,169 +27,178 @@ func TestReconcileAll(t *testing.T) { clock := quartz.NewMock(t) - // Helper to build a minimal Preset row belonging to a given org. - newPresetRow := func(orgID uuid.UUID) database.GetTemplatePresetsWithPrebuildsRow { - return database.GetTemplatePresetsWithPrebuildsRow{ - ID: uuid.New(), - OrganizationID: orgID, - } - } - tests := []struct { name string - includePreset []bool + includePreset bool preExistingOrgMembership []bool preExistingGroup []bool preExistingGroupMembership []bool // Expected outcomes - expectOrgMembershipExists *bool - expectGroupExists *bool - expectUserInGroup *bool + expectOrgMembershipExists bool + expectGroupExists bool + expectUserInGroup bool }{ { name: "if there are no presets, membership reconciliation is a no-op", - includePreset: []bool{false}, + includePreset: false, preExistingOrgMembership: []bool{true, false}, preExistingGroup: []bool{true, false}, preExistingGroupMembership: []bool{true, false}, - expectOrgMembershipExists: ptr.To(false), - expectGroupExists: ptr.To(false), + expectOrgMembershipExists: false, + expectGroupExists: false, + expectUserInGroup: false, }, { name: "if there is a preset, then we should enforce org and group membership in all cases", - includePreset: []bool{true}, + includePreset: true, preExistingOrgMembership: []bool{true, false}, preExistingGroup: []bool{true, false}, preExistingGroupMembership: []bool{true, false}, - expectOrgMembershipExists: ptr.To(true), - expectGroupExists: ptr.To(true), - expectUserInGroup: ptr.To(true), + expectOrgMembershipExists: true, + expectGroupExists: true, + expectUserInGroup: true, }, } for _, tc := range tests { tc := tc - for _, includePreset := range tc.includePreset { - includePreset := includePreset - for _, preExistingOrgMembership := range tc.preExistingOrgMembership { - preExistingOrgMembership := preExistingOrgMembership - for _, preExistingGroup := range tc.preExistingGroup { - preExistingGroup := preExistingGroup - for _, preExistingGroupMembership := range tc.preExistingGroupMembership { - preExistingGroupMembership := preExistingGroupMembership - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - // nolint:gocritic // Reconciliation happens as prebuilds system user, not a human user. - ctx := dbauthz.AsPrebuildsOrchestrator(testutil.Context(t, testutil.WaitLong)) - _, db := coderdtest.NewWithDatabase(t, nil) - - defaultOrg, err := db.GetDefaultOrganization(ctx) - require.NoError(t, err) - - // introduce an unrelated organization to ensure that the membership reconciler doesn't interfere with it. - unrelatedOrg := dbgen.Organization(t, db, database.Organization{}) - targetOrg := dbgen.Organization(t, db, database.Organization{}) - - // Ensure membership to unrelated org. - dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: unrelatedOrg.ID, UserID: database.PrebuildsSystemUserID}) - - if preExistingOrgMembership { - // System user already a member of both orgs. - dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: targetOrg.ID, UserID: database.PrebuildsSystemUserID}) - } + includePreset := tc.includePreset + for _, preExistingOrgMembership := range tc.preExistingOrgMembership { + preExistingOrgMembership := preExistingOrgMembership + for _, preExistingGroup := range tc.preExistingGroup { + preExistingGroup := preExistingGroup + for _, preExistingGroupMembership := range tc.preExistingGroupMembership { + preExistingGroupMembership := preExistingGroupMembership + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // nolint:gocritic // Reconciliation happens as prebuilds system user, not a human user. + ctx := dbauthz.AsPrebuildsOrchestrator(testutil.Context(t, testutil.WaitLong)) + client, db := coderdtest.NewWithDatabase(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + + defaultOrg, err := db.GetDefaultOrganization(ctx) + require.NoError(t, err) + + // Introduce an unrelated organization to ensure that the membership reconciler doesn't interfere with it. + unrelatedOrg := dbgen.Organization(t, db, database.Organization{}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: unrelatedOrg.ID, UserID: database.PrebuildsSystemUserID}) + + // Organization to test + targetOrg := dbgen.Organization(t, db, database.Organization{}) + + // Prebuilds system user is a member of the organization + if preExistingOrgMembership { + dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: targetOrg.ID, UserID: database.PrebuildsSystemUserID}) + } + + // Organization has the prebuilds group + var prebuildsGroup database.Group + if preExistingGroup { + prebuildsGroup = dbgen.Group(t, db, database.Group{ + Name: prebuilds.PrebuiltWorkspacesGroupName, + DisplayName: prebuilds.PrebuiltWorkspacesGroupDisplayName, + OrganizationID: targetOrg.ID, + QuotaAllowance: 0, + }) - // Create pre-existing prebuilds group if required by test case - var prebuildsGroup database.Group - if preExistingGroup { - prebuildsGroup = dbgen.Group(t, db, database.Group{ - Name: prebuilds.PrebuiltWorkspacesGroupName, - DisplayName: prebuilds.PrebuiltWorkspacesGroupDisplayName, - OrganizationID: targetOrg.ID, - QuotaAllowance: 0, + // Add the system user to the group if required by test case + if preExistingGroupMembership { + dbgen.GroupMember(t, db, database.GroupMemberTable{ + GroupID: prebuildsGroup.ID, + UserID: database.PrebuildsSystemUserID, }) - - // Add the system user to the group if preExistingGroupMembership is true - if preExistingGroupMembership { - dbgen.GroupMember(t, db, database.GroupMemberTable{ - GroupID: prebuildsGroup.ID, - UserID: database.PrebuildsSystemUserID, - }) - } } - - presets := []database.GetTemplatePresetsWithPrebuildsRow{newPresetRow(unrelatedOrg.ID)} - if includePreset { - presets = append(presets, newPresetRow(targetOrg.ID)) - } - - // Verify memberships before reconciliation. - preReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ - UserID: database.PrebuildsSystemUserID, - }) - require.NoError(t, err) - expectedMembershipsBefore := []uuid.UUID{defaultOrg.ID, unrelatedOrg.ID} - if preExistingOrgMembership { - expectedMembershipsBefore = append(expectedMembershipsBefore, targetOrg.ID) - } - require.ElementsMatch(t, expectedMembershipsBefore, extractOrgIDs(preReconcileMemberships)) - - // Reconcile - reconciler := prebuilds.NewStoreMembershipReconciler(db, clock) - require.NoError(t, reconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, presets)) - - // Verify memberships after reconciliation. - postReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ - UserID: database.PrebuildsSystemUserID, - }) + } + + // Setup unrelated org preset + dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + OrganizationID: unrelatedOrg.ID, + CreatedBy: owner.UserID, + }).Preset(database.TemplateVersionPreset{ + DesiredInstances: sql.NullInt32{ + Int32: 1, + Valid: true, + }, + }).Do() + + // Setup target org preset + dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + OrganizationID: targetOrg.ID, + CreatedBy: owner.UserID, + }).Preset(database.TemplateVersionPreset{ + DesiredInstances: sql.NullInt32{ + Int32: 0, + Valid: includePreset, + }, + }).Do() + + // Verify memberships before reconciliation. + preReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ + UserID: database.PrebuildsSystemUserID, + }) + require.NoError(t, err) + expectedMembershipsBefore := []uuid.UUID{defaultOrg.ID, unrelatedOrg.ID} + if preExistingOrgMembership { + expectedMembershipsBefore = append(expectedMembershipsBefore, targetOrg.ID) + } + require.ElementsMatch(t, expectedMembershipsBefore, extractOrgIDs(preReconcileMemberships)) + + // Reconcile + reconciler := prebuilds.NewStoreMembershipReconciler(db, clock, slogtest.Make(t, nil)) + require.NoError(t, reconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, prebuilds.PrebuiltWorkspacesGroupName)) + + // Verify memberships after reconciliation. + postReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ + UserID: database.PrebuildsSystemUserID, + }) + require.NoError(t, err) + expectedMembershipsAfter := expectedMembershipsBefore + if !preExistingOrgMembership && tc.expectOrgMembershipExists { + expectedMembershipsAfter = append(expectedMembershipsAfter, targetOrg.ID) + } + require.ElementsMatch(t, expectedMembershipsAfter, extractOrgIDs(postReconcileMemberships)) + + // Verify prebuilds group behavior based on expected outcomes + prebuildsGroup, err = db.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{ + OrganizationID: targetOrg.ID, + Name: prebuilds.PrebuiltWorkspacesGroupName, + }) + if tc.expectGroupExists { require.NoError(t, err) - expectedMembershipsAfter := expectedMembershipsBefore - if !preExistingOrgMembership && tc.expectOrgMembershipExists != nil && *tc.expectOrgMembershipExists { - expectedMembershipsAfter = append(expectedMembershipsAfter, targetOrg.ID) - } - require.ElementsMatch(t, expectedMembershipsAfter, extractOrgIDs(postReconcileMemberships)) - - // Verify prebuilds group behavior based on expected outcomes - prebuildsGroup, err = db.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{ - OrganizationID: targetOrg.ID, - Name: prebuilds.PrebuiltWorkspacesGroupName, - }) - if tc.expectGroupExists != nil && *tc.expectGroupExists { + require.Equal(t, prebuilds.PrebuiltWorkspacesGroupName, prebuildsGroup.Name) + require.Equal(t, prebuilds.PrebuiltWorkspacesGroupDisplayName, prebuildsGroup.DisplayName) + require.Equal(t, int32(0), prebuildsGroup.QuotaAllowance) // Default quota should be 0 + + if tc.expectUserInGroup { + // Check that the system user is a member of the prebuilds group + groupMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: prebuildsGroup.ID, + IncludeSystem: true, + }) require.NoError(t, err) - require.Equal(t, prebuilds.PrebuiltWorkspacesGroupName, prebuildsGroup.Name) - require.Equal(t, prebuilds.PrebuiltWorkspacesGroupDisplayName, prebuildsGroup.DisplayName) - require.Equal(t, int32(0), prebuildsGroup.QuotaAllowance) // Default quota should be 0 - - if tc.expectUserInGroup != nil && *tc.expectUserInGroup { - // Check that the system user is a member of the prebuilds group - groupMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ - GroupID: prebuildsGroup.ID, - IncludeSystem: true, - }) - require.NoError(t, err) - require.Len(t, groupMembers, 1) - require.Equal(t, database.PrebuildsSystemUserID, groupMembers[0].UserID) - } - - // If no preset exists, then we do not enforce group membership: - if tc.expectUserInGroup != nil && !*tc.expectUserInGroup { - // Check that the system user is NOT a member of the prebuilds group - groupMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ - GroupID: prebuildsGroup.ID, - IncludeSystem: true, - }) - require.NoError(t, err) - require.Len(t, groupMembers, 0) - } + require.Len(t, groupMembers, 1) + require.Equal(t, database.PrebuildsSystemUserID, groupMembers[0].UserID) } - if !preExistingGroup && tc.expectGroupExists != nil && !*tc.expectGroupExists { - // Verify that no prebuilds group exists - require.Error(t, err) - require.True(t, errors.Is(err, sql.ErrNoRows)) + // If no preset exists, then we do not enforce group membership: + if !tc.expectUserInGroup { + // Check that the system user is NOT a member of the prebuilds group + groupMembers, err := db.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{ + GroupID: prebuildsGroup.ID, + IncludeSystem: true, + }) + require.NoError(t, err) + require.Len(t, groupMembers, 0) } - }) - } + } + + if !preExistingGroup && !tc.expectGroupExists { + // Verify that no prebuilds group exists + require.Error(t, err) + require.True(t, errors.Is(err, sql.ErrNoRows)) + } + }) } } } diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 5e5eec68ab382..0e658e887b097 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -298,6 +298,12 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { return nil } + membershipReconciler := NewStoreMembershipReconciler(c.store, c.clock, logger) + err = membershipReconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, PrebuiltWorkspacesGroupName) + if err != nil { + return xerrors.Errorf("reconcile prebuild membership: %w", err) + } + snapshot, err := c.SnapshotState(ctx, c.store) if err != nil { return xerrors.Errorf("determine current snapshot: %w", err) @@ -310,12 +316,6 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { return nil } - membershipReconciler := NewStoreMembershipReconciler(c.store, c.clock) - err = membershipReconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, snapshot.Presets) - if err != nil { - return xerrors.Errorf("reconcile prebuild membership: %w", err) - } - var eg errgroup.Group // Reconcile presets in parallel. Each preset in its own goroutine. for _, preset := range snapshot.Presets { From 64240931467d18b7d40275327654986a2040b70d Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Thu, 30 Oct 2025 12:16:19 +0000 Subject: [PATCH 07/12] feat: add prebuilds reconciliation duration metric (#20535) (#20581) Related to PR: https://github.com/coder/coder/pull/20535 (cherry picked from commit aad1b401c12b287c5a33389db991e8e68c92c358) --- coderd/prebuilds/api.go | 7 +- coderd/prebuilds/noop.go | 6 +- .../coderd/prebuilds/metricscollector_test.go | 6 +- enterprise/coderd/prebuilds/reconcile.go | 36 +++++-- enterprise/coderd/prebuilds/reconcile_test.go | 101 ++++++++++++++---- 5 files changed, 125 insertions(+), 31 deletions(-) diff --git a/coderd/prebuilds/api.go b/coderd/prebuilds/api.go index ed39f2a322776..0deab99416fd5 100644 --- a/coderd/prebuilds/api.go +++ b/coderd/prebuilds/api.go @@ -37,13 +37,18 @@ type ReconciliationOrchestrator interface { TrackResourceReplacement(ctx context.Context, workspaceID, buildID uuid.UUID, replacements []*sdkproto.ResourceReplacement) } +// ReconcileStats contains statistics about a reconciliation cycle. +type ReconcileStats struct { + Elapsed time.Duration +} + type Reconciler interface { StateSnapshotter // ReconcileAll orchestrates the reconciliation of all prebuilds across all templates. // It takes a global snapshot of the system state and then reconciles each preset // in parallel, creating or deleting prebuilds as needed to reach their desired states. - ReconcileAll(ctx context.Context) error + ReconcileAll(ctx context.Context) (ReconcileStats, error) } // StateSnapshotter defines the operations necessary to capture workspace prebuilds state. diff --git a/coderd/prebuilds/noop.go b/coderd/prebuilds/noop.go index 170b0a12af6fd..0859d428b4796 100644 --- a/coderd/prebuilds/noop.go +++ b/coderd/prebuilds/noop.go @@ -17,7 +17,11 @@ func (NoopReconciler) Run(context.Context) {} func (NoopReconciler) Stop(context.Context, error) {} func (NoopReconciler) TrackResourceReplacement(context.Context, uuid.UUID, uuid.UUID, []*sdkproto.ResourceReplacement) { } -func (NoopReconciler) ReconcileAll(context.Context) error { return nil } + +func (NoopReconciler) ReconcileAll(context.Context) (ReconcileStats, error) { + return ReconcileStats{}, nil +} + func (NoopReconciler) SnapshotState(context.Context, database.Store) (*GlobalSnapshot, error) { return &GlobalSnapshot{}, nil } diff --git a/enterprise/coderd/prebuilds/metricscollector_test.go b/enterprise/coderd/prebuilds/metricscollector_test.go index f9584e9ec2c25..aa9886fb7ad1b 100644 --- a/enterprise/coderd/prebuilds/metricscollector_test.go +++ b/enterprise/coderd/prebuilds/metricscollector_test.go @@ -485,7 +485,7 @@ func TestMetricsCollector_ReconciliationPausedMetric(t *testing.T) { require.NoError(t, err) // Run reconciliation to update the metric - err = reconciler.ReconcileAll(ctx) + _, err = reconciler.ReconcileAll(ctx) require.NoError(t, err) // Check that the metric shows reconciliation is not paused @@ -514,7 +514,7 @@ func TestMetricsCollector_ReconciliationPausedMetric(t *testing.T) { require.NoError(t, err) // Run reconciliation to update the metric - err = reconciler.ReconcileAll(ctx) + _, err = reconciler.ReconcileAll(ctx) require.NoError(t, err) // Check that the metric shows reconciliation is paused @@ -543,7 +543,7 @@ func TestMetricsCollector_ReconciliationPausedMetric(t *testing.T) { require.NoError(t, err) // Run reconciliation to update the metric - err = reconciler.ReconcileAll(ctx) + _, err = reconciler.ReconcileAll(ctx) require.NoError(t, err) // Check that the metric shows reconciliation is not paused diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go index 0e658e887b097..f280436ea98c8 100644 --- a/enterprise/coderd/prebuilds/reconcile.go +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/hashicorp/go-multierror" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" @@ -44,7 +45,6 @@ type StoreReconciler struct { logger slog.Logger clock quartz.Clock registerer prometheus.Registerer - metrics *MetricsCollector notifEnq notifications.Enqueuer buildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker] @@ -53,6 +53,11 @@ type StoreReconciler struct { stopped atomic.Bool done chan struct{} provisionNotifyCh chan database.ProvisionerJob + + // Prebuild state metrics + metrics *MetricsCollector + // Operational metrics + reconciliationDuration prometheus.Histogram } var _ prebuilds.ReconciliationOrchestrator = &StoreReconciler{} @@ -105,6 +110,15 @@ func NewStoreReconciler(store database.Store, // If the registerer fails to register the metrics collector, it's not fatal. logger.Error(context.Background(), "failed to register prometheus metrics", slog.Error(err)) } + + factory := promauto.With(registerer) + reconciler.reconciliationDuration = factory.NewHistogram(prometheus.HistogramOpts{ + Namespace: "coderd", + Subsystem: "prebuilds", + Name: "reconciliation_duration_seconds", + Help: "Duration of each prebuilds reconciliation cycle.", + Buckets: prometheus.DefBuckets, + }) } return reconciler @@ -176,10 +190,15 @@ func (c *StoreReconciler) Run(ctx context.Context) { // instead of waiting for the next reconciliation interval case <-ticker.C: // Trigger a new iteration on each tick. - err := c.ReconcileAll(ctx) + stats, err := c.ReconcileAll(ctx) if err != nil { c.logger.Error(context.Background(), "reconciliation failed", slog.Error(err)) } + + if c.reconciliationDuration != nil { + c.reconciliationDuration.Observe(stats.Elapsed.Seconds()) + } + c.logger.Debug(ctx, "reconciliation stats", slog.F("elapsed", stats.Elapsed)) case <-ctx.Done(): // nolint:gocritic // it's okay to use slog.F() for an error in this case // because we want to differentiate two different types of errors: ctx.Err() and context.Cause() @@ -263,19 +282,24 @@ func (c *StoreReconciler) Stop(ctx context.Context, cause error) { // be reconciled again, leading to another workspace being provisioned. Two workspace builds will be occurring // simultaneously for the same preset, but once both jobs have completed the reconciliation loop will notice the // extraneous instance and delete it. -func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { +func (c *StoreReconciler) ReconcileAll(ctx context.Context) (stats prebuilds.ReconcileStats, err error) { + start := c.clock.Now() + defer func() { + stats.Elapsed = c.clock.Since(start) + }() + logger := c.logger.With(slog.F("reconcile_context", "all")) select { case <-ctx.Done(): logger.Warn(context.Background(), "reconcile exiting prematurely; context done", slog.Error(ctx.Err())) - return nil + return stats, nil default: } logger.Debug(ctx, "starting reconciliation") - err := c.WithReconciliationLock(ctx, logger, func(ctx context.Context, _ database.Store) error { + err = c.WithReconciliationLock(ctx, logger, func(ctx context.Context, _ database.Store) error { // Check if prebuilds reconciliation is paused settingsJSON, err := c.store.GetPrebuildsSettings(ctx) if err != nil { @@ -348,7 +372,7 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { logger.Error(ctx, "failed to reconcile", slog.Error(err)) } - return err + return stats, err } func (c *StoreReconciler) reportHardLimitedPresets(snapshot *prebuilds.GlobalSnapshot) { diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go index 4b359ba9df429..7548faebd7dab 100644 --- a/enterprise/coderd/prebuilds/reconcile_test.go +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -72,7 +72,8 @@ func TestNoReconciliationActionsIfNoPresets(t *testing.T) { require.Equal(t, templateVersion, gotTemplateVersion) // when we trigger the reconciliation loop for all templates - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // then no reconciliation actions are taken // because without presets, there are no prebuilds @@ -126,7 +127,8 @@ func TestNoReconciliationActionsIfNoPrebuilds(t *testing.T) { require.NotEmpty(t, presetParameters) // when we trigger the reconciliation loop for all templates - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // then no reconciliation actions are taken // because without prebuilds, there is nothing to reconcile @@ -428,7 +430,8 @@ func (tc testCase) run(t *testing.T) { // Run the reconciliation multiple times to ensure idempotency // 8 was arbitrary, but large enough to reasonably trust the result for i := 1; i <= 8; i++ { - require.NoErrorf(t, controller.ReconcileAll(ctx), "failed on iteration %d", i) + _, err := controller.ReconcileAll(ctx) + require.NoErrorf(t, err, "failed on iteration %d", i) if tc.shouldCreateNewPrebuild != nil { newPrebuildCount := 0 @@ -542,7 +545,8 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) { // Run the reconciliation multiple times to ensure idempotency // 8 was arbitrary, but large enough to reasonably trust the result for i := 1; i <= 8; i++ { - require.NoErrorf(t, controller.ReconcileAll(ctx), "failed on iteration %d", i) + _, err := controller.ReconcileAll(ctx) + require.NoErrorf(t, err, "failed on iteration %d", i) newPrebuildCount := 0 workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) @@ -668,7 +672,7 @@ func TestPrebuildScheduling(t *testing.T) { DesiredInstances: 5, }) - err := controller.ReconcileAll(ctx) + _, err := controller.ReconcileAll(ctx) require.NoError(t, err) // get workspace builds @@ -751,7 +755,8 @@ func TestInvalidPreset(t *testing.T) { // Run the reconciliation multiple times to ensure idempotency // 8 was arbitrary, but large enough to reasonably trust the result for i := 1; i <= 8; i++ { - require.NoErrorf(t, controller.ReconcileAll(ctx), "failed on iteration %d", i) + _, err := controller.ReconcileAll(ctx) + require.NoErrorf(t, err, "failed on iteration %d", i) workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) @@ -817,7 +822,8 @@ func TestDeletionOfPrebuiltWorkspaceWithInvalidPreset(t *testing.T) { }) // Old prebuilt workspace should be deleted. - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) builds, err := db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{ WorkspaceID: prebuiltWorkspace.ID, @@ -916,12 +922,15 @@ func TestSkippingHardLimitedPresets(t *testing.T) { // Trigger reconciliation to attempt creating a new prebuild. // The outcome depends on whether the hard limit has been reached. - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // These two additional calls to ReconcileAll should not trigger any notifications. // A notification is only sent once. - require.NoError(t, controller.ReconcileAll(ctx)) - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // Verify the final state after reconciliation. workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) @@ -1093,12 +1102,15 @@ func TestHardLimitedPresetShouldNotBlockDeletion(t *testing.T) { // Trigger reconciliation to attempt creating a new prebuild. // The outcome depends on whether the hard limit has been reached. - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // These two additional calls to ReconcileAll should not trigger any notifications. // A notification is only sent once. - require.NoError(t, controller.ReconcileAll(ctx)) - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // Verify the final state after reconciliation. // When hard limit is reached, no new workspace should be created. @@ -1141,7 +1153,8 @@ func TestHardLimitedPresetShouldNotBlockDeletion(t *testing.T) { } // Trigger reconciliation to make sure that successful, but outdated prebuilt workspace will be deleted. - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID) require.NoError(t, err) @@ -1740,7 +1753,8 @@ func TestExpiredPrebuildsMultipleActions(t *testing.T) { } // Trigger reconciliation to process expired prebuilds and enforce desired state. - require.NoError(t, controller.ReconcileAll(ctx)) + _, err = controller.ReconcileAll(ctx) + require.NoError(t, err) // Sort non-expired workspaces by CreatedAt in ascending order (oldest first) sort.Slice(nonExpiredWorkspaces, func(i, j int) bool { @@ -2145,7 +2159,8 @@ func TestCancelPendingPrebuilds(t *testing.T) { require.NoError(t, err) // When: the reconciliation loop is triggered - require.NoError(t, reconciler.ReconcileAll(ctx)) + _, err = reconciler.ReconcileAll(ctx) + require.NoError(t, err) if tt.shouldCancel { // Then: the pending prebuild job from non-active version should be canceled @@ -2347,7 +2362,8 @@ func TestCancelPendingPrebuilds(t *testing.T) { templateBVersion3Pending := setupPrebuilds(t, db, owner.OrganizationID, templateBID, templateBVersion3ID, templateBVersion3PresetID, 1, true) // When: the reconciliation loop is executed - require.NoError(t, reconciler.ReconcileAll(ctx)) + _, err := reconciler.ReconcileAll(ctx) + require.NoError(t, err) // Then: template A version 1 running workspaces should not be canceled checkIfJobCanceledAndDeleted(t, clock, ctx, db, false, templateAVersion1Running) @@ -2369,6 +2385,51 @@ func TestCancelPendingPrebuilds(t *testing.T) { }) } +func TestReconciliationStats(t *testing.T) { + t.Parallel() + + // Setup + clock := quartz.NewReal() + db, ps := dbtestutil.NewDB(t) + client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + Clock: clock, + }) + fakeEnqueuer := newFakeEnqueuer() + registry := prometheus.NewRegistry() + cache := files.New(registry, &coderdtest.FakeAuthorizer{}) + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug) + reconciler := prebuilds.NewStoreReconciler(db, ps, cache, codersdk.PrebuildsConfig{}, logger, clock, registry, fakeEnqueuer, newNoopUsageCheckerPtr()) + owner := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + // Create a template version with a preset + dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + OrganizationID: owner.OrganizationID, + CreatedBy: owner.UserID, + }).Preset(database.TemplateVersionPreset{ + DesiredInstances: sql.NullInt32{ + Int32: 1, + Valid: true, + }, + }).Do() + + // Verify that ReconcileAll tracks and returns elapsed time + start := time.Now() + stats, err := reconciler.ReconcileAll(ctx) + actualElapsed := time.Since(start) + require.NoError(t, err) + require.Greater(t, stats.Elapsed, time.Duration(0)) + + // Verify stats.Elapsed matches actual execution time + require.InDelta(t, actualElapsed.Milliseconds(), stats.Elapsed.Milliseconds(), 100) + // Verify reconciliation loop is not unexpectedly slow + require.Less(t, stats.Elapsed, 5*time.Second) +} + func newNoopEnqueuer() *notifications.NoopEnqueuer { return notifications.NewNoopEnqueuer() } @@ -2863,7 +2924,7 @@ func TestReconciliationRespectsPauseSetting(t *testing.T) { _ = setupTestDBPreset(t, db, templateVersionID, 2, "test") // Initially, reconciliation should create prebuilds - err := reconciler.ReconcileAll(ctx) + _, err := reconciler.ReconcileAll(ctx) require.NoError(t, err) // Verify that prebuilds were created @@ -2890,7 +2951,7 @@ func TestReconciliationRespectsPauseSetting(t *testing.T) { require.Len(t, workspaces, 0, "prebuilds should be deleted") // Run reconciliation again - it should be paused and not recreate prebuilds - err = reconciler.ReconcileAll(ctx) + _, err = reconciler.ReconcileAll(ctx) require.NoError(t, err) // Verify that no new prebuilds were created because reconciliation is paused @@ -2903,7 +2964,7 @@ func TestReconciliationRespectsPauseSetting(t *testing.T) { require.NoError(t, err) // Run reconciliation again - it should now recreate the prebuilds - err = reconciler.ReconcileAll(ctx) + _, err = reconciler.ReconcileAll(ctx) require.NoError(t, err) // Verify that prebuilds were recreated From effbe4e52e83ef78bfcbac858fbbaf4751aa854b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Oct 2025 17:02:25 +0000 Subject: [PATCH 08/12] refactor: remove TaskAppID from codersdk.WorkspaceBuild (#20583) (#20592) `TaskAppID` has not yet been shipped. We're dropping this field in favor of using the same information but from `codersdk.Task`. --- Cherry picked from d80b5fc8eda9b72e1b793fef627e83b8b36479da https://github.com/coder/coder/pull/20583 --- coderd/apidoc/docs.go | 6 +- coderd/apidoc/swagger.json | 6 +- coderd/workspacebuilds.go | 1 - codersdk/workspacebuilds.go | 3 +- docs/reference/api/builds.md | 8 +- docs/reference/api/schemas.md | 6 +- docs/reference/api/workspaces.md | 6 - site/src/api/typesGenerated.ts | 3 +- site/src/pages/TaskPage/TaskApps.stories.tsx | 14 +- site/src/pages/TaskPage/TaskApps.tsx | 13 +- site/src/pages/TaskPage/TaskPage.stories.tsx | 196 +++++++++---------- site/src/pages/TaskPage/TaskPage.tsx | 4 +- 12 files changed, 121 insertions(+), 145 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e459b94b3fdaf..76a38452552d4 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -20527,7 +20527,7 @@ const docTemplate = `{ "type": "object", "properties": { "ai_task_sidebar_app_id": { - "description": "Deprecated: This field has been replaced with ` + "`" + `TaskAppID` + "`" + `", + "description": "Deprecated: This field has been replaced with ` + "`" + `Task.WorkspaceAppID` + "`" + `", "type": "string", "format": "uuid" }, @@ -20609,10 +20609,6 @@ const docTemplate = `{ } ] }, - "task_app_id": { - "type": "string", - "format": "uuid" - }, "template_version_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8eef9dfb8e0d7..4aec870e77587 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -18861,7 +18861,7 @@ "type": "object", "properties": { "ai_task_sidebar_app_id": { - "description": "Deprecated: This field has been replaced with `TaskAppID`", + "description": "Deprecated: This field has been replaced with `Task.WorkspaceAppID`", "type": "string", "format": "uuid" }, @@ -18939,10 +18939,6 @@ } ] }, - "task_app_id": { - "type": "string", - "format": "uuid" - }, "template_version_id": { "type": "string", "format": "uuid" diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 1e3020376041b..7918a334339d9 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -1219,7 +1219,6 @@ func (api *API) convertWorkspaceBuild( TemplateVersionPresetID: presetID, HasAITask: hasAITask, AITaskSidebarAppID: taskAppID, - TaskAppID: taskAppID, HasExternalAgent: hasExternalAgent, }, nil } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index fee4c114b7eae..8c38bd5c6469b 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -89,9 +89,8 @@ type WorkspaceBuild struct { MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"` TemplateVersionPresetID *uuid.UUID `json:"template_version_preset_id" format:"uuid"` HasAITask *bool `json:"has_ai_task,omitempty"` - // Deprecated: This field has been replaced with `TaskAppID` + // Deprecated: This field has been replaced with `Task.WorkspaceAppID` AITaskSidebarAppID *uuid.UUID `json:"ai_task_sidebar_app_id,omitempty" format:"uuid"` - TaskAppID *uuid.UUID `json:"task_app_id,omitempty" format:"uuid"` HasExternalAgent *bool `json:"has_external_agent,omitempty"` } diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index ea207f84eab39..82b7cb8365a3e 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -222,7 +222,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -464,7 +463,6 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -1197,7 +1195,6 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -1512,7 +1509,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -1540,7 +1536,7 @@ Status Code **200** | Name | Type | Required | Restrictions | Description | |----------------------------------|--------------------------------------------------------------------------------------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `[array item]` | array | false | | | -| `» ai_task_sidebar_app_id` | string(uuid) | false | | Deprecated: This field has been replaced with `TaskAppID` | +| `» ai_task_sidebar_app_id` | string(uuid) | false | | Deprecated: This field has been replaced with `Task.WorkspaceAppID` | | `» build_number` | integer | false | | | | `» created_at` | string(date-time) | false | | | | `» daily_cost` | integer | false | | | @@ -1691,7 +1687,6 @@ Status Code **200** | `»» type` | string | false | | | | `»» workspace_transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | | `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | -| `» task_app_id` | string(uuid) | false | | | | `» template_version_id` | string(uuid) | false | | | | `» template_version_name` | string | false | | | | `» template_version_preset_id` | string(uuid) | false | | | @@ -2013,7 +2008,6 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 4317324f002be..5d6a66efafee1 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -10164,7 +10164,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -11339,7 +11338,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -11357,7 +11355,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | Name | Type | Required | Restrictions | Description | |------------------------------|-------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------| -| `ai_task_sidebar_app_id` | string | false | | Deprecated: This field has been replaced with `TaskAppID` | +| `ai_task_sidebar_app_id` | string | false | | Deprecated: This field has been replaced with `Task.WorkspaceAppID` | | `build_number` | integer | false | | | | `created_at` | string | false | | | | `daily_cost` | integer | false | | | @@ -11373,7 +11371,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | | `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | | | `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | | -| `task_app_id` | string | false | | | | `template_version_id` | string | false | | | | `template_version_name` | string | false | | | | `template_version_preset_id` | string | false | | | @@ -12163,7 +12160,6 @@ If the schedule is empty, the user will be updated to use the default schedule.| } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 3e52d9e0a2d60..4bd188df3daf5 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -277,7 +277,6 @@ of the template will be used. } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -573,7 +572,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -894,7 +892,6 @@ of the template will be used. } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -1176,7 +1173,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -1473,7 +1469,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", @@ -2029,7 +2024,6 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ } ], "status": "pending", - "task_app_id": "ca438251-3e16-4fae-b9ab-dd3c237c3735", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6d703cbcfe2d6..8d86362b06227 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -6399,10 +6399,9 @@ export interface WorkspaceBuild { readonly template_version_preset_id: string | null; readonly has_ai_task?: boolean; /** - * Deprecated: This field has been replaced with `TaskAppID` + * Deprecated: This field has been replaced with `Task.WorkspaceAppID` */ readonly ai_task_sidebar_app_id?: string; - readonly task_app_id?: string; readonly has_external_agent?: boolean; } diff --git a/site/src/pages/TaskPage/TaskApps.stories.tsx b/site/src/pages/TaskPage/TaskApps.stories.tsx index 752c52cfde423..dcce3f5949d21 100644 --- a/site/src/pages/TaskPage/TaskApps.stories.tsx +++ b/site/src/pages/TaskPage/TaskApps.stories.tsx @@ -1,5 +1,6 @@ import { MockPrimaryWorkspaceProxy, + MockTask, MockUserOwner, MockWorkspace, MockWorkspaceAgent, @@ -8,7 +9,7 @@ import { } from "testHelpers/entities"; import { withAuthProvider, withProxyProvider } from "testHelpers/storybook"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import type { Workspace, WorkspaceApp } from "api/typesGenerated"; +import type { Task, Workspace, WorkspaceApp } from "api/typesGenerated"; import { getPreferredProxy } from "contexts/ProxyContext"; import kebabCase from "lodash/kebabCase"; import { TaskApps } from "./TaskApps"; @@ -19,6 +20,11 @@ const mockExternalApp: WorkspaceApp = { health: "healthy", }; +const mockTask: Task = { + ...MockTask, + workspace_app_id: null, +}; + const meta: Meta = { title: "pages/TaskPage/TaskApps", component: TaskApps, @@ -33,24 +39,28 @@ type Story = StoryObj; export const NoEmbeddedApps: Story = { args: { + task: mockTask, workspace: mockWorkspaceWithApps([]), }, }; export const WithExternalAppsOnly: Story = { args: { + task: mockTask, workspace: mockWorkspaceWithApps([mockExternalApp]), }, }; export const WithEmbeddedApps: Story = { args: { + task: mockTask, workspace: mockWorkspaceWithApps([mockEmbeddedApp()]), }, }; export const WithMixedApps: Story = { args: { + task: mockTask, workspace: mockWorkspaceWithApps([mockEmbeddedApp(), mockExternalApp]), }, }; @@ -69,6 +79,7 @@ export const WithWildcardWarning: Story = { user: MockUserOwner, }, args: { + task: mockTask, workspace: mockWorkspaceWithApps([ { ...mockEmbeddedApp(), @@ -80,6 +91,7 @@ export const WithWildcardWarning: Story = { export const WithManyEmbeddedApps: Story = { args: { + task: mockTask, workspace: mockWorkspaceWithApps([ mockEmbeddedApp("Code Server"), mockEmbeddedApp("Jupyter Notebook"), diff --git a/site/src/pages/TaskPage/TaskApps.tsx b/site/src/pages/TaskPage/TaskApps.tsx index 2f05145bca46a..59ada2181119b 100644 --- a/site/src/pages/TaskPage/TaskApps.tsx +++ b/site/src/pages/TaskPage/TaskApps.tsx @@ -1,4 +1,4 @@ -import type { Workspace } from "api/typesGenerated"; +import type { Task, Workspace } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { DropdownMenu, @@ -24,18 +24,17 @@ import { docs } from "utils/docs"; import { TaskAppIFrame, TaskIframe } from "./TaskAppIframe"; type TaskAppsProps = { + task: Task; workspace: Workspace; }; const TERMINAL_TAB_ID = "terminal"; -export const TaskApps: FC = ({ workspace }) => { +export const TaskApps: FC = ({ task, workspace }) => { const apps = getAllAppsWithAgent(workspace).filter( // The Chat UI app will be displayed in the sidebar, so we don't want to // show it as a web app. - (app) => - app.id !== workspace.latest_build.task_app_id && - app.health !== "disabled", + (app) => app.id !== task.workspace_app_id && app.health !== "disabled", ); const [embeddedApps, externalApps] = splitEmbeddedAndExternalApps(apps); const [activeAppId, setActiveAppId] = useState(embeddedApps.at(0)?.id); @@ -43,8 +42,8 @@ export const TaskApps: FC = ({ workspace }) => { embeddedApps.length > 0 || externalApps.length > 0; const taskAgent = apps.at(0)?.agent; const terminalHref = getTerminalHref({ - username: workspace.owner_name, - workspace: workspace.name, + username: task.owner_name, + workspace: task.workspace_name, agent: taskAgent?.name, }); const isTerminalActive = activeAppId === TERMINAL_TAB_ID; diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index 19003eb621699..df9129de7f32d 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -22,7 +22,7 @@ import { } from "testHelpers/storybook"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { API } from "api/api"; -import type { Workspace, WorkspaceApp } from "api/typesGenerated"; +import type { Task, Workspace, WorkspaceApp } from "api/typesGenerated"; import { expect, spyOn, userEvent, waitFor, within } from "storybook/test"; import { reactRouterParameters } from "storybook-addon-remix-react-router"; import TaskPage from "./TaskPage"; @@ -210,87 +210,70 @@ export const WaitingStartupScripts: Story = { export const SidebarAppNotFound: Story = { beforeEach: () => { - const workspace = mockTaskWorkspace(MockClaudeCodeApp, MockVSCodeApp); - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue({ - ...workspace, - latest_build: { - ...workspace.latest_build, - task_app_id: "non-existent-app-id", - }, + const [task, workspace] = mockTaskWithWorkspace( + MockClaudeCodeApp, + MockVSCodeApp, + ); + spyOn(API.experimental, "getTask").mockResolvedValue({ + ...task, + workspace_app_id: null, }); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, }; export const SidebarAppHealthDisabled: Story = { beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace( - { - ...MockClaudeCodeApp, - health: "disabled", - }, - MockVSCodeApp, - ), + const [task, workspace] = mockTaskWithWorkspace( + { ...MockClaudeCodeApp, health: "disabled" }, + MockVSCodeApp, ); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, }; export const SidebarAppInitializing: Story = { beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace( - { - ...MockClaudeCodeApp, - health: "initializing", - }, - MockVSCodeApp, - ), + const [task, workspace] = mockTaskWithWorkspace( + { ...MockClaudeCodeApp, health: "initializing" }, + MockVSCodeApp, ); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, }; export const SidebarAppHealthy: Story = { beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace( - { - ...MockClaudeCodeApp, - health: "healthy", - }, - MockVSCodeApp, - ), + const [task, workspace] = mockTaskWithWorkspace( + { ...MockClaudeCodeApp, health: "healthy" }, + MockVSCodeApp, ); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, }; export const SidebarAppUnhealthy: Story = { beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace( - { - ...MockClaudeCodeApp, - health: "unhealthy", - }, - MockVSCodeApp, - ), + const [task, workspace] = mockTaskWithWorkspace( + { ...MockClaudeCodeApp, health: "unhealthy" }, + MockVSCodeApp, ); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, }; const mainAppHealthStory = (health: WorkspaceApp["health"]) => ({ beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace(MockClaudeCodeApp, { - ...MockVSCodeApp, - health, - }), - ); + const [task, workspace] = mockTaskWithWorkspace(MockClaudeCodeApp, { + ...MockVSCodeApp, + health, + }); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, }); @@ -301,10 +284,12 @@ export const MainAppUnhealthy: Story = mainAppHealthStory("unhealthy"); export const Active: Story = { decorators: [withProxyProvider()], beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace(MockClaudeCodeApp, MockVSCodeApp), + const [task, workspace] = mockTaskWithWorkspace( + MockClaudeCodeApp, + MockVSCodeApp, ); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -322,10 +307,12 @@ export const Active: Story = { export const ActivePreview: Story = { decorators: [withProxyProvider()], beforeEach: () => { - spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); - spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - mockTaskWorkspace(MockClaudeCodeApp, MockVSCodeApp), + const [task, workspace] = mockTaskWithWorkspace( + MockClaudeCodeApp, + MockVSCodeApp, ); + spyOn(API.experimental, "getTask").mockResolvedValue(task); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -454,51 +441,56 @@ export const WorkspaceStartFailureWithDialog: Story = { }, }; -function mockTaskWorkspace( +function mockTaskWithWorkspace( sidebarApp: WorkspaceApp, activeApp: WorkspaceApp, -): Workspace { - return { - ...MockWorkspace, - latest_build: { - ...MockWorkspace.latest_build, - has_ai_task: true, - task_app_id: sidebarApp.id, - resources: [ - { - ...MockWorkspaceResource, - agents: [ - { - ...MockWorkspaceAgentReady, - apps: [ - sidebarApp, - activeApp, - { - ...MockWorkspaceApp, - slug: "zed", - id: "zed", - display_name: "Zed", - icon: "/icon/zed.svg", - health: "healthy", - }, - { - ...MockWorkspaceApp, - slug: "preview", - id: "preview", - display_name: "Preview", - health: "healthy", - }, - { - ...MockWorkspaceApp, - slug: "disabled", - id: "disabled", - display_name: "Disabled", - }, - ], - }, - ], - }, - ], +): [Task, Workspace] { + return [ + { + ...MockTask, + workspace_app_id: sidebarApp.id, + }, + { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + has_ai_task: true, + resources: [ + { + ...MockWorkspaceResource, + agents: [ + { + ...MockWorkspaceAgentReady, + apps: [ + sidebarApp, + activeApp, + { + ...MockWorkspaceApp, + slug: "zed", + id: "zed", + display_name: "Zed", + icon: "/icon/zed.svg", + health: "healthy", + }, + { + ...MockWorkspaceApp, + slug: "preview", + id: "preview", + display_name: "Preview", + health: "healthy", + }, + { + ...MockWorkspaceApp, + slug: "disabled", + id: "disabled", + display_name: "Disabled", + }, + ], + }, + ], + }, + ], + }, }, - }; + ]; } diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 17cdc8b6861d7..793a3b44b12eb 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -149,7 +149,7 @@ const TaskPage = () => { content = ; } else { const chatApp = getAllAppsWithAgent(workspace).find( - (app) => app.id === workspace.latest_build.task_app_id, + (app) => app.id === task.workspace_app_id, ); content = ( @@ -174,7 +174,7 @@ const TaskPage = () => {
- + ); From cb4ea1f3970518d620bde14ea511bf1c4129327a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 3 Nov 2025 09:00:42 +0000 Subject: [PATCH 09/12] fix(coderd): fix audit log resource link for tasks (#20545) (#20547) Existing task audit log links were incorrect. As audit log links are generated on-the-fly, this does not require backfill. (cherry picked from commit 566146af72a048adcff88a6390205df9181686bf) --- coderd/audit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/audit.go b/coderd/audit.go index e43ed1c5128ec..3a3237a9fed50 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -509,11 +509,11 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit if err != nil { return "" } - workspace, err := api.Database.GetWorkspaceByID(ctx, task.WorkspaceID.UUID) + user, err := api.Database.GetUserByID(ctx, task.OwnerID) if err != nil { return "" } - return fmt.Sprintf("/tasks/%s/%s", workspace.OwnerName, task.Name) + return fmt.Sprintf("/tasks/%s/%s", user.Username, task.ID) default: return "" From d82ba7e3a4e18ad85fda871edc135cf851d58f71 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 3 Nov 2025 09:01:57 +0000 Subject: [PATCH 10/12] fix(coderd): disallow POSTing a workspace build on a deleted workspace (#20584) (#20586) - Adds a check on /api/v2/workspacebuilds to disallow creating a START or STOP build if the workspace is deleted. - DELETEs are still allowed. (cherry picked from commit 38017010cec19bb0fc5479a7debcc341a5a63a9d) --- coderd/workspacebuilds.go | 9 +++++ coderd/workspacebuilds_test.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 7918a334339d9..d064a0ef3f574 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -335,6 +335,15 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } + // We want to allow a delete build for a deleted workspace, but not a start or stop build. + if workspace.Deleted && createBuild.Transition != codersdk.WorkspaceTransitionDelete { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: fmt.Sprintf("Cannot %s a deleted workspace!", createBuild.Transition), + Detail: "This workspace has been deleted and cannot be modified.", + }) + return + } + apiBuild, err := api.postWorkspaceBuildsInternal( ctx, apiKey, diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index f857296db1a5c..d0ab64b1aeb32 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1840,6 +1840,68 @@ func TestPostWorkspaceBuild(t *testing.T) { require.NoError(t, err) require.Equal(t, codersdk.BuildReasonDashboard, build.Reason) }) + t.Run("DeletedWorkspace", func(t *testing.T) { + t.Parallel() + + // Given: a workspace that has already been deleted + var ( + ctx = testutil.Context(t, testutil.WaitShort) + logger = slogtest.Make(t, &slogtest.Options{}).Leveled(slog.LevelError) + adminClient, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{ + Logger: &logger, + }) + admin = coderdtest.CreateFirstUser(t, adminClient) + workspaceOwnerClient, member1 = coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID) + otherMemberClient, _ = coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID) + ws = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{OwnerID: member1.ID, OrganizationID: admin.OrganizationID}). + Seed(database.WorkspaceBuild{Transition: database.WorkspaceTransitionDelete}). + Do() + ) + + // This needs to be done separately as provisionerd handles marking the workspace as deleted + // and we're skipping provisionerd here for speed. + require.NoError(t, db.UpdateWorkspaceDeletedByID(dbauthz.AsProvisionerd(ctx), database.UpdateWorkspaceDeletedByIDParams{ + ID: ws.Workspace.ID, + Deleted: true, + })) + + // Assert test invariant: Workspace should be deleted + dbWs, err := db.GetWorkspaceByID(dbauthz.AsProvisionerd(ctx), ws.Workspace.ID) + require.NoError(t, err) + require.True(t, dbWs.Deleted, "workspace should be deleted") + + for _, tc := range []struct { + user *codersdk.Client + tr codersdk.WorkspaceTransition + expectStatus int + }{ + // You should not be allowed to mess with a workspace you don't own, regardless of its deleted state. + {otherMemberClient, codersdk.WorkspaceTransitionStart, http.StatusNotFound}, + {otherMemberClient, codersdk.WorkspaceTransitionStop, http.StatusNotFound}, + {otherMemberClient, codersdk.WorkspaceTransitionDelete, http.StatusNotFound}, + // Starting or stopping a workspace is not allowed when it is deleted. + {workspaceOwnerClient, codersdk.WorkspaceTransitionStart, http.StatusConflict}, + {workspaceOwnerClient, codersdk.WorkspaceTransitionStop, http.StatusConflict}, + // We allow a delete just in case a retry is required. In most cases, this will be a no-op. + // Note: this is the last test case because it will change the state of the workspace. + {workspaceOwnerClient, codersdk.WorkspaceTransitionDelete, http.StatusOK}, + } { + // When: we create a workspace build with the given transition + _, err = tc.user.CreateWorkspaceBuild(ctx, ws.Workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: tc.tr, + }) + + // Then: we allow ONLY a delete build for a deleted workspace. + if tc.expectStatus < http.StatusBadRequest { + require.NoError(t, err, "creating a %s build for a deleted workspace should not error", tc.tr) + } else { + var apiError *codersdk.Error + require.Error(t, err, "creating a %s build for a deleted workspace should return an error", tc.tr) + require.ErrorAs(t, err, &apiError) + require.Equal(t, tc.expectStatus, apiError.StatusCode()) + } + } + }) } func TestWorkspaceBuildTimings(t *testing.T) { From fa43ea8e688bfcf8a7591936274a54d5a0058497 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 3 Nov 2025 09:41:16 +0000 Subject: [PATCH 11/12] chore: remove brazil fly.io proxy (#20601) (#20645) (cherry picked from commit 7182c53df7648cd7db8551629226c4a0a1cc8559) Co-authored-by: Dean Sheather --- .github/fly-wsproxies/sao-paulo-coder.toml | 34 ---------------------- .github/workflows/deploy.yaml | 2 -- 2 files changed, 36 deletions(-) delete mode 100644 .github/fly-wsproxies/sao-paulo-coder.toml diff --git a/.github/fly-wsproxies/sao-paulo-coder.toml b/.github/fly-wsproxies/sao-paulo-coder.toml deleted file mode 100644 index b6c9b964631ef..0000000000000 --- a/.github/fly-wsproxies/sao-paulo-coder.toml +++ /dev/null @@ -1,34 +0,0 @@ -app = "sao-paulo-coder" -primary_region = "gru" - -[experimental] - entrypoint = ["/bin/sh", "-c", "CODER_DERP_SERVER_RELAY_URL=\"http://[${FLY_PRIVATE_IP}]:3000\" /opt/coder wsproxy server"] - auto_rollback = true - -[build] - image = "ghcr.io/coder/coder-preview:main" - -[env] - CODER_ACCESS_URL = "https://sao-paulo.fly.dev.coder.com" - CODER_HTTP_ADDRESS = "0.0.0.0:3000" - CODER_PRIMARY_ACCESS_URL = "https://dev.coder.com" - CODER_WILDCARD_ACCESS_URL = "*--apps.sao-paulo.fly.dev.coder.com" - CODER_VERBOSE = "true" - -[http_service] - internal_port = 3000 - force_https = true - auto_stop_machines = true - auto_start_machines = true - min_machines_running = 0 - -# Ref: https://fly.io/docs/reference/configuration/#http_service-concurrency -[http_service.concurrency] - type = "requests" - soft_limit = 50 - hard_limit = 100 - -[[vm]] - cpu_kind = "shared" - cpus = 2 - memory_mb = 512 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 30d9e384149fa..6ea750a11caac 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -163,12 +163,10 @@ jobs: run: | flyctl deploy --image "$IMAGE" --app paris-coder --config ./.github/fly-wsproxies/paris-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_PARIS" --yes flyctl deploy --image "$IMAGE" --app sydney-coder --config ./.github/fly-wsproxies/sydney-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SYDNEY" --yes - flyctl deploy --image "$IMAGE" --app sao-paulo-coder --config ./.github/fly-wsproxies/sao-paulo-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SAO_PAULO" --yes flyctl deploy --image "$IMAGE" --app jnb-coder --config ./.github/fly-wsproxies/jnb-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_JNB" --yes env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} IMAGE: ${{ inputs.image }} TOKEN_PARIS: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }} TOKEN_SYDNEY: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }} - TOKEN_SAO_PAULO: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }} TOKEN_JNB: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }} From 563612eb3b3b585a729cabc3555fda5f34dbbb71 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 3 Nov 2025 10:04:41 +0000 Subject: [PATCH 12/12] fix: delete related task when deleting workspace (#20567) (#20585) * Instead of prompting the user to start a deleted workspace (which is silly), prompt them to create a new task instead. * Adds a warning dialog when deleting a workspace related to a task * Updates provisionerdserver to delete the related task if a workspace is related to a task (cherry picked from commit 73dedcc76568436c9c5c6091b849e1093e4f2f85) --- coderd/aitasks_test.go | 52 ++++++++++++++++--- coderd/database/dbauthz/dbauthz.go | 4 +- .../provisionerdserver/provisionerdserver.go | 8 +++ .../WorkspaceDeleteDialog.stories.tsx | 12 ++++- .../WorkspaceDeleteDialog.tsx | 22 +++++++- site/src/pages/TaskPage/TaskPage.stories.tsx | 10 ++++ site/src/pages/TaskPage/TaskPage.tsx | 22 +++++++- 7 files changed, 118 insertions(+), 12 deletions(-) diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index d3b5e240d8301..5fb24d7b1f546 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -478,10 +478,10 @@ func TestTasks(t *testing.T) { } }) - t.Run("NoWorkspace", func(t *testing.T) { + t.Run("DeletedWorkspace", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) template := createAITemplate(t, client, user) ctx := testutil.Context(t, testutil.WaitLong) @@ -495,14 +495,54 @@ func TestTasks(t *testing.T) { ws, err := client.Workspace(ctx, task.WorkspaceID.UUID) require.NoError(t, err) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) - // Delete the task workspace - coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionDelete) - // We should still be able to fetch the task after deleting its workspace + + // Mark the workspace as deleted directly in the database, bypassing provisionerd. + require.NoError(t, db.UpdateWorkspaceDeletedByID(dbauthz.AsProvisionerd(ctx), database.UpdateWorkspaceDeletedByIDParams{ + ID: ws.ID, + Deleted: true, + })) + // We should still be able to fetch the task if its workspace was deleted. + // Provisionerdserver will attempt delete the related task when deleting a workspace. + // This test ensures that we can still handle the case where, for some reason, the + // task has not been marked as deleted, but the workspace has. task, err = exp.TaskByID(ctx, task.ID) - require.NoError(t, err, "fetching a task should still work after deleting its related workspace") + require.NoError(t, err, "fetching a task should still work if its related workspace is deleted") err = exp.DeleteTask(ctx, task.OwnerID.String(), task.ID) require.NoError(t, err, "should be possible to delete a task with no workspace") }) + + t.Run("DeletingTaskWorkspaceDeletesTask", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + template := createAITemplate(t, client, user) + + ctx := testutil.Context(t, testutil.WaitLong) + + exp := codersdk.NewExperimentalClient(client) + task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Input: "delete me", + }) + require.NoError(t, err) + require.True(t, task.WorkspaceID.Valid, "task should have a workspace ID") + ws, err := client.Workspace(ctx, task.WorkspaceID.UUID) + require.NoError(t, err) + if assert.True(t, ws.TaskID.Valid, "task id should be set on workspace") { + assert.Equal(t, task.ID, ws.TaskID.UUID, "workspace task id should match") + } + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + + // When; the task workspace is deleted + coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionDelete) + // Then: the task associated with the workspace is also deleted + _, err = exp.TaskByID(ctx, task.ID) + require.Error(t, err, "expected an error fetching the task") + var sdkErr *codersdk.Error + require.ErrorAs(t, err, &sdkErr, "expected a codersdk.Error") + require.Equal(t, http.StatusNotFound, sdkErr.StatusCode()) + }) }) t.Run("Send", func(t *testing.T) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 1b2a6a5d97590..8066ebd0479a1 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -219,8 +219,8 @@ var ( rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, - // Provisionerd needs to read and update tasks associated with workspaces. - rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate}, + // Provisionerd needs to read, update, and delete tasks associated with workspaces. + rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, rbac.ResourceApiKey.Type: {policy.WildcardSymbol}, // When org scoped provisioner credentials are implemented, // this can be reduced to read a specific org. diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index bf7741bdc260f..2e00796d1cd64 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2278,6 +2278,14 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro if err != nil { return xerrors.Errorf("update workspace deleted: %w", err) } + if workspace.TaskID.Valid { + if _, err := db.DeleteTask(ctx, database.DeleteTaskParams{ + ID: workspace.TaskID.UUID, + DeletedAt: dbtime.Now(), + }); err != nil && !errors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("delete task related to workspace: %w", err) + } + } return nil }, nil) diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx index b5fcd44b7c9c8..7debfb1ce9d8c 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx @@ -1,4 +1,8 @@ -import { MockFailedWorkspace, MockWorkspace } from "testHelpers/entities"; +import { + MockFailedWorkspace, + MockTaskWorkspace, + MockWorkspace, +} from "testHelpers/entities"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { daysAgo } from "utils/time"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; @@ -45,3 +49,9 @@ export const UnhealthyAdminView: Story = { canDeleteFailedWorkspace: true, }, }; + +export const WithTask: Story = { + args: { + workspace: MockTaskWorkspace, + }, +}; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx index 2cfb74f2765c3..245f95c0f74d8 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx @@ -56,6 +56,8 @@ export const WorkspaceDeleteDialog: FC = ({ (workspace.latest_build.status === "failed" || workspace.latest_build.status === "canceled"); + const hasTask = !!workspace.task_id; + return ( = ({ "data-testid": "delete-dialog-name-confirmation", }} /> + {hasTask && ( +
+
+

This workspace is related to a task

+ + Deleting this workspace will also delete{" "} + + this task + + . + +
+
+ )} {canOrphan && ( -
+
({ + warnContainer: (theme) => ({ marginTop: 24, display: "flex", backgroundColor: theme.roles.danger.background, diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index df9129de7f32d..b247a2a16b377 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -1,4 +1,5 @@ import { + MockDeletedWorkspace, MockFailedWorkspace, MockStartingWorkspace, MockStoppedWorkspace, @@ -169,6 +170,15 @@ export const TerminatedBuildWithStatus: Story = { }, }; +export const DeletedWorkspace: Story = { + beforeEach: () => { + spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( + MockDeletedWorkspace, + ); + }, +}; + export const WaitingStartupScripts: Story = { beforeEach: () => { spyOn(API.experimental, "getTask").mockResolvedValue(MockTask); diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index 793a3b44b12eb..f57d0e9f1772c 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -221,7 +221,27 @@ const WorkspaceNotRunning: FC = ({ workspace }) => { ? mutateStartWorkspace.error : undefined; - return ( + const deleted = workspace.latest_build?.transition === ("delete" as const); + + return deleted ? ( + +
+
+

+ Task workspace was deleted. +

+ + This task cannot be resumed. Delete this task and create a new one. + + +
+
+
+ ) : (