syntax = "proto3";

package tars.configurator.v1;

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

option go_package = "github.com/tetrateio/fraser/api/tars/configurator/v1;configuratorv1";

// ConfiguratorService manages liaison configuration state.
// This service replaces the pub/sub mechanism with a polling-based approach.
//
// Flow:
// 1. Dashboard triggers configuration update via TriggerUpdate
// 2. Valet generates configuration and stores in database with version
// 3. Liaison worker polls GetConfiguration periodically
// 4. Worker detects version changes and downloads new configuration
// 5. Worker applies configuration to cluster
service ConfiguratorService {
  // GetConfiguration retrieves the latest configuration for a user.
  // Returns configuration only if the version is newer than the provided
  // version. Supports batch queries for multiple users for efficiency.
  rpc GetConfiguration(GetConfigurationRequest) returns (GetConfigurationResponse) {
    option (tars.docs.v1.group) = "Configuration";
    option (google.api.http) = {
      post: "/configurator/v1/config"
      body: "*"
    };
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // TriggerUpdate triggers a configuration update for a specific user and
  // config type. This is called by the dashboard when configuration changes.
  rpc TriggerUpdate(TriggerUpdateRequest) returns (TriggerUpdateResponse) {
    option (tars.docs.v1.group) = "Configuration";
    option (google.api.http) = {
      post: "/configurator/v1/trigger"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // ListConfigurations lists all configurations with optional filtering.
  // Used for debugging and monitoring.
  rpc ListConfigurations(ListConfigurationsRequest) returns (ListConfigurationsResponse) {
    option (tars.docs.v1.group) = "Configuration";
    option (google.api.http) = {get: "/configurator/v1/configs"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // ListPendingEvents returns configuration events that need processing.
  // Used by workers to discover which configurations have updates. The worker
  // piggybacks its tare-doctor runtime observation onto this poll
  // (request.doctor_runtime) so MP can explain a stale doctor report — hence
  // this is no longer NO_SIDE_EFFECTS: a poll updates the data plane's
  // doctor-runtime state.
  rpc ListPendingEvents(ListPendingEventsRequest) returns (ListPendingEventsResponse) {
    option (tars.docs.v1.group) = "Events";
    option (google.api.http) = {get: "/configurator/v1/pending-events"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // ReconcileWorkspace re-enqueues missing or stale slot events for
  // the caller's workspace by cloning the latest sibling event of each
  // slot the workspace has not yet converged on. Used by the worker's
  // periodic drift sweep. Workspace is resolved from the caller's auth
  // claims; callers without a workspace claim are rejected.
  rpc ReconcileWorkspace(ReconcileWorkspaceRequest) returns (ReconcileWorkspaceResponse) {
    option (tars.docs.v1.group) = "Workspace Sync";
    option (google.api.http) = {
      post: "/configurator/v1/reconcile-workspace"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // FullSyncWorkspace is the heavier "last resort" replay path: walks
  // `liaison_configurations` directly rather than sibling events, so it
  // can recover legacy unscoped events the drift sweep can't see. Worker
  // calls this once on startup and every Nth drift sweep tick. Workspace
  // and customer are resolved from the caller's auth claims.
  rpc FullSyncWorkspace(FullSyncWorkspaceRequest) returns (FullSyncWorkspaceResponse) {
    option (tars.docs.v1.group) = "Workspace Sync";
    option (google.api.http) = {
      post: "/configurator/v1/full-sync-workspace"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // AcknowledgeEvent marks an event as acknowledged by a worker.
  // This prevents other workers from processing the same event.
  rpc AcknowledgeEvent(AcknowledgeEventRequest) returns (AcknowledgeEventResponse) {
    option (tars.docs.v1.group) = "Events";
    option (google.api.http) = {
      post: "/configurator/v1/acknowledge-event"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // SetDataplaneURL persists the customer-facing gateway URL for a data plane
  // (workspace). Called by `tare install` to register the URL at install time
  // and by the management-plane dashboard when an admin edits it later.
  //
  // trigger_update=false (install path) only writes the system_settings row.
  // trigger_update=true (admin-edit path) additionally fires a PROXY_URL
  // configuration event so the running data plane converges on the new URL
  // via the worker's existing event-handling loop.
  rpc SetDataplaneURL(SetDataplaneURLRequest) returns (SetDataplaneURLResponse) {
    option (tars.docs.v1.group) = "Dataplane";
    option (google.api.http) = {
      post: "/configurator/v1/dataplane-url"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // UpsertMCPCatalogServer wires a deployed in-cluster MCP server into the
  // customer's catalog. Used by `tare mcp enable <server>`: the CLI looks
  // up the server's seed entry in mcp-catalog-tare-mcp-with-tools.json, asks
  // the operator for the in-cluster URL, and submits this RPC to set
  // is_enabled=true on the corresponding mcp_catalog_servers row.
  //
  // Default behaviour (force_overwrite=false): if the row exists, only
  // url + is_enabled (+updated_at) are updated; dashboard-set fields
  // (description / icon / categories / tools / etc.) are preserved.
  // If the row is new, the seed-provided fields are used to populate it.
  //
  // force_overwrite=true: every seed field is written, clobbering any
  // dashboard edits. Use sparingly — intended for "reset this row to
  // seed defaults" repairs.
  rpc UpsertMCPCatalogServer(UpsertMCPCatalogServerRequest) returns (UpsertMCPCatalogServerResponse) {
    option (tars.docs.v1.group) = "MCP Catalog";
    option (google.api.http) = {
      post: "/configurator/v1/mcp-catalog/upsert"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // ValidateDataplane lets a data plane's filter confirm that this DP is
  // still a valid (non-revoked) member of the customer and fetch the
  // customer's current gatekeeper public keys. Authentication is via the
  // caller's service-account JWT signed with its DP keypair
  // (kid=`instance-<...>`). When the DP keypair has been soft-deleted at
  // MP, the auth layer rejects the call with Unauthenticated — the filter
  // treats that as "revoked" without needing a separate response field.
  // On success, the response carries the customer's gatekeeper public
  // keys so the filter can repopulate its in-memory cache (and the
  // worker can repopulate `tars-config[gatekeeper-public-keys]`).
  rpc ValidateDataplane(ValidateDataplaneRequest) returns (ValidateDataplaneResponse) {
    option (tars.docs.v1.group) = "Dataplane";
    option (google.api.http) = {
      post: "/configurator/v1/validate-dataplane"
      body: "*"
    };
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_SERVICE_ACCOUNT]
      scopes: ["admin"]
    };
  }

  // CompleteEvent marks an event as successfully applied or failed.
  rpc CompleteEvent(CompleteEventRequest) returns (CompleteEventResponse) {
    option (tars.docs.v1.group) = "Events";
    option (google.api.http) = {
      post: "/configurator/v1/complete-event"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // ListActiveUsers returns a list of user IDs that have active configurations.
  // Used for monitoring and debugging.
  rpc ListActiveUsers(ListActiveUsersRequest) returns (ListActiveUsersResponse) {
    option (tars.docs.v1.group) = "Monitoring";
    option (google.api.http) = {get: "/configurator/v1/active-users"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // ListActiveProviders returns a list of active (enabled) AI providers.
  // Used for monitoring, debugging, and querying available provider configurations.
  rpc ListActiveProviders(ListActiveProvidersRequest) returns (ListActiveProvidersResponse) {
    option (tars.docs.v1.group) = "Monitoring";
    option (google.api.http) = {get: "/configurator/v1/active-providers"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // SaveBYOKBackends saves BYOK provider backends configuration.
  // Used by workers to persist BYOK backend metadata to database.
  rpc SaveBYOKBackends(SaveBYOKBackendsRequest) returns (SaveBYOKBackendsResponse) {
    option (tars.docs.v1.group) = "BYOK Backends";
    option (google.api.http) = {
      post: "/configurator/v1/byok-backends"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // DeleteBYOKBackends deletes BYOK provider backends configuration.
  // Used by workers to remove BYOK backend metadata from database.
  rpc DeleteBYOKBackends(DeleteBYOKBackendsRequest) returns (DeleteBYOKBackendsResponse) {
    option (tars.docs.v1.group) = "BYOK Backends";
    option (google.api.http) = {delete: "/configurator/v1/byok-backends/{user_id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // RevokeUserToken revokes a user token by JTI.
  // Used by workers in enterprise mode to revoke tokens when API keys are deleted.
  rpc RevokeUserToken(RevokeUserTokenRequest) returns (RevokeUserTokenResponse) {
    option (tars.docs.v1.group) = "User Tokens";
    option (google.api.http) = {
      post: "/configurator/v1/user-tokens/{jti}:revoke"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_API_KEY,
        AUTH_TYPE_SERVICE_ACCOUNT
      ]
      scopes: ["admin"]
    };
  }

  // Replaces metadata JSON on an existing liaison_configuration_events row.
  // Lets workers push mid-flight progress onto an event without changing
  // its status (AcknowledgeEvent/CompleteEvent are terminal-only).
  // Worker writes are scoped to the caller's workspace claim.
  rpc UpdateEventMetadata(UpdateEventMetadataRequest) returns (UpdateEventMetadataResponse) {
    option (tars.docs.v1.group) = "Events";
    option (google.api.http) = {
      post: "/configurator/v1/event-metadata"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_SERVICE_ACCOUNT]
      scopes: ["admin"]
    };
  }

  // SubmitDataplaneUpgradeCredentials stores operator-provided registry
  // credentials for a DATAPLANE_UPGRADE event parked in NeedsCredentials.
  // Payload is KEK-envelope encrypted at MP and stored in
  // dataplane_upgrade_credentials. Dashboard (admin) is the only caller.
  rpc SubmitDataplaneUpgradeCredentials(SubmitDataplaneUpgradeCredentialsRequest) returns (SubmitDataplaneUpgradeCredentialsResponse) {
    option (tars.docs.v1.group) = "Dataplane Upgrade";
    option (google.api.http) = {
      post: "/configurator/v1/dataplane-upgrade-credentials"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // GetDataplaneUpgradeCredentials returns the operator-submitted
  // credentials for an upgrade event. MP decrypts server-side and
  // returns plaintext over TLS — the worker (DP cluster) never has
  // KEK access. Caller's workspace claim must match the event's
  // workspace. Read-only — no state change until AckDataplaneUpgradeCredentials.
  rpc GetDataplaneUpgradeCredentials(GetDataplaneUpgradeCredentialsRequest) returns (GetDataplaneUpgradeCredentialsResponse) {
    option (tars.docs.v1.group) = "Dataplane Upgrade";
    option (google.api.http) = {
      post: "/configurator/v1/dataplane-upgrade-credentials/get"
      body: "*"
    };
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_SERVICE_ACCOUNT]
      scopes: ["admin"]
    };
  }

  // AckDataplaneUpgradeCredentials confirms the worker successfully
  // materialised the credentials Secret in-cluster. MP DELETEs the
  // ciphertext row immediately — no 30-day retention. Workspace claim
  // must match. Idempotent: a second Ack on an already-deleted row
  // returns success.
  rpc AckDataplaneUpgradeCredentials(AckDataplaneUpgradeCredentialsRequest) returns (AckDataplaneUpgradeCredentialsResponse) {
    option (tars.docs.v1.group) = "Dataplane Upgrade";
    option (google.api.http) = {
      post: "/configurator/v1/dataplane-upgrade-credentials/ack"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_SERVICE_ACCOUNT]
      scopes: ["admin"]
    };
  }

  // CancelDataplaneUpgrade emits a pending UPGRADE_CANCEL event against
  // the workspace. The liaison worker picks it up via the normal
  // ListPendingEvents poll, matches the in-flight TarsUpgrade CR by name
  // (= upgrade_event_id), and patches spec.cancel so the running Job
  // initiates helm --atomic rollback at its next 5s poll tick. Idempotent:
  // a no-op when the CR already has spec.cancel=true or has reached a
  // terminal phase.
  rpc CancelDataplaneUpgrade(CancelDataplaneUpgradeRequest) returns (CancelDataplaneUpgradeResponse) {
    option (tars.docs.v1.group) = "Dataplane Upgrade";
    option (google.api.http) = {
      post: "/configurator/v1/dataplane-upgrade/cancel"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }

  // RollbackDataplaneUpgrade emits a pending UPGRADE_ROLLBACK event
  // against the workspace. Operator-initiated post-Completed rollback:
  // dashboard shows a Rollback button after a successful upgrade so the
  // operator can return to the prior helm revision (e.g. they want to
  // unwind a release that's behaving badly under real traffic). Worker
  // spawns a new rollback Job that runs
  // `helm rollback tars <prev-revision> --wait --atomic`. The new event
  // becomes the latest dataplane-upgrade view so the dashboard renders
  // the in-flight rollback through phase RollingBack → RolledBack.
  rpc RollbackDataplaneUpgrade(RollbackDataplaneUpgradeRequest) returns (RollbackDataplaneUpgradeResponse) {
    option (tars.docs.v1.group) = "Dataplane Upgrade";
    option (google.api.http) = {
      post: "/configurator/v1/dataplane-upgrade/rollback"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_API_KEY]
      scopes: ["admin"]
    };
  }
}

// ConfigType defines the type of configuration being managed.
enum ConfigType {
  CONFIG_TYPE_UNSPECIFIED = 0;
  CONFIG_TYPE_GLOBAL_ROUTES = 1; // Global routing configuration
  CONFIG_TYPE_USER_ROUTES = 2; // User-specific routing configuration (includes
  // fallback and traffic split)
  CONFIG_TYPE_BYOK = 3; // Bring Your Own Key configuration
  CONFIG_TYPE_MCP_PROFILE = 4; // MCP Profile configuration
  CONFIG_TYPE_MCP_BACKENDS = 5; // MCP Backends configuration
  CONFIG_TYPE_OAUTH_TOKEN = 6; // OAuth Token configuration
  CONFIG_TYPE_BYOK_BACKENDS = 7; // BYOK Backends configuration (backend names and metadata)
  CONFIG_TYPE_GUARDRAILS = 8; // AOD guardrails configuration
  CONFIG_TYPE_PROXY_URL = 9; // Proxy URL from system_settings
  CONFIG_TYPE_PROXY = 10; // Proxy dataplane configuration (tars-dataplane dynamic module)
  CONFIG_TYPE_PROVIDER_SECRETS = 11; // LLM provider secret rotation (fs-* Secrets)
  // MP-initiated remote DP upgrade. Trigger payload (target_version,
  // allow_downtime, credentials_ref) goes into event.metadata; worker
  // amends the same metadata via UpdateEventMetadata as phases progress.
  CONFIG_TYPE_DATAPLANE_UPGRADE = 12;
  // Project-scoped BYOK configuration for one route owner. Unlike
  // CONFIG_TYPE_BYOK (per-user, creator == route owner), the payload carries a
  // route owner U with their effective key set (project-level keys overridden
  // by U's user-scoped keys); each key retains its own creator id so the worker
  // can fetch the secret from the creator's realm. Emitted by the aikeys
  // Register/Revoke refresher, one per affected project member.
  CONFIG_TYPE_BYOK_SCOPED = 13;
}

// EventType defines the type of event that triggered the update.
enum EventType {
  EVENT_TYPE_UNSPECIFIED = 0;
  EVENT_TYPE_NEW = 1; // New configuration created
  EVENT_TYPE_UPDATE = 2; // Configuration updated
  // Upgrade-flow trigger (paired with CONFIG_TYPE_DATAPLANE_UPGRADE).
  // Distinct from UPDATE so the DB column self-documents what the event
  // is actually doing.
  EVENT_TYPE_UPGRADE = 8;
  // Cancel request for an in-flight upgrade (paired with
  // CONFIG_TYPE_DATAPLANE_UPGRADE). Metadata carries upgrade_event_id of
  // the row being cancelled; worker matches by TarsUpgrade CR name.
  EVENT_TYPE_UPGRADE_CANCEL = 9;
  // Operator-initiated rollback request after a completed upgrade
  // (paired with CONFIG_TYPE_DATAPLANE_UPGRADE). Metadata carries
  // upgrade_event_id of the upgrade being rolled back; worker spawns a
  // new TarsUpgrade CR + rollback Job that runs `helm rollback` against
  // the prior revision.
  EVENT_TYPE_UPGRADE_ROLLBACK = 10;
  EVENT_TYPE_DELETE = 3; // Configuration deleted
  EVENT_TYPE_UPDATE_BYOK = 4; // BYOK-specific update
  EVENT_TYPE_UPDATE_MCP = 5; // MCP-specific update
  EVENT_TYPE_UPDATE_OAUTH = 6; // OAuth-specific update
}

// GetConfigurationRequest requests configuration for users.
message GetConfigurationRequest {
  // List of user queries to check for updates.
  // Supports batch queries for efficiency.
  repeated UserConfigQuery queries = 1;
}

// UserConfigQuery queries configuration for a specific user and config type.
message UserConfigQuery {
  // User ID to query configuration for.
  // Use "system" for global/system-level configuration.
  string user_id = 1;

  // Configuration type to query.
  ConfigType config_type = 2;

  // Current version the client has.
  // Server returns configuration only if version is newer.
  int64 current_version = 3;

  // Key ID for user-specific configurations.
  // Used for BYOK, User routes, MCP profiles, etc.
  string key_id = 4;
}

// GetConfigurationResponse returns configurations that have updates.
message GetConfigurationResponse {
  // List of configurations with updates.
  // Only includes configs where version > current_version in request.
  repeated Configuration configurations = 1;
}

// TriggerUpdateRequest triggers a configuration update.
message TriggerUpdateRequest {
  // User ID for the configuration.
  // If not provided, uses authenticated user ID.
  // Use "system" for global configuration.
  string user_id = 1;

  // Configuration type to update.
  ConfigType config_type = 2;

  // Event type that triggered this update.
  EventType event_type = 3;

  // Optional key ID for user-specific configurations.
  // Used for BYOK, MCP profiles, etc.
  string key_id = 4;

  // Optional workspace (data plane) scope for the event.
  // When set, the configurator emits a single event scoped to this
  // workspace only. When unset, the configurator fans the event out
  // to every workspace belonging to the caller's customer, so every
  // data plane in that customer converges on the new configuration.
  string workspace_id = 5;

  // Optional inline payload for config types that take operator-provided
  // params. Ignored by config types that read their state from DB. Today
  // only CONFIG_TYPE_DATAPLANE_UPGRADE consumes this. The fields end up
  // merged into the persisted event.metadata JSON so the worker can read
  // them at dispatch time.
  TriggerMetadata metadata = 6;
}

// TriggerMetadata is the open extension point on TriggerUpdate for config
// types whose payload is operator-supplied rather than DB-derived. Add
// per-config-type sub-messages here as new types appear.
message TriggerMetadata {
  // Set when config_type=CONFIG_TYPE_DATAPLANE_UPGRADE.
  DataplaneUpgradeTrigger dataplane_upgrade = 1;
}

// DataplaneUpgradeTrigger is the operator-provided payload for one
// MP-initiated remote DP upgrade attempt. Mirrors the fields persisted
// into liaison_configuration_events.metadata when the event is written.
message DataplaneUpgradeTrigger {
  // tare release tag to upgrade to (e.g. "v0.4.1"). Shared across Liaison,
  // EG, AIGW, data-plane Envoy, and ExtProc.
  string target_version = 1;
  // Bypass the data-plane HA preflight. UI gates this to Super Admin.
  bool allow_downtime = 2;
  // Optional reference to a stored credential bundle (v1.x retry-with-creds
  // flow). Empty for the initial trigger; populated when the operator
  // resubmits after a NeedsCredentials reject.
  string credentials_ref = 3;
  // When true on a retry-with-creds trigger, the worker preserves the
  // referenced service-secret row after delivery so subsequent upgrades
  // in this workspace can reuse it without re-prompting.
  bool persist_in_cluster = 4;
  // Check names the operator has accepted via the dashboard "proceed
  // anyway" flow. Matching Preflight error rows are downgraded to
  // warnings on the next attempt so the upgrade proceeds; the row stays
  // in status.preflightChecks for audit.
  //
  // Persistence is implicit: the dashboard reads the previous event's
  // metadata to pre-check the blocker modal on retry, so "remember this"
  // is the same code path as "include in this trigger". No parallel
  // storage.
  repeated string acknowledged_preflight_checks = 5;
}

// TriggerUpdateResponse confirms the update trigger.
message TriggerUpdateResponse {
  // Configuration that was generated/updated.
  Configuration configuration = 1;

  // Whether a new configuration was generated.
  bool configuration_updated = 2;
}

// ListConfigurationsRequest lists configurations with filtering.
message ListConfigurationsRequest {
  // Filter by user ID (optional).
  string user_id = 1;

  // Filter by config type (optional).
  ConfigType config_type = 2;

  // Pagination.
  int32 page_size = 3;
  // Page token returned by a prior call's next_page_token.
  string page_token = 4;
}

// ListConfigurationsResponse returns list of configurations.
message ListConfigurationsResponse {
  // Configurations matching the request filters for this page.
  repeated Configuration configurations = 1;
  // Token to pass as page_token to fetch the next page; empty when no more.
  string next_page_token = 2;
}

// ListPendingEventsRequest requests list of pending configuration events.
message ListPendingEventsRequest {
  // Maximum number of events to return.
  // If not specified, defaults to 100.
  int32 limit = 1;

  // The liaison's out-of-band observation of the tare-doctor CronJob,
  // piggybacked on the poll. Lets MP surface *why* the doctor stopped
  // reporting — the doctor can't report when it is itself the broken
  // component, so the (live) worker reports on its behalf. Empty on legacy
  // workers and until the worker's first doctor sweep.
  DoctorRuntimeStatus doctor_runtime = 2;
}

// DoctorRuntimeStatus is the liaison worker's backup view of the tare-doctor
// CronJob's runtime health, derived from in-cluster Job/pod state. Used to
// explain a stale doctor report rather than to gate anything.
message DoctorRuntimeStatus {
  // Worker pod identity (hostname / pod name) of the reporting instance.
  string instance_id = 1;
  // Running liaison build version, from the worker binary's embedded version.
  string tare_version = 2;
  // Verdict for the doctor CronJob: "ok" (recent success / currently running),
  // "failing" (last Job failed), or "suspended".
  string state = 3;
  // Human-readable failure reason when state="failing" (e.g.
  // "ImagePullBackOff", "CrashLoopBackOff", "exit code 1").
  string reason = 4;
  // CronJob's last successful completion; unset if never / unknown.
  google.protobuf.Timestamp last_successful_run = 5;
  // When the worker computed this verdict (worker clock).
  google.protobuf.Timestamp observed_at = 6;
}

// ListPendingEventsResponse returns list of pending events.
message ListPendingEventsResponse {
  // Pending configuration events awaiting worker processing.
  repeated ConfigurationEvent events = 1;
}

// ReconcileWorkspaceRequest is the (empty) request for ReconcileWorkspace.
// Target workspace is resolved from the caller's auth claims.
message ReconcileWorkspaceRequest {}

// ReconcileWorkspaceResponse reports how many slot events the sweep
// enqueued. replayed_total is the sum of the per-bucket counts.
message ReconcileWorkspaceResponse {
  // Total slot events enqueued across all buckets (sum of the fields below).
  int32 replayed_total = 1;
  // Events enqueued for the global (system-wide) config slot.
  int32 replayed_global = 2;
  // Events enqueued for per-user config slots.
  int32 replayed_per_user = 3;
  // Events enqueued for per-user, per-key config slots.
  int32 replayed_per_user_per_key = 4;
}

// FullSyncWorkspaceRequest is the (empty) request for FullSyncWorkspace.
// Target workspace and customer are resolved from the caller's auth claims.
message FullSyncWorkspaceRequest {}

// FullSyncWorkspaceResponse reports how many slot events the full sync
// enqueued, scoped to per-user buckets (global is handled by drift sweep).
message FullSyncWorkspaceResponse {
  // Total per-user slot events enqueued (sum of the fields below).
  int32 replayed_total = 1;
  // Events enqueued for per-user config slots.
  int32 replayed_per_user = 2;
  // Events enqueued for per-user, per-key config slots.
  int32 replayed_per_user_per_key = 3;
}

// ConfigurationEvent represents a configuration change event.
message ConfigurationEvent {
  // Unique event identifier.
  string id = 1;

  // User ID who owns this configuration.
  string user_id = 2;

  // Configuration type.
  ConfigType config_type = 3;

  // Event type (new/update/delete).
  string event_type = 4;

  // Optional key ID for user-specific configurations.
  string key_id = 5;

  // Reference to the configuration in liaison_configurations table.
  string config_id = 6;

  // Configuration version number.
  int64 config_version = 7;

  // Event status (pending/acknowledged/applied/failed).
  string status = 8;

  // When the event was triggered.
  google.protobuf.Timestamp triggered_at = 9;

  // Retry count for failed events.
  int32 retry_count = 10;

  // Workspace (data plane) this event is scoped to. Empty for legacy
  // unscoped events written before multi-data-plane support.
  string workspace_id = 11;

  // Raw event metadata JSON from liaison_configuration_events.metadata.
  // Most config types leave this empty (the worker reads config_data via
  // GetConfiguration instead). DATAPLANE_UPGRADE events carry their
  // trigger payload here so the worker can dispatch off the event directly.
  string metadata = 12;
}

// AcknowledgeEventRequest acknowledges an event.
message AcknowledgeEventRequest {
  // Event ID to acknowledge.
  string event_id = 1;

  // Worker ID that is acknowledging the event.
  string worker_id = 2;
}

// AcknowledgeEventResponse confirms acknowledgment.
message AcknowledgeEventResponse {
  // True if the event was acknowledged (claimed) by this worker.
  bool success = 1;
}

// CompleteEventRequest marks an event as complete (applied or failed).
message CompleteEventRequest {
  // Event ID to complete.
  string event_id = 1;

  // Whether the event was successfully applied.
  bool success = 2;

  // Error message if failed.
  string error_message = 3;
}

// CompleteEventResponse confirms completion.
message CompleteEventResponse {
  // True if the event status was updated to applied or failed.
  bool success = 1;
}

// ListActiveUsersRequest requests list of active users.
message ListActiveUsersRequest {
  // Maximum number of users to return.
  // If not specified, defaults to 100.
  int32 limit = 1;
}

// ListActiveUsersResponse returns list of active user IDs.
message ListActiveUsersResponse {
  // User IDs that currently have active configurations.
  repeated string user_ids = 1;
}

// Configuration represents a liaison configuration entry.
message Configuration {
  // Unique identifier.
  string id = 1;

  // User ID who owns this configuration.
  // "system" for global configuration.
  string user_id = 2;

  // Configuration type.
  ConfigType config_type = 3;

  // Configuration data as JSON.
  // The structure depends on config_type:
  // - BYOK: AIProvidersList with BYOK keys
  // - MCP_PROFILE: MCPProfile list
  // - OAUTH_TOKEN: OAuth token metadata
  // - GLOBAL_ROUTES: Global route map
  // - FALLBACK: FallbackSettings
  // - TRAFFIC_SPLIT: TrafficSplitSettings
  string config_data = 4;

  // Version number (monotonically increasing).
  // Used by workers to detect changes.
  int64 version = 5;

  // Optional key ID for user-specific configurations.
  string key_id = 6;

  // Timestamps.
  google.protobuf.Timestamp created_at = 7;
  // When this configuration was last modified.
  google.protobuf.Timestamp updated_at = 8;
}

// ProviderBackend represents a single BYOK provider backend.
message ProviderBackend {
  // Backend name (e.g., "fsbb-anthropic-owner-keyid").
  string name = 1;

  // Owner/user ID.
  string owner = 2;

  // Provider name (e.g., "anthropic").
  string provider = 3;

  // Traffic weight for load balancing.
  int32 weight = 4;

  // Fallback priority (lower is higher priority).
  int32 priority = 5;

  // BYOK policy ("none", "always", "primary", "fallback").
  string byok_policy = 6;

  // Whether provider is external (not managed by TARS).
  bool is_external = 7;

  // Model name in this provider.
  string model_name = 8;
}

// SaveBYOKBackendsRequest saves BYOK backends configuration.
message SaveBYOKBackendsRequest {
  // User ID who owns these backends.
  string user_id = 1;

  // Key address (owner identifier) for these backends.
  string key_address = 2;

  // List of provider backends to save.
  repeated ProviderBackend backends = 3;
}

// SaveBYOKBackendsResponse confirms the save operation.
message SaveBYOKBackendsResponse {
  // Configuration that was saved.
  Configuration configuration = 1;

  // Whether the configuration was created or updated.
  bool updated = 2;
}

// DeleteBYOKBackendsRequest deletes BYOK backends configuration.
message DeleteBYOKBackendsRequest {
  // User ID who owns these backends.
  string user_id = 1;
}

// DeleteBYOKBackendsResponse confirms the delete operation.
message DeleteBYOKBackendsResponse {
  // Whether the configuration was deleted.
  bool deleted = 1;
}

// RevokeUserTokenRequest revokes a user token by JTI.
message RevokeUserTokenRequest {
  // JWT ID (JTI) of the token to revoke.
  string jti = 1;

  // User ID who owns the token being revoked.
  // Used to populate revoked_by field.
  string user_id = 2;

  // Optional reason for revocation.
  optional string reason = 3;
}

// RevokeUserTokenResponse confirms the revocation.
message RevokeUserTokenResponse {
  // Whether the token was successfully revoked.
  bool success = 1;

  // When the token was revoked.
  google.protobuf.Timestamp revoked_at = 2;
}

// Provider represents an AI provider from the database.
message Provider {
  // Provider slug/name (e.g., "openai", "anthropic").
  string slug = 1;

  // Display name for the provider.
  string display_name = 2;

  // API base URL for the provider endpoint.
  string api_base_url = 3;

  // Authentication type (e.g., "api_key", "aws", "vertex").
  string auth_type = 4;

  // Whether the provider is enabled.
  bool is_enabled = 5;

  // Additional metadata as JSON string.
  string metadata = 6;

  // Timestamps.
  google.protobuf.Timestamp created_at = 7;
  // When this provider record was last modified.
  google.protobuf.Timestamp updated_at = 8;
}

// ListActiveProvidersRequest requests list of active AI providers.
message ListActiveProvidersRequest {}

// ListActiveProvidersResponse returns list of active AI providers.
message ListActiveProvidersResponse {
  // Active (enabled) AI providers from the database.
  repeated Provider providers = 1;
}

// SetDataplaneURLRequest registers (or updates) the customer-facing gateway
// URL for a data plane.
message SetDataplaneURLRequest {
  // Workspace ID of the data plane whose URL is being set. Must match the
  // workspace claim on the caller's service-account token.
  string workspace_id = 1;

  // Full gateway URL (scheme + host + optional port), e.g.
  // "https://router.acmecorp.tetrate.ai".
  string url = 2;

  // When true, fire a PROXY_URL configuration event after persisting the
  // URL so the running data plane converges on the new value via the
  // worker's event loop. Dashboard admin edits set this; `tare install`
  // leaves it false because the data plane is being created and the chart
  // populates tars-config[proxy-url] directly at install time.
  bool trigger_update = 3;
}

// SetDataplaneURLResponse confirms the URL was persisted.
message SetDataplaneURLResponse {}

// UpsertMCPCatalogServerRequest carries the operator-provided URL plus
// the catalog-seed fields needed to populate a new row in
// mcp_catalog_servers. See the RPC comment above for semantics.
message UpsertMCPCatalogServerRequest {
  // Workspace ID — must match the workspace claim on the caller's SA
  // token. API-key callers (admin scope) are trusted across workspaces.
  string workspace_id = 1;

  // Stable identifier — matches mcp_catalog_servers.id. Comes from the
  // catalog seed JSON; the operator picks the server name on the
  // command line (`tare mcp enable jira`).
  string id = 2;

  // In-cluster (or public) URL the operator wants the dashboard to use
  // as this server's backend endpoint. The only field the operator
  // explicitly enters — everything else flows from the seed JSON.
  string url = 3;

  // Seed-derived display fields. Used only when the row is being
  // INSERTed (or when force_overwrite=true); ignored on sparse updates
  // of an existing row.
  string name = 4;
  // Human-readable description of the MCP server.
  string description = 5;
  // URL of the server's display icon.
  string icon_url = 6;
  // Catalog category tags the server is grouped under.
  repeated string categories = 7;

  // Auth metadata, also seed-derived. Same write semantics as the
  // display fields.
  string authentication = 8;
  // Whether clients must authenticate to use this server.
  bool requires_auth = 9;

  // When true, every field above (including url) replaces the existing
  // row, clobbering dashboard edits. Default false: sparse update —
  // only url + is_enabled (+ updated_at) move on an existing row.
  bool force_overwrite = 10;
}

// UpsertMCPCatalogServerResponse describes the outcome.
message UpsertMCPCatalogServerResponse {
  // True if a new row was inserted; false if an existing row was updated.
  bool created = 1;
}

// ValidateDataplaneRequest is sent by a data plane's filter (via the
// worker's authenticated channel) to confirm this DP is still active and
// fetch the customer's gatekeeper public keys.
//
// The caller is identified by the SA JWT it signs with its DP keypair
// (kid=`instance-<...>`); MP's auth layer looks the kid up in the
// `keypairs` table and rejects the call with Unauthenticated when the row
// is soft-deleted. No fields are required — everything MP needs is in the
// authenticated claims.
message ValidateDataplaneRequest {}

// ValidateDataplaneResponse returns the customer's current gatekeeper
// public keys. The filter uses these to verify user-token (API key) JWTs
// when its in-process key cache is cold.
message ValidateDataplaneResponse {
  // Gatekeeper public keys for the customer this DP belongs to. Contains
  // the customer-shared keypair rows (kid has no `instance-` prefix);
  // the DP's own identity key is not included.
  repeated GatekeeperPublicKey gatekeeper_public_keys = 1;
}

// GatekeeperPublicKey is a single customer-shared keypair entry the
// filter uses to verify API-key JWTs.
message GatekeeperPublicKey {
  // Key ID, matches the `kid` header on API-key JWTs.
  string kid = 1;
  // JWS algorithm (e.g., "ES256").
  string alg = 2;
  // JWK `use` value (e.g., "sig").
  string use = 3;
  // PEM-encoded public key.
  string public_key_pem = 4;
}

// Full replacement of the metadata JSON on an existing event row. The
// handler does NOT merge keys — callers send the complete intended state.
// Workspace ownership is enforced server-side against the caller's claims.
// Upgrade workers use this to push TarsUpgrade.status changes back to MP.
message UpdateEventMetadataRequest {
  // Event ID (UUID, matches ConfigurationEvent.id) whose metadata is
  // being replaced.
  string event_id = 1;
  // Full intended metadata JSON. Empty string clears the column.
  string metadata = 2;
}

message UpdateEventMetadataResponse {
  // Server returns success=false when the event doesn't exist or when the
  // caller's workspace doesn't own it. No error code path; callers should
  // log and continue (a stale event id can race with operator-initiated
  // cancellation, which is benign).
  bool success = 1;
}

message CancelDataplaneUpgradeRequest {
  // Upgrade event id whose in-flight CR should be cancelled.
  string upgrade_event_id = 1;
}

message CancelDataplaneUpgradeResponse {
  bool success = 1;
}

message RollbackDataplaneUpgradeRequest {
  // Upgrade event id of the completed upgrade being rolled back. Used
  // for audit + display ("rollback of upgrade <id>") and so the worker
  // can resolve the workspace + target revision from the original event
  // metadata.
  string upgrade_event_id = 1;
}

message RollbackDataplaneUpgradeResponse {
  bool success = 1;
  // Event id of the new rollback event. The dashboard polls this id to
  // watch the rollback Job progress.
  string rollback_event_id = 2;
}

// DataplaneUpgradeCredential is one slot of operator-supplied registry
// credentials. Username + password are passed as cleartext to MP over
// TLS, encrypted at rest, and only ever returned to the cluster's
// service-account-authenticated worker.
message DataplaneUpgradeCredential {
  // Slot names the registry side these credentials authenticate against.
  // Matches the missingCredentials enum surfaced in TarsUpgrade.status
  // (SourceRegistry / DestRegistryPush).
  string slot = 1;
  // Registry username for this slot (cleartext over TLS, encrypted at rest).
  string username = 2;
  // Registry password/token for this slot (cleartext over TLS, encrypted at rest).
  string password = 3;
  // Registry host override. Empty means use the slot's implicit
  // registry — source uses tare.tetrate.ai, destination is resolved
  // from the customer's image_registry value.
  string registry = 4;
}

message SubmitDataplaneUpgradeCredentialsRequest {
  // Upgrade event id (the liaison_configuration_events.id whose
  // metadata carries the trigger). Workspace claim must match.
  string upgrade_event_id = 1;
  // Operator-supplied registry credentials, one entry per required slot.
  repeated DataplaneUpgradeCredential credentials = 2;
  // When true, the worker creates the in-cluster Secret WITHOUT an
  // ownerReference back to the TarsUpgrade CR, and labels it
  // `tars.tetrate.ai/persistent=true`. The Secret survives the
  // upgrade so future upgrades and image pulls can reuse it.
  //
  // When false (the default), the Secret is short-lived: ownerRef
  // cascade-deletes it when the CR is removed, and the orphan
  // janitor sweeps after terminal phases. Operators who don't tick
  // the form's "save for future upgrades" box get this.
  //
  // Either way, MP DELETEs the ciphertext row from
  // dataplane_upgrade_credentials as soon as AckDataplaneUpgradeCredentials
  // arrives — the secret material never sits in the MP DB after sync
  // completes.
  bool persist_in_cluster = 3;
}

message SubmitDataplaneUpgradeCredentialsResponse {
  // Echoes back the row id of the stored ciphertext for audit log
  // correlation.
  string credentials_id = 1;
  // True if the ciphertext row was stored successfully.
  bool success = 2;
}

message GetDataplaneUpgradeCredentialsRequest {
  // Upgrade event id whose stored credentials to fetch. Workspace claim must match.
  string upgrade_event_id = 1;
}

message GetDataplaneUpgradeCredentialsResponse {
  // Plaintext credentials, decrypted at MP. Caller (worker) is
  // expected to materialise them as a dockerconfigjson Secret in
  // the cluster and call Ack to confirm successful delivery.
  repeated DataplaneUpgradeCredential credentials = 1;
  // Empty when no operator submission has happened yet. Worker
  // treats this as "keep polling" and re-tries on its next tick.
  bool present = 2;
  // Mirrors SubmitDataplaneUpgradeCredentialsRequest.persist_in_cluster.
  // Worker uses this to decide whether to set ownerReferences on the
  // generated Secret.
  bool persist_in_cluster = 3;
}

message AckDataplaneUpgradeCredentialsRequest {
  // Upgrade event id whose credentials were materialised. Workspace claim must match.
  string upgrade_event_id = 1;
  // True when the worker successfully created the in-cluster Secret;
  // MP DELETEs the ciphertext row. False when materialisation failed
  // (e.g. RBAC) — MP keeps the row so the operator's submission
  // isn't lost; worker will retry on its next poll.
  bool delivered = 2;
}

message AckDataplaneUpgradeCredentialsResponse {
  // True if the ciphertext row was deleted (or already gone — idempotent).
  bool success = 1;
}
