// agent.proto — Control-plane ↔ Agent 通信契约
//
// 安全红线（禁止违反）：
//   1. 所有消息中只使用 dp_uuid 标识数据通道，严禁出现 user_id / email 等用户身份字段。
//   2. ReportUsage 不含目的地址、DNS 查询等任何可还原用户行为的字段（无日志口径）。
//   3. 本文件冻结后，任何 breaking change 须先通过 buf breaking 审查。

syntax = "proto3";

package pangolin.agent.v1;

import "google/protobuf/timestamp.proto";

option go_package = "github.com/wangjia/pangolin/server/internal/pb/pangolin/agent/v1;agentv1";

// ─── Service ──────────────────────────────────────────────────────────────────

// AgentService 是控制面暴露给 Agent 的唯一 gRPC 接口。
// 连接方向：Agent 主动拨号控制面。
// 除 Enroll 外，所有 RPC 均需通过 mTLS 客户端证书鉴权。
service AgentService {
  // Enroll 供新节点申请证书。唯一不要求 mTLS 客户端证书的入口。
  // Agent 提交 bootstrap_token（云初始化注入）与 CSR，控制面签发短期证书。
  rpc Enroll(EnrollRequest) returns (EnrollResponse);

  // Register 是 mTLS 建立后的首次调用，返回该节点的全量配置快照。
  // ConfigSnapshot 直接作为响应类型（非 RegisterResponse），是有意设计——快照兼用于
  // ApplyConfig 下发，避免重复定义。buf:lint:ignore 声明如下。
  // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME
  rpc Register(RegisterRequest) returns (ConfigSnapshot);

  // Heartbeat 由 Agent 每 30 秒发送一次，上报实时负载并与控制面对账配置版本。
  // 若 need_full_resync == true，Agent 应主动重新调用 Register。
  rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);

  // Subscribe 建立控制面 → Agent 的服务端流，用于下发 Command。
  // Agent 重连时携带 last_command_id，控制面补发缺失命令（at-least-once 语义）。
  // Command 直接作为流元素类型（非 SubscribeResponse），是有意设计——下游消费者
  // 直接操作 Command，无需额外包装。buf:lint:ignore 声明如下。
  // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME
  rpc Subscribe(SubscribeRequest) returns (stream Command);

  // Ack 用于 Agent 向控制面回执命令的执行结果。
  rpc Ack(AckRequest) returns (AckResponse);

  // ReportUsage 上报各数据通道的流量用量，供计费/限速使用。
  // 红线：items 中只含 dp_uuid 与字节/分钟计数，不含目的地址或 DNS 字段。
  rpc ReportUsage(ReportUsageRequest) returns (ReportUsageResponse);
}

// ─── Enroll ───────────────────────────────────────────────────────────────────

message EnrollRequest {
  // bootstrap_token 由云初始化（cloud-init）注入，单次有效，控制面验证后即失效。
  string bootstrap_token = 1;
  // csr_pem 是 Agent 生成的 X.509 证书签名请求（PEM 编码）。
  string csr_pem = 2;
}

message EnrollResponse {
  // cert_pem 是控制面签发的节点证书（PEM 编码）。
  string cert_pem = 1;
  // ca_pem 是控制面 CA 证书链（PEM 编码），供 Agent 验证后续 TLS。
  string ca_pem = 2;
  // not_after 是证书过期时间，Agent 应在到期前主动轮换。
  google.protobuf.Timestamp not_after = 3;
}

// ─── Register ─────────────────────────────────────────────────────────────────

message RegisterRequest {
  // agent_version 是 Agent 的语义化版本字符串，例如 "1.2.3"。
  string agent_version = 1;
  // capabilities 描述 Agent 支持的功能集，例如 ["hysteria2", "reality"]。
  repeated string capabilities = 2;
}

// ─── Heartbeat ────────────────────────────────────────────────────────────────

message HeartbeatRequest {
  // load 是节点实时负载快照。
  NodeLoad load = 1;
  // config_version 是 Agent 当前持有的配置版本号，用于对账。
  uint64 config_version = 2;
}

message HeartbeatResponse {
  // need_full_resync 为 true 时，Agent 应重新调用 Register 获取全量配置。
  bool need_full_resync = 1;
}

// NodeLoad 节点实时负载，由 Heartbeat 携带。
message NodeLoad {
  // online_count 是当前在线连接数（代理隧道数）。
  uint32 online_count = 1;
  // bandwidth_up 是上行带宽（bytes/s）。
  uint64 bandwidth_up = 2;
  // bandwidth_down 是下行带宽（bytes/s）。
  uint64 bandwidth_down = 3;
  // cpu_percent 是 CPU 使用率（0–100）。
  float cpu_percent = 4;
}

// ─── Subscribe ────────────────────────────────────────────────────────────────

message SubscribeRequest {
  // last_command_id 是 Agent 已成功处理的最后一个命令 ID。
  // 首次订阅传 0，控制面将从头补发未 Ack 的命令。
  uint64 last_command_id = 1;
}

// ─── Command ──────────────────────────────────────────────────────────────────

// Command 是控制面下发给 Agent 的指令，at-least-once 投递语义。
// command_id 单调递增，Agent 根据 last_command_id 实现幂等去重。
message Command {
  // command_id 全局单调递增，用于续传与去重。
  uint64 command_id = 1;

  oneof payload {
    UpsertCredential upsert_credential = 2;
    RevokeCredential revoke_credential = 3;
    RotateCredential rotate_credential = 4;
    ApplyConfig apply_config = 5;
    Lifecycle lifecycle = 6;
  }
}

// UpsertCredential 新增或更新数据通道凭证。
// 红线：只含 dp_uuid，不含 user_id / email。
message UpsertCredential {
  // dp_uuid 是数据通道的唯一标识符（Data-Plane UUID）。
  string dp_uuid = 1;
  // expires_at 是凭证到期时间，到期后 Agent 应拒绝该 dp_uuid 的连接请求。
  google.protobuf.Timestamp expires_at = 2;
}

// RevokeCredential 立即吊销数据通道凭证，Agent 应断开对应连接。
// 红线：只含 dp_uuid，不含 user_id / email。
message RevokeCredential {
  string dp_uuid = 1;
}

// RotateCredential 原子替换凭证，grace_seconds 内新旧 dp_uuid 均有效。
// 红线：只含 dp_uuid，不含 user_id / email。
message RotateCredential {
  string old_dp_uuid = 1;
  string new_dp_uuid = 2;
  // grace_seconds 是新旧凭证共存的宽限期（秒），到期后 old_dp_uuid 自动失效。
  uint32 grace_seconds = 3;
}

// ApplyConfig 将一份完整的 ConfigSnapshot 应用到 Agent。
message ApplyConfig {
  ConfigSnapshot config = 1;
}

// Lifecycle 控制节点生命周期。
message Lifecycle {
  enum Action {
    ACTION_UNSPECIFIED = 0;
    // ACTION_DRAIN 停止接受新连接，等待存量连接自然结束。
    ACTION_DRAIN = 1;
    // ACTION_SHUTDOWN 立即停止所有服务并退出进程。
    ACTION_SHUTDOWN = 2;
  }
  Action action = 1;
}

// ─── ConfigSnapshot ───────────────────────────────────────────────────────────

// ConfigSnapshot 是节点运行所需的全量配置，由 Register 返回或 ApplyConfig 下发。
message ConfigSnapshot {
  // config_version 单调递增，Agent 使用此版本号与控制面对账。
  uint64 config_version = 1;
  // credentials 是当前有效的数据通道凭证列表。
  // 红线：只含 dp_uuid，不含 user_id / email。
  repeated CredentialEntry credentials = 2;
  // reality_sni 是 REALITY 协议使用的 SNI 域名（例如 "www.example.com"）。
  string reality_sni = 3;
  // hy2_port_start / hy2_port_end 是 Hysteria2 监听的 UDP 端口范围。
  uint32 hy2_port_start = 4;
  uint32 hy2_port_end = 5;
}

// CredentialEntry 单条数据通道凭证。
// 红线：只含 dp_uuid 与有效期，不含 user_id / email。
message CredentialEntry {
  string dp_uuid = 1;
  google.protobuf.Timestamp expires_at = 2;
}

// ─── Ack ──────────────────────────────────────────────────────────────────────

message AckRequest {
  uint64 command_id = 1;
  bool ok = 2;
  // error_message 仅在 ok == false 时填写，描述失败原因。
  string error_message = 3;
}

message AckResponse {}

// ─── ReportUsage ──────────────────────────────────────────────────────────────

message ReportUsageRequest {
  repeated UsageItem items = 1;
}

// UsageItem 单个数据通道的用量记录。
// 红线：只含 dp_uuid 与流量计数，不含目的地址、DNS 查询等可还原用户行为的字段。
message UsageItem {
  string dp_uuid = 1;
  uint64 bytes_up = 2;
  uint64 bytes_down = 3;
  uint32 minutes = 4;
}

message ReportUsageResponse {}
