syntax = "proto3";

package tars.admin.v1;

import "buf/validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "tars/auth/v1/auth_options.proto";
import "tars/docs/v1/docs.proto";

service AdminService {
  // Health Check

  // Ping endpoint for health checks and authentication testing
  rpc Ping(PingRequest) returns (PingResponse) {
    option (google.api.http) = {get: "/admin/v1/ping"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Workspace Management

  // Create workspace
  rpc CreateWorkspace(CreateWorkspaceRequest) returns (CreateWorkspaceResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {
      post: "/admin/v1/workspaces"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // List workspaces
  rpc ListWorkspaces(ListWorkspacesRequest) returns (ListWorkspacesResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {get: "/admin/v1/workspaces"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Get workspace
  rpc GetWorkspace(GetWorkspaceRequest) returns (GetWorkspaceResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {get: "/admin/v1/workspaces/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Update workspace
  rpc UpdateWorkspace(UpdateWorkspaceRequest) returns (UpdateWorkspaceResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {
      patch: "/admin/v1/workspaces/{id}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Set the customer-facing gateway URL for a workspace data plane.
  // Writes system_settings.dataplane-url:<workspace_id> and optionally fires a PROXY_URL config event.
  rpc SetWorkspaceURL(SetWorkspaceURLRequest) returns (SetWorkspaceURLResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {
      post: "/admin/v1/workspaces/{id}/url"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Assign workspace to a project (or clear back to default project by omitting project_id)
  rpc SetWorkspaceProject(SetWorkspaceProjectRequest) returns (SetWorkspaceProjectResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {
      post: "/admin/v1/workspaces/{id}/project"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Delete workspace (soft delete)
  rpc DeleteWorkspace(DeleteWorkspaceRequest) returns (DeleteWorkspaceResponse) {
    option (tars.docs.v1.group) = "Workspaces";
    option (google.api.http) = {delete: "/admin/v1/workspaces/{id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Service Account Management

  // Create service account
  rpc CreateServiceAccount(CreateServiceAccountRequest) returns (CreateServiceAccountResponse) {
    option (tars.docs.v1.group) = "Service Accounts";
    option (google.api.http) = {
      post: "/admin/v1/service-accounts"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // List service accounts
  rpc ListServiceAccounts(ListServiceAccountsRequest) returns (ListServiceAccountsResponse) {
    option (tars.docs.v1.group) = "Service Accounts";
    option (google.api.http) = {get: "/admin/v1/service-accounts"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Get service account
  rpc GetServiceAccount(GetServiceAccountRequest) returns (GetServiceAccountResponse) {
    option (tars.docs.v1.group) = "Service Accounts";
    option (google.api.http) = {get: "/admin/v1/service-accounts/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Revoke service account
  rpc RevokeServiceAccount(RevokeServiceAccountRequest) returns (RevokeServiceAccountResponse) {
    option (tars.docs.v1.group) = "Service Accounts";
    option (google.api.http) = {
      post: "/admin/v1/service-accounts/{id}:revoke"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Restore service account
  // Restores a revoked service account to active status.
  // Only one active service account per customer_id + workspace_id is allowed.
  rpc RestoreServiceAccount(RestoreServiceAccountRequest) returns (RestoreServiceAccountResponse) {
    option (tars.docs.v1.group) = "Service Accounts";
    option (google.api.http) = {
      post: "/admin/v1/service-accounts/{id}:restore"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // User Token Management

  // Create user token
  // Generates a JWT token for user authentication with gatekeeper.
  rpc CreateUserToken(CreateUserTokenRequest) returns (CreateUserTokenResponse) {
    option (tars.docs.v1.group) = "User Tokens";
    option (google.api.http) = {
      post: "/admin/v1/user-tokens"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // List user tokens
  rpc ListUserTokens(ListUserTokensRequest) returns (ListUserTokensResponse) {
    option (tars.docs.v1.group) = "User Tokens";
    option (google.api.http) = {get: "/admin/v1/user-tokens"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Get user token
  rpc GetUserToken(GetUserTokenRequest) returns (GetUserTokenResponse) {
    option (tars.docs.v1.group) = "User Tokens";
    option (google.api.http) = {get: "/admin/v1/user-tokens/{jti}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Revoke user token
  // Revokes a user token by its JTI (JWT ID).
  rpc RevokeUserToken(RevokeUserTokenRequest) returns (RevokeUserTokenResponse) {
    option (tars.docs.v1.group) = "User Tokens";
    option (google.api.http) = {
      post: "/admin/v1/user-tokens/{jti}:revoke"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Telemetry Config Management

  // Create telemetry config
  rpc CreateTelemetryConfig(CreateTelemetryConfigRequest) returns (CreateTelemetryConfigResponse) {
    option (tars.docs.v1.group) = "Telemetry";
    option (google.api.http) = {
      post: "/admin/v1/telemetry-configs"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // List telemetry configs
  rpc ListTelemetryConfigs(ListTelemetryConfigsRequest) returns (ListTelemetryConfigsResponse) {
    option (tars.docs.v1.group) = "Telemetry";
    option (google.api.http) = {get: "/admin/v1/telemetry-configs"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Get telemetry config
  rpc GetTelemetryConfig(GetTelemetryConfigRequest) returns (GetTelemetryConfigResponse) {
    option (tars.docs.v1.group) = "Telemetry";
    option (google.api.http) = {get: "/admin/v1/telemetry-configs/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Update telemetry config
  rpc UpdateTelemetryConfig(UpdateTelemetryConfigRequest) returns (UpdateTelemetryConfigResponse) {
    option (tars.docs.v1.group) = "Telemetry";
    option (google.api.http) = {
      patch: "/admin/v1/telemetry-configs/{id}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Delete telemetry config
  rpc DeleteTelemetryConfig(DeleteTelemetryConfigRequest) returns (DeleteTelemetryConfigResponse) {
    option (tars.docs.v1.group) = "Telemetry";
    option (google.api.http) = {delete: "/admin/v1/telemetry-configs/{id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Customer Management

  rpc CreateCustomer(CreateCustomerRequest) returns (CreateCustomerResponse) {
    option (tars.docs.v1.group) = "Customers";
    option (google.api.http) = {
      post: "/admin/v1/customers"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc ListCustomers(ListCustomersRequest) returns (ListCustomersResponse) {
    option (tars.docs.v1.group) = "Customers";
    option (google.api.http) = {get: "/admin/v1/customers"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc GetCustomer(GetCustomerRequest) returns (GetCustomerResponse) {
    option (tars.docs.v1.group) = "Customers";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc UpdateCustomer(UpdateCustomerRequest) returns (UpdateCustomerResponse) {
    option (tars.docs.v1.group) = "Customers";
    option (google.api.http) = {
      patch: "/admin/v1/customers/{customer_id}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc DeleteCustomer(DeleteCustomerRequest) returns (DeleteCustomerResponse) {
    option (tars.docs.v1.group) = "Customers";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Project Management

  rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse) {
    option (tars.docs.v1.group) = "Projects";
    option (google.api.http) = {
      post: "/admin/v1/customers/{customer_id}/projects"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) {
    option (tars.docs.v1.group) = "Projects";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc GetProject(GetProjectRequest) returns (GetProjectResponse) {
    option (tars.docs.v1.group) = "Projects";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects/{project_id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc UpdateProject(UpdateProjectRequest) returns (UpdateProjectResponse) {
    option (tars.docs.v1.group) = "Projects";
    option (google.api.http) = {
      patch: "/admin/v1/customers/{customer_id}/projects/{project_id}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc DeleteProject(DeleteProjectRequest) returns (DeleteProjectResponse) {
    option (tars.docs.v1.group) = "Projects";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}/projects/{project_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Project Member Management

  rpc AddProjectMember(AddProjectMemberRequest) returns (AddProjectMemberResponse) {
    option (tars.docs.v1.group) = "Project Members";
    option (google.api.http) = {
      post: "/admin/v1/customers/{customer_id}/projects/{project_id}/members"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc ListProjectMembers(ListProjectMembersRequest) returns (ListProjectMembersResponse) {
    option (tars.docs.v1.group) = "Project Members";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects/{project_id}/members"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc UpdateProjectMember(UpdateProjectMemberRequest) returns (UpdateProjectMemberResponse) {
    option (tars.docs.v1.group) = "Project Members";
    option (google.api.http) = {
      patch: "/admin/v1/customers/{customer_id}/projects/{project_id}/members/{user_id}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc RemoveProjectMember(RemoveProjectMemberRequest) returns (RemoveProjectMemberResponse) {
    option (tars.docs.v1.group) = "Project Members";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}/projects/{project_id}/members/{user_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Model Catalog

  rpc ListModels(ListModelsRequest) returns (ListModelsResponse) {
    option (tars.docs.v1.group) = "Models";
    option (google.api.http) = {get: "/admin/v1/models"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc GetModel(GetModelRequest) returns (GetModelResponse) {
    option (tars.docs.v1.group) = "Models";
    option (google.api.http) = {get: "/admin/v1/models/{model_id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Project Model Assignment

  rpc AddProjectModel(AddProjectModelRequest) returns (AddProjectModelResponse) {
    option (tars.docs.v1.group) = "Project Models";
    option (google.api.http) = {
      post: "/admin/v1/customers/{customer_id}/projects/{project_id}/models"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc ListProjectModels(ListProjectModelsRequest) returns (ListProjectModelsResponse) {
    option (tars.docs.v1.group) = "Project Models";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects/{project_id}/models"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc RemoveProjectModel(RemoveProjectModelRequest) returns (RemoveProjectModelResponse) {
    option (tars.docs.v1.group) = "Project Models";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}/projects/{project_id}/models/{model_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Project Provider Assignment

  rpc AddProjectProvider(AddProjectProviderRequest) returns (AddProjectProviderResponse) {
    option (tars.docs.v1.group) = "Project Providers";
    option (google.api.http) = {
      post: "/admin/v1/customers/{customer_id}/projects/{project_id}/providers"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc ListProjectProviders(ListProjectProvidersRequest) returns (ListProjectProvidersResponse) {
    option (tars.docs.v1.group) = "Project Providers";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects/{project_id}/providers"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc RemoveProjectProvider(RemoveProjectProviderRequest) returns (RemoveProjectProviderResponse) {
    option (tars.docs.v1.group) = "Project Providers";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}/projects/{project_id}/providers/{provider_name}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Project MCP Server Assignment

  rpc AddProjectMcpServer(AddProjectMcpServerRequest) returns (AddProjectMcpServerResponse) {
    option (tars.docs.v1.group) = "Project MCP Servers";
    option (google.api.http) = {
      post: "/admin/v1/customers/{customer_id}/projects/{project_id}/mcp-servers"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc ListProjectMcpServers(ListProjectMcpServersRequest) returns (ListProjectMcpServersResponse) {
    option (tars.docs.v1.group) = "Project MCP Servers";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects/{project_id}/mcp-servers"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc RemoveProjectMcpServer(RemoveProjectMcpServerRequest) returns (RemoveProjectMcpServerResponse) {
    option (tars.docs.v1.group) = "Project MCP Servers";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}/projects/{project_id}/mcp-servers/{server_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // Project Settings

  rpc GetProjectSettings(GetProjectSettingsRequest) returns (GetProjectSettingsResponse) {
    option (tars.docs.v1.group) = "Project Settings";
    option (google.api.http) = {get: "/admin/v1/customers/{customer_id}/projects/{project_id}/settings"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc UpsertProjectSetting(UpsertProjectSettingRequest) returns (UpsertProjectSettingResponse) {
    option (tars.docs.v1.group) = "Project Settings";
    option (google.api.http) = {
      put: "/admin/v1/customers/{customer_id}/projects/{project_id}/settings/{setting_name}"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  rpc DeleteProjectSetting(DeleteProjectSettingRequest) returns (DeleteProjectSettingResponse) {
    option (tars.docs.v1.group) = "Project Settings";
    option (google.api.http) = {delete: "/admin/v1/customers/{customer_id}/projects/{project_id}/settings/{setting_name}"};
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }
}

// ===== Health Check Messages =====

message PingRequest {}

message PingResponse {
  // Human-readable status message (e.g. "pong").
  string message = 1;
  // Server time when the ping was handled.
  google.protobuf.Timestamp timestamp = 2;
}

// ===== Workspace Messages =====

message CreateWorkspaceRequest {
  // Customer (tenant) slug that will own the workspace.
  string customer_id = 1 [(buf.validate.field).string = {
    pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
    min_len: 1
    max_len: 63
  }];

  // Caller-chosen workspace slug, unique within the customer.
  string workspace_id = 2 [(buf.validate.field).string = {
    pattern: "^[a-zA-Z0-9-_.]+$"
    min_len: 1
    max_len: 253
  }];

  // Optional human-readable display name.
  optional string name = 3 [(buf.validate.field).string.max_len = 255];
  // Optional free-text description.
  optional string description = 4 [(buf.validate.field).string.max_len = 1000];
  // Optional user-defined key/value labels for organization/filtering.
  map<string, string> labels = 5 [(buf.validate.field).map = {
    max_pairs: 50
    keys: {
      string: {
        pattern: "^[a-z0-9]([a-z0-9-_.]*[a-z0-9])?$"
        max_len: 63
      }
    }
    values: {
      string: {max_len: 255}
    }
  }];

  // Optional project this workspace belongs to. Empty = default project.
  optional string project_id = 6 [(buf.validate.field).string.max_len = 253];
}

message CreateWorkspaceResponse {
  // The newly created workspace.
  Workspace workspace = 1;
}

message ListWorkspacesRequest {
  // Optional filter: only return workspaces owned by this customer slug.
  optional string customer_id = 1 [(buf.validate.field).string = {
    pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
    max_len: 63
  }];

  // Max workspaces to return per page (0 = server default).
  optional int32 page_size = 2 [(buf.validate.field).int32 = {
    gte: 0
    lte: 1000
  }];

  // Opaque cursor from a previous response's next_page_token.
  optional string page_token = 3;
}

message ListWorkspacesResponse {
  // Page of matching workspaces.
  repeated Workspace workspaces = 1;
  // Cursor for the next page; empty when there are no more results.
  string next_page_token = 2;
}

message GetWorkspaceRequest {
  // Workspace UUID (the internal `id`, not the customer-chosen workspace_id).
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
}

message GetWorkspaceResponse {
  // The requested workspace.
  Workspace workspace = 1;
}

message UpdateWorkspaceRequest {
  // Workspace UUID to update.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];

  // New display name; unset leaves the existing value.
  optional string name = 2 [(buf.validate.field).string.max_len = 255];
  // New description; unset leaves the existing value.
  optional string description = 3 [(buf.validate.field).string.max_len = 1000];
  // Replacement set of labels.
  map<string, string> labels = 4 [(buf.validate.field).map = {
    max_pairs: 50
    keys: {
      string: {
        pattern: "^[a-z0-9]([a-z0-9-_.]*[a-z0-9])?$"
        max_len: 63
      }
    }
    values: {
      string: {max_len: 255}
    }
  }];
}

message UpdateWorkspaceResponse {
  // The updated workspace.
  Workspace workspace = 1;
}

message SetWorkspaceProjectRequest {
  // Workspace UUID to reassign.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
  // project_id to assign. Omit or set to empty string to clear back to the default project (NULL).
  optional string project_id = 2 [(buf.validate.field).string.max_len = 255];
}

message SetWorkspaceProjectResponse {
  // The workspace with its updated project assignment.
  Workspace workspace = 1;
}

message SetWorkspaceURLRequest {
  // Workspace UUID whose data-plane URL is being set.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
  // url is the full https:// URL of the data plane's customer-facing gateway endpoint.
  string url = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 2048
  }];
  // trigger_update fires a PROXY_URL config event to the data plane after persisting the URL.
  bool trigger_update = 3;
}

message SetWorkspaceURLResponse {
  // The workspace reflecting the new data-plane URL.
  Workspace workspace = 1;
}

message DeleteWorkspaceRequest {
  // Workspace UUID to soft-delete.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
}

message DeleteWorkspaceResponse {
  // True if the workspace was deleted.
  bool success = 1;
  // When the soft delete took effect.
  google.protobuf.Timestamp deleted_at = 2;
}

message Workspace {
  // Server-generated workspace UUID (primary key).
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Customer-chosen workspace slug, unique within the customer.
  string workspace_id = 3;
  // Human-readable display name.
  optional string name = 4;
  // Free-text description.
  optional string description = 5;
  // User-defined key/value labels.
  map<string, string> labels = 6;
  // Creation timestamp.
  google.protobuf.Timestamp created_at = 7;
  // Last-modification timestamp.
  google.protobuf.Timestamp updated_at = 8;
  // Soft-delete timestamp; unset while the workspace is active.
  optional google.protobuf.Timestamp deleted_at = 9;
  // Project this workspace belongs to; empty means the default project.
  string project_id = 10;
}

// ===== Service Account Messages =====

message CreateServiceAccountRequest {
  // Customer slug the service account belongs to.
  string customer_id = 1 [(buf.validate.field).string = {
    pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
    min_len: 1
    max_len: 63
  }];

  // Workspace slug the service account is scoped to.
  string workspace_id = 2 [(buf.validate.field).string = {
    pattern: "^[a-zA-Z0-9-_.]+$"
    min_len: 1
    max_len: 253
  }];

  // How many days the generated credential stays valid (default applied if unset).
  optional int64 validity_days = 3 [(buf.validate.field).int64 = {
    gte: 1
    lte: 3650
  }];

  // Management-plane API base URI embedded in the service account.
  optional string api_uri = 4 [(buf.validate.field).string.uri = true];
  // Customer-facing data-plane serve URL embedded in the service account.
  optional string serve_url = 18 [(buf.validate.field).string.uri = true];
  // JWT issuer (iss) to embed in the auth-server config.
  optional string issuer = 5 [(buf.validate.field).string.uri = true];
  // JWT audience (aud) to embed in the auth-server config.
  optional string audience = 6 [(buf.validate.field).string = {
    pattern: "^[a-zA-Z0-9._-]+$"
    max_len: 253
  }];
  // JWKS endpoint URI for verifying tokens issued to this account.
  optional string jwks_uri = 7 [(buf.validate.field).string.uri = true];

  // OTLP exporter endpoint for telemetry export.
  optional string telemetry_endpoint = 8;
  // OTLP wire protocol: grpc, http/protobuf, or http/json.
  optional string telemetry_protocol = 9 [(buf.validate.field).string = {
    in: [
      "",
      "grpc",
      "http/protobuf",
      "http/json"
    ]
  }];
  // If true, skip TLS verification for the telemetry endpoint.
  optional bool telemetry_insecure = 10;
  // OTLP export timeout as a Go duration (e.g. "10s").
  optional string telemetry_timeout = 11 [(buf.validate.field).string = {pattern: "^[0-9]+(ns|us|ms|s|m|h)$"}];
  // Extra headers attached to telemetry exports (e.g. auth tokens).
  map<string, string> telemetry_headers = 12;

  // OAuth-style scopes granted to the service account.
  repeated string scopes = 13 [(buf.validate.field).repeated = {
    min_items: 0
    max_items: 10
    items: {
      string: {
        pattern: "^[a-z_]+$"
        max_len: 50
      }
    }
  }];

  // Optional: Router configuration to include in generated service account.
  // When providing router_key this may be either a plaintext key (the server will
  // store it in the configured secret manager and replace with a secret ref) or
  // a secret reference (e.g. noop://..., sm://..., arn:aws:...).
  optional string router_url = 14 [(buf.validate.field).string.uri = true];
  optional string router_key = 15;

  // Optional: Internal API key for dataplane → management plane authentication.
  // In production (DB mode): Must be a secret reference (e.g., projects/.../secrets/internal-api-key-...).
  // Generated offline via admin CLI and stored in remote Secret Manager (GCP/AWS).
  // In file mode: Can be plaintext for local development.
  // Typically per-customer (one key shared by all service accounts for the customer).
  optional string internal_api_key = 16;

  // Optional: Local OTEL environment variable overrides to embed in the service account.
  // Values may be plaintext or secret references (e.g., sm://project/secret, noop://name).
  // The server resolves any secret references via the configured secret manager and
  // re-stores them as valet:// references before embedding in the generated service account.
  // The dataplane resolves valet:// references at load time so initLocalDestination sees
  // plaintext values when constructing the OTEL exporter.
  // Example: {"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer sm://myproject/otlp-token"}
  map<string, string> telemetry_local_vars = 17;
}

message CreateServiceAccountResponse {
  // The generated service account, including the private key (returned only here).
  ServiceAccount service_account = 1;
  // Integrity checksum over the canonical service-account JSON.
  string checksum = 2;
  // Hash algorithm used for `checksum` (e.g. "sha256").
  string checksum_algorithm = 3;
}

message ServiceAccount {
  // Service account UUID.
  string id = 1;
  // Credential type discriminator; always "service_account".
  string type = 2;
  // Owning customer slug.
  string customer_id = 3;
  // Workspace slug the account is scoped to.
  string workspace_id = 4;
  // Email-style subject identifier used as the JWT `sub`.
  string client_email = 5;
  // Key ID (kid) of the signing key pair.
  string private_key_id = 6;
  // PEM-encoded RSA private key (returned only at creation time).
  string private_key = 7;
  // Management-plane API base URI.
  string api_uri = 8;
  // Customer-facing data-plane serve URL.
  string serve_url = 15;
  // Issuer/audience/JWKS settings for the auth server.
  AuthServerConfig auth_server = 9;
  // Issuer/audience/keys plus optional router config for gatekeeper.
  GatekeeperConfig gatekeeper = 10;
  // Telemetry export configuration.
  DataExportsConfig data_exports = 11;
  // Scopes granted to the account.
  repeated string scopes = 12;
  // Creation timestamp.
  google.protobuf.Timestamp created_at = 13;
  // Expiry timestamp derived from validity_days.
  google.protobuf.Timestamp expires_at = 14;
}

message AuthServerConfig {
  // Expected JWT issuer (iss).
  string issuer = 1;
  // Expected JWT audience (aud).
  string audience = 2;
  // JWKS endpoint for key discovery.
  string jwks_uri = 3;
  // Public keys for offline token verification.
  repeated PublicKey public_keys = 4;
}

message GatekeeperConfig {
  // Expected JWT issuer (iss) for gatekeeper-issued tokens.
  string issuer = 1;
  // Expected JWT audience (aud).
  string audience = 2;
  // Public keys for verifying gatekeeper tokens.
  repeated PublicKey public_keys = 3;
  // Optional router configuration for TARS routing
  RouterConfig router = 4;
}

message RouterConfig {
  // TARS router base URL.
  string url = 1 [(buf.validate.field).string.uri = true];
  // Secret reference (or the raw value in server responses) for router key.
  string key = 2;
}

message PublicKey {
  // Key ID (kid) matching the JWT header.
  string kid = 1;
  // Signing algorithm (e.g. "RS256").
  string alg = 2;
  // Intended key use (e.g. "sig").
  string use = 3;
  // PEM-encoded public key material.
  string key = 4;
}

message DataExportsConfig {
  // Telemetry/OTLP export settings.
  TelemetryConfig telemetry = 1;
}

message TelemetryConfig {
  // OTLP exporter endpoint.
  string endpoint = 1;
  // OTLP wire protocol (grpc, http/protobuf, http/json).
  string protocol = 2;
  // If true, TLS verification is skipped.
  bool insecure = 3;
  // Export timeout as a Go duration string.
  string timeout = 4;
  // Extra headers attached to exports.
  map<string, string> headers = 5;
}

enum ServiceAccountStatus {
  // Status not set / not filtered.
  SERVICE_ACCOUNT_STATUS_UNSPECIFIED = 0;
  // Account is usable.
  SERVICE_ACCOUNT_STATUS_ACTIVE = 1;
  // Account was manually revoked.
  SERVICE_ACCOUNT_STATUS_REVOKED = 2;
  // Account passed its expiry date.
  SERVICE_ACCOUNT_STATUS_EXPIRED = 3;
}

message ListServiceAccountsRequest {
  // Optional filter by owning customer slug.
  optional string customer_id = 1 [(buf.validate.field).string = {
    pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
    max_len: 63
  }];

  // Optional filter by workspace slug.
  optional string workspace_id = 2 [(buf.validate.field).string = {
    pattern: "^[a-zA-Z0-9-_.]+$"
    max_len: 253
  }];

  // Optional filter by account status.
  optional ServiceAccountStatus status = 3;

  // Max results per page (0 = server default).
  optional int32 page_size = 4 [(buf.validate.field).int32 = {
    gte: 0
    lte: 1000
  }];

  // Opaque pagination cursor from a prior response.
  optional string page_token = 5;
}

message ListServiceAccountsResponse {
  // Page of service account summaries (no private keys).
  repeated ServiceAccountInfo service_accounts = 1;
  // Cursor for the next page; empty when exhausted.
  string next_page_token = 2;
}

message GetServiceAccountRequest {
  // Service account UUID to fetch.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
}

message GetServiceAccountResponse {
  // The requested service account summary.
  ServiceAccountInfo service_account = 1;
}

message ServiceAccountInfo {
  // Service account UUID.
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Workspace slug the account is scoped to.
  string workspace_id = 3;
  // Email-style subject identifier (client_email).
  string email = 4;
  // Current lifecycle status.
  ServiceAccountStatus status = 5;
  // Creation timestamp.
  google.protobuf.Timestamp created_at = 6;
  // Expiry timestamp.
  google.protobuf.Timestamp expires_at = 7;
  // Last time the account authenticated; unset if never used.
  optional google.protobuf.Timestamp last_used_at = 8;
  // Fingerprint of the account's public key.
  string public_key_fingerprint = 9;
  // Arbitrary stored metadata for the account.
  map<string, string> metadata = 10;
  // Revocation timestamp; unset unless revoked.
  optional google.protobuf.Timestamp revoked_at = 11;
}

message RevokeServiceAccountRequest {
  // Service account UUID to revoke.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
  // Optional human-readable revocation reason (audit trail).
  optional string reason = 2 [(buf.validate.field).string.max_len = 500];
}

message RevokeServiceAccountResponse {
  // True if the account was revoked.
  bool success = 1;
  // When the revocation took effect.
  google.protobuf.Timestamp revoked_at = 2;
}

message RestoreServiceAccountRequest {
  // Service account UUID to restore to active.
  string id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
  // Optional human-readable restore reason (audit trail).
  optional string reason = 2 [(buf.validate.field).string.max_len = 500];
}

message RestoreServiceAccountResponse {
  // True if the account was restored.
  bool success = 1;
  // The restored account summary.
  ServiceAccountInfo service_account = 2;
}

// ===== User Token Messages =====

message CreateUserTokenRequest {
  // Customer ID (e.g., "acme", "default")
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 100
    pattern: "^[a-z0-9_-]+$"
  }];

  // Workspace ID (e.g., "production", "staging", "internal")
  string workspace_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 100
    pattern: "^[a-z0-9_-]+$"
  }];

  // User ID from authentication provider (opaque identifier)
  string user_id = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 255
  }];

  // User role from authentication (e.g., "developer", "admin")
  string role = 4 [(buf.validate.field).string = {
    min_len: 1
    max_len: 100
  }];

  // Organization unit from authentication (e.g., "engineering", "sales")
  string organization_unit = 5 [(buf.validate.field).string = {
    min_len: 1
    max_len: 255
  }];

  // Token lifetime in days (default applied if unset).
  optional int64 expires_in_days = 6 [(buf.validate.field).int64 = {
    gte: 1
    lte: 365
  }];

  // OAuth-style scopes embedded in the issued token.
  repeated string scopes = 7 [(buf.validate.field).repeated = {
    min_items: 0
    max_items: 10
    items: {
      string: {
        pattern: "^[a-z_]+$"
        max_len: 50
      }
    }
  }];

  // Optional human-readable name for the token
  optional string name = 8 [(buf.validate.field).string.max_len = 255];

  // If true, the token will not be persisted to the database.
  // This is used for impersonation scenarios where the token is temporary.
  optional bool impersonate = 9;

  // Optional metadata for impersonation tracking.
  // When impersonate=true, this should contain the original user/admin who is impersonating.
  optional ImpersonationMetadata impersonation_metadata = 10;
}

message ImpersonationMetadata {
  // The key identifier (ID or key hash) of the user/admin performing the impersonation.
  string impersonator_key = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 255
  }];

  // Optional reason for impersonation.
  optional string reason = 2 [(buf.validate.field).string.max_len = 500];

  // Optional additional context.
  map<string, string> context = 3;
}

message CreateUserTokenResponse {
  // The signed JWT to use as a bearer credential.
  string access_token = 1;
  // Token type, typically "Bearer".
  string token_type = 2;
  // Seconds until the token expires.
  int64 expires_in = 3;
  // Absolute expiry timestamp.
  google.protobuf.Timestamp expires_at = 4;
  // JWT ID (jti); used to look up and revoke the token.
  string jti = 5;

  // Database storage identifiers (for internal use)
  string key_prefix = 6; // First 12 chars of xxhash64(jti)
  string key_hash = 7; // JTI (used for revocation lookups)
  string key_address = 8; // xxhash64(userId) as hex string
}

enum UserTokenStatus {
  // Status not set / not filtered.
  USER_TOKEN_STATUS_UNSPECIFIED = 0;
  // Token is valid and usable.
  USER_TOKEN_STATUS_ACTIVE = 1;
  // Token was manually revoked.
  USER_TOKEN_STATUS_REVOKED = 2;
  // Token passed its expiry.
  USER_TOKEN_STATUS_EXPIRED = 3;
}

message ListUserTokensRequest {
  // Optional filter by issuing user ID.
  optional string user_id = 1 [(buf.validate.field).string.max_len = 255];
  // Optional filter by token status.
  optional UserTokenStatus status = 2;

  // Max results per page (0 = server default).
  optional int32 page_size = 3 [(buf.validate.field).int32 = {
    gte: 0
    lte: 1000
  }];

  // Opaque pagination cursor from a prior response.
  optional string page_token = 4;
}

message ListUserTokensResponse {
  // Page of user token summaries.
  repeated UserTokenInfo tokens = 1;
  // Cursor for the next page; empty when exhausted.
  string next_page_token = 2;
}

message GetUserTokenRequest {
  // JWT ID (jti) of the token to fetch.
  string jti = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 64
  }];
}

message GetUserTokenResponse {
  // The requested token summary.
  UserTokenInfo token = 1;
}

message UserTokenInfo {
  // JWT ID (jti), unique per token.
  string jti = 1;
  // User the token was issued for.
  string user_id = 2;
  // Role embedded in the token.
  string role = 3;
  // Organization unit embedded in the token.
  string organization_unit = 4;
  // Current lifecycle status.
  UserTokenStatus status = 5;
  // Issuance timestamp.
  google.protobuf.Timestamp created_at = 6;
  // Expiry timestamp.
  google.protobuf.Timestamp expires_at = 7;
  // Last time the token was used; unset if never used.
  optional google.protobuf.Timestamp last_used_at = 8;
  // Revocation timestamp; unset unless revoked.
  optional google.protobuf.Timestamp revoked_at = 9;
  // Reason recorded at revocation, if any.
  optional string revoked_reason = 10;
}

message RevokeUserTokenRequest {
  // JWT ID (jti) of the token to revoke.
  string jti = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 64
  }];

  // Optional human-readable revocation reason (audit trail).
  optional string reason = 2 [(buf.validate.field).string.max_len = 500];
}

message RevokeUserTokenResponse {
  // True if the token was revoked.
  bool success = 1;
  // When the revocation took effect.
  google.protobuf.Timestamp revoked_at = 2;
}

// ===== Telemetry Forwarding Config Messages =====

message TelemetryForwardingConfig {
  string id = 1; // UUID
  string user_id = 2;
  string telemetry_type = 3; // 'traces', 'metrics', 'logs'
  string sink_url = 4; // Destination URL
  string protocol = 5; // 'grpc', 'http/json', 'http/protobuf'
  string auth_type = 6; // 'none', 'bearer', 'basic', 'api_key'
  optional string auth_value = 7; // Decrypted auth value (only returned in Get/Create responses)
  optional string api_key_header = 8; // Header name for API key auth
  bool enabled = 9;
  google.protobuf.Timestamp last_checked_at = 10;
  string status = 11; // 'connected', 'disconnected', 'error', 'testing'
  google.protobuf.Timestamp created_at = 12;
  google.protobuf.Timestamp updated_at = 13;
}

message CreateTelemetryConfigRequest {
  // Owner of the forwarding config.
  string user_id = 1 [(buf.validate.field).string.min_len = 1];
  // Signal type to forward: traces, metrics, or logs.
  string telemetry_type = 2 [(buf.validate.field).string = {
    in: [
      "traces",
      "metrics",
      "logs"
    ]
  }];
  // Destination OTLP sink URL.
  string sink_url = 3 [(buf.validate.field).string = {
    min_len: 1
    uri: true
  }];
  // OTLP wire protocol for the sink.
  string protocol = 4 [(buf.validate.field).string = {
    in: [
      "grpc",
      "http/json",
      "http/protobuf"
    ]
  }];
  // Authentication scheme for the sink.
  string auth_type = 5 [(buf.validate.field).string = {
    in: [
      "none",
      "bearer",
      "basic",
      "api_key"
    ]
  }];
  optional string auth_value = 6; // Plain text auth value to be encrypted
  // Header name to carry the key when auth_type is api_key.
  optional string api_key_header = 7 [(buf.validate.field).string.max_len = 256];
  // Whether forwarding starts enabled (default enabled if unset).
  optional bool enabled = 8;
}

message CreateTelemetryConfigResponse {
  // The created forwarding config (auth_value decrypted).
  TelemetryForwardingConfig config = 1;
}

message ListTelemetryConfigsRequest {
  optional string user_id = 1; // Filter by user_id
  // Optional filter by signal type.
  optional string telemetry_type = 2 [(buf.validate.field).string = {
    in: [
      "traces",
      "metrics",
      "logs"
    ]
  }];
  optional bool enabled = 3; // Filter by enabled status
}

message ListTelemetryConfigsResponse {
  // Matching forwarding configs.
  repeated TelemetryForwardingConfig configs = 1;
  // Total number of matching configs.
  int32 total = 2;
}

message GetTelemetryConfigRequest {
  // Forwarding config ID to fetch.
  string id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 64
  }];
}

message GetTelemetryConfigResponse {
  // The requested forwarding config.
  TelemetryForwardingConfig config = 1;
}

message UpdateTelemetryConfigRequest {
  // Forwarding config ID to update.
  string id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 64
  }];
  // New sink URL; unset leaves it unchanged.
  optional string sink_url = 2 [(buf.validate.field).string.uri = true];
  // New OTLP protocol; unset leaves it unchanged.
  optional string protocol = 3 [(buf.validate.field).string = {
    in: [
      "grpc",
      "http/json",
      "http/protobuf"
    ]
  }];
  // New auth scheme; unset leaves it unchanged.
  optional string auth_type = 4 [(buf.validate.field).string = {
    in: [
      "none",
      "bearer",
      "basic",
      "api_key"
    ]
  }];
  optional string auth_value = 5; // Plain text auth value to be encrypted
  // New API key header name; unset leaves it unchanged.
  optional string api_key_header = 6 [(buf.validate.field).string.max_len = 256];
  // New enabled flag; unset leaves it unchanged.
  optional bool enabled = 7;
  // Override the connection status (connected/disconnected/error/testing).
  optional string status = 8 [(buf.validate.field).string = {
    in: [
      "connected",
      "disconnected",
      "error",
      "testing"
    ]
  }];
}

message UpdateTelemetryConfigResponse {
  // The updated forwarding config.
  TelemetryForwardingConfig config = 1;
}

message DeleteTelemetryConfigRequest {
  // Forwarding config ID to delete.
  string id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 64
  }];
}

message DeleteTelemetryConfigResponse {
  // True if the config was deleted.
  bool success = 1;
}

// ===== Customer Messages =====

message Customer {
  // Server-generated customer UUID (primary key).
  string id = 1;
  // Customer slug, unique across the deployment.
  string customer_id = 2;
  // Human-readable display name.
  optional string name = 3;
  // Free-text description.
  optional string description = 4;
  // User-defined key/value labels.
  map<string, string> labels = 5;
  // Creation timestamp.
  google.protobuf.Timestamp created_at = 6;
  // Last-modification timestamp.
  google.protobuf.Timestamp updated_at = 7;
  // Soft-delete timestamp; unset while active.
  optional google.protobuf.Timestamp deleted_at = 8;
}

message CreateCustomerRequest {
  // Caller-chosen customer slug, unique across the deployment.
  string customer_id = 1 [(buf.validate.field).string = {
    pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
    min_len: 1
    max_len: 63
  }];
  // Optional display name.
  optional string name = 2 [(buf.validate.field).string.max_len = 255];
  // Optional description.
  optional string description = 3 [(buf.validate.field).string.max_len = 1000];
  // Optional user-defined labels.
  map<string, string> labels = 4;
}

message CreateCustomerResponse {
  // The newly created customer.
  Customer customer = 1;
}

message ListCustomersRequest {
  // Max results per page (0 = server default).
  optional int32 page_size = 1 [(buf.validate.field).int32 = {
    gte: 0
    lte: 1000
  }];
  // Opaque pagination cursor from a prior response.
  optional string page_token = 2;
}

message ListCustomersResponse {
  // Page of customers.
  repeated Customer customers = 1;
  // Cursor for the next page; empty when exhausted.
  string next_page_token = 2;
}

message GetCustomerRequest {
  // Customer slug to fetch.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
}

message GetCustomerResponse {
  // The requested customer.
  Customer customer = 1;
}

message UpdateCustomerRequest {
  // Customer slug to update.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // New display name; unset leaves it unchanged.
  optional string name = 2 [(buf.validate.field).string.max_len = 255];
  // New description; unset leaves it unchanged.
  optional string description = 3 [(buf.validate.field).string.max_len = 1000];
  // Replacement set of labels.
  map<string, string> labels = 4;
}

message UpdateCustomerResponse {
  // The updated customer.
  Customer customer = 1;
}

message DeleteCustomerRequest {
  // Customer slug to soft-delete.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
}

message DeleteCustomerResponse {
  // True if the customer was deleted.
  bool success = 1;
  // When the soft delete took effect.
  google.protobuf.Timestamp deleted_at = 2;
}

// ===== Project Messages =====

message Project {
  // Server-generated project UUID (primary key).
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Project slug, unique within the customer.
  string project_id = 3;
  // Human-readable display name.
  optional string name = 4;
  // Free-text description.
  optional string description = 5;
  // User-defined key/value labels.
  map<string, string> labels = 6;
  // Creation timestamp.
  google.protobuf.Timestamp created_at = 7;
  // Last-modification timestamp.
  google.protobuf.Timestamp updated_at = 8;
  // Soft-delete timestamp; unset while active.
  optional google.protobuf.Timestamp deleted_at = 9;
}

message CreateProjectRequest {
  // Customer slug that will own the project.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Caller-chosen project slug, unique within the customer.
  string project_id = 2 [(buf.validate.field).string = {
    pattern: "^[a-z0-9]([a-z0-9-_.]*[a-z0-9])?$"
    min_len: 1
    max_len: 253
  }];
  // Optional display name.
  optional string name = 3 [(buf.validate.field).string.max_len = 255];
  // Optional description.
  optional string description = 4 [(buf.validate.field).string.max_len = 1000];
  // Optional user-defined labels.
  map<string, string> labels = 5;
}

message CreateProjectResponse {
  // The newly created project.
  Project project = 1;
}

message ListProjectsRequest {
  // Customer slug whose projects to list.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Max results per page (0 = server default).
  optional int32 page_size = 2 [(buf.validate.field).int32 = {
    gte: 0
    lte: 1000
  }];
  // Opaque pagination cursor from a prior response.
  optional string page_token = 3;
}

message ListProjectsResponse {
  // Page of projects.
  repeated Project projects = 1;
  // Cursor for the next page; empty when exhausted.
  string next_page_token = 2;
}

message GetProjectRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project slug to fetch.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message GetProjectResponse {
  // The requested project.
  Project project = 1;
}

message UpdateProjectRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project slug to update.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // New display name; unset leaves it unchanged.
  optional string name = 3 [(buf.validate.field).string.max_len = 255];
  // New description; unset leaves it unchanged.
  optional string description = 4 [(buf.validate.field).string.max_len = 1000];
  // Replacement set of labels.
  map<string, string> labels = 5;
}

message UpdateProjectResponse {
  // The updated project.
  Project project = 1;
}

message DeleteProjectRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project slug to soft-delete.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message DeleteProjectResponse {
  // True if the project was deleted.
  bool success = 1;
  // When the soft delete took effect.
  google.protobuf.Timestamp deleted_at = 2;
}

// ===== Project Member Messages =====

message ProjectMember {
  // Membership row UUID.
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Project the membership belongs to.
  string project_id = 3;
  // The member's user ID.
  string user_id = 4;
  // The member's role in the project ("admin" or "member").
  string role = 5;
  // When the member was added.
  google.protobuf.Timestamp created_at = 6;
  // When the membership was last modified.
  google.protobuf.Timestamp updated_at = 7;
}

message AddProjectMemberRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to add the member to.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // User ID to add as a member.
  string user_id = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 255
  }];
  // Role to grant: "admin" or "member".
  string role = 4 [(buf.validate.field).string = {
    in: [
      "admin",
      "member"
    ]
  }];
}

message AddProjectMemberResponse {
  // The created membership.
  ProjectMember member = 1;
}

message ListProjectMembersRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project whose members to list.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message ListProjectMembersResponse {
  // All members of the project.
  repeated ProjectMember members = 1;
}

message UpdateProjectMemberRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project the member belongs to.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // User ID of the member to update.
  string user_id = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 255
  }];
  // New role: "admin" or "member".
  string role = 4 [(buf.validate.field).string = {
    in: [
      "admin",
      "member"
    ]
  }];
}

message UpdateProjectMemberResponse {
  // The updated membership.
  ProjectMember member = 1;
}

message RemoveProjectMemberRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to remove the member from.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // User ID of the member to remove.
  string user_id = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 255
  }];
}

message RemoveProjectMemberResponse {
  // True if the member was removed.
  bool success = 1;
}

// ===== Model Catalog Messages =====

message AIModel {
  // Catalog entry UUID.
  string id = 1;
  // Provider-facing model identifier (e.g. "gpt-4o").
  string model = 2;
  // Provider that serves this model (e.g. "openai", "anthropic").
  string provider = 3;
  // Optional mode/modality (e.g. "chat", "embedding").
  optional string mode = 4;
  // Whether the model is currently enabled in the catalog.
  bool is_enabled = 5;
  // Input token price per million tokens.
  double input_price_per_million = 6;
  // Output token price per million tokens.
  double output_price_per_million = 7;
  // Maximum context window in tokens.
  int32 context_window = 8;
  // Supported capability tags (e.g. "vision", "tools").
  repeated string capabilities = 9;
}

message ListModelsRequest {
  // If true, return only enabled models.
  bool only_enabled = 1;
}

message ListModelsResponse {
  // Catalog models matching the request.
  repeated AIModel models = 1;
}

message GetModelRequest {
  // Catalog entry UUID to fetch.
  string model_id = 1 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
}

message GetModelResponse {
  // The requested model.
  AIModel model = 1;
}

// ===== Project Model Messages =====

message ProjectModelEntry {
  // Assignment row UUID.
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Project the model is assigned to.
  string project_id = 3;
  // Catalog model UUID assigned to the project.
  string model_id = 4;
  // Full catalog details of the assigned model.
  AIModel model = 5;
  // When the model was assigned.
  google.protobuf.Timestamp created_at = 6;
}

message AddProjectModelRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to assign the model to.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Catalog model UUID to assign.
  string model_id = 3 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
}

message AddProjectModelResponse {
  // The created model assignment.
  ProjectModelEntry entry = 1;
}

message ListProjectModelsRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project whose assigned models to list.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message ListProjectModelsResponse {
  // Models assigned to the project.
  repeated ProjectModelEntry entries = 1;
}

message RemoveProjectModelRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to remove the model from.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Catalog model UUID to unassign.
  string model_id = 3 [(buf.validate.field).string = {pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}];
}

message RemoveProjectModelResponse {
  // True if the assignment was removed.
  bool success = 1;
}

// ===== Project Provider Messages =====

message ProjectProviderEntry {
  // Assignment row UUID.
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Project the provider is enabled for.
  string project_id = 3;
  // Provider name enabled for the project (e.g. "openai").
  string provider_name = 4;
  // When the provider was assigned.
  google.protobuf.Timestamp created_at = 5;
}

message AddProjectProviderRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to enable the provider for.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Provider name to enable (e.g. "openai").
  string provider_name = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
}

message AddProjectProviderResponse {
  // The created provider assignment.
  ProjectProviderEntry entry = 1;
}

message ListProjectProvidersRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project whose enabled providers to list.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message ListProjectProvidersResponse {
  // Providers enabled for the project.
  repeated ProjectProviderEntry entries = 1;
}

message RemoveProjectProviderRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to disable the provider for.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Provider name to disable.
  string provider_name = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
}

message RemoveProjectProviderResponse {
  // True if the provider was removed.
  bool success = 1;
}

// ===== Project MCP Server Messages =====

message ProjectMcpServerEntry {
  // Assignment row UUID.
  string id = 1;
  // Owning customer slug.
  string customer_id = 2;
  // Project the MCP server is assigned to.
  string project_id = 3;
  // Catalog server ID assigned to the project (e.g. "github").
  string server_id = 4;
  // When the server was assigned.
  google.protobuf.Timestamp created_at = 5;
}

message AddProjectMcpServerRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to assign the MCP server to.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Catalog server ID to assign (e.g. "github").
  string server_id = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
    // Path-safe: server_id is placed verbatim in the REST delete route's URL
    // path segment (/mcp-servers/{server_id}). Reject "/" and other
    // path-sensitive characters here so an entry can't be added through the
    // body yet be undeletable through REST; the start/end anchors also reject
    // "." and ".." traversal segments.
    pattern: "^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$"
  }];
}

message AddProjectMcpServerResponse {
  // The created MCP server assignment.
  ProjectMcpServerEntry entry = 1;
}

message ListProjectMcpServersRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project whose assigned MCP servers to list.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message ListProjectMcpServersResponse {
  // MCP servers assigned to the project.
  repeated ProjectMcpServerEntry entries = 1;
}

message RemoveProjectMcpServerRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to remove the MCP server from.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Catalog server ID to remove.
  string server_id = 3 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
    // Path-safe: must match the pattern enforced on add (this id also rides
    // in the REST delete route's URL path segment).
    pattern: "^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$"
  }];
}

message RemoveProjectMcpServerResponse {
  // True if the MCP server assignment was removed.
  bool success = 1;
}

// ===== Project Settings Messages =====

message ProjectSetting {
  // Setting key (e.g. "global-fallback", "rate-limit").
  string setting_name = 1;
  // Setting value (string-encoded).
  string setting_value = 2;
  // When the setting was last updated.
  google.protobuf.Timestamp updated_at = 3;
}

message GetProjectSettingsRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project whose settings to fetch.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
}

message GetProjectSettingsResponse {
  // All settings for the project.
  repeated ProjectSetting settings = 1;
}

message UpsertProjectSettingRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to set the value on.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Setting key to create or update ("global-fallback" or "rate-limit").
  string setting_name = 3 [(buf.validate.field).string = {
    in: [
      "global-fallback",
      "rate-limit"
    ]
  }];
  // New string-encoded value for the setting.
  string setting_value = 4 [(buf.validate.field).string = {
    min_len: 1
    max_len: 1024
  }];
}

message UpsertProjectSettingResponse {
  // The created or updated setting.
  ProjectSetting setting = 1;
}

message DeleteProjectSettingRequest {
  // Owning customer slug.
  string customer_id = 1 [(buf.validate.field).string = {
    min_len: 1
    max_len: 63
  }];
  // Project to delete the setting from.
  string project_id = 2 [(buf.validate.field).string = {
    min_len: 1
    max_len: 253
  }];
  // Setting key to delete ("global-fallback" or "rate-limit").
  string setting_name = 3 [(buf.validate.field).string = {
    in: [
      "global-fallback",
      "rate-limit"
    ]
  }];
}

message DeleteProjectSettingResponse {
  // True if the setting was deleted.
  bool success = 1;
}
