syntax = "proto3";

package clawker.admin.v1;

option go_package = "github.com/schmitthub/clawker/api/admin/v1";

// AdminService is the gRPC surface the clawker CLI calls on the control
// plane. The CP owns privileged kernel state (BPF programs + pinned maps)
// and the firewall stack (Envoy + CoreDNS) and handles per-container
// firewall lifecycle.
//
// Transport: mTLS over TCP (AdminPort, default 7443, configurable via
// Settings). The CLI presents a client certificate signed by the CLI CA.
// Authorization: OAuth2 access tokens with the uniform "admin" scope,
// validated via Hydra token introspection (RFC 7662).
//
// Method naming follows the `<Domain><Action>[<Object>]` convention.
// Firewall is the only domain today; future domains (Monitor, Hostproxy,
// Clawkerd, ...) layer additional handlers onto the same AdminService so
// the CLI sees one unified surface per CP. Every method is registered for
// "admin" scope enforcement (INV-B2-009).
//
// Response messages are named `Firewall*Result` rather than
// `Firewall*Response` to match Go's `(Result, error)` convention — the
// handler returns typed Result structs from queued closures and maps
// those into the wire messages below.
service AdminService {
  // --- Firewall domain (13 methods) ---

  // FirewallInit brings the firewall stack (Envoy + CoreDNS) up. BPF
  // programs are loaded once at CP startup; this RPC is the idempotent
  // stack-up signal from the CLI. Global — no container_id.
  rpc FirewallInit(FirewallInitRequest) returns (FirewallInitResult);

  // FirewallRemove is global teardown — stops Envoy + CoreDNS, detaches
  // all BPF programs, and flushes all eBPF state plus pending timers.
  rpc FirewallRemove(FirewallRemoveRequest) returns (FirewallRemoveResult);

  // FirewallEnable enrolls a container into the global container_map
  // routing. Idempotent. The CP resolves the container's cgroup path
  // internally via Docker API with drift guard (INV-B2-016) — callers
  // send only container_id.
  rpc FirewallEnable(FirewallEnableRequest) returns (FirewallEnableResult);

  // FirewallDisable removes a container from container_map. BPF links
  // remain attached so re-enable is cheap. BPF fast path exits to bypass
  // on lookup miss.
  rpc FirewallDisable(FirewallDisableRequest) returns (FirewallDisableResult);

  // FirewallBypass = timed Disable + CP-side dead-man timer that calls
  // Enable on expiry. Drift guard inherited from the Enable path.
  rpc FirewallBypass(FirewallBypassRequest) returns (FirewallBypassResult);

  // FirewallAddRules adds egress rules to the store and hot-reloads the
  // stack. Synchronous: returns after the stack is healthy again.
  rpc FirewallAddRules(FirewallAddRulesRequest) returns (FirewallAddRulesResult);

  // FirewallRemoveRule removes a single egress rule by (dst, proto, port)
  // and hot-reloads the stack. Returns NOT_FOUND (sentinel
  // RULE_NOT_FOUND) when no rule matches the key — typos and malformed
  // hostnames both land here, since anything that can't match an
  // existing rule key is a miss.
  rpc FirewallRemoveRule(FirewallRemoveRuleRequest) returns (FirewallRemoveRuleResult);

  // FirewallListRules returns the current normalized/deduplicated rule
  // set from the store. Read-only.
  rpc FirewallListRules(FirewallListRulesRequest) returns (FirewallListRulesResult);

  // FirewallReload regenerates configs and restarts Envoy + CoreDNS from
  // the current rules store state without mutating rules.
  rpc FirewallReload(FirewallReloadRequest) returns (FirewallReloadResult);

  // FirewallStatus is the firewall-domain health snapshot (stack running,
  // Envoy/CoreDNS health, rule count, network topology).
  rpc FirewallStatus(FirewallStatusRequest) returns (FirewallStatusResult);

  // FirewallRotateCA regenerates the MITM CA + per-domain certs. Restarts
  // Envoy so the new chain is picked up.
  rpc FirewallRotateCA(FirewallRotateCARequest) returns (FirewallRotateCAResult);

  // FirewallSyncRoutes atomically replaces the global route_map.
  rpc FirewallSyncRoutes(FirewallSyncRoutesRequest) returns (FirewallSyncRoutesResult);

  // FirewallResolveHostname performs a DNS lookup from inside the CP's
  // network namespace — used to resolve host.docker.internal during
  // per-container enroll.
  rpc FirewallResolveHostname(FirewallResolveHostnameRequest) returns (FirewallResolveHostnameResult);

  // ListAgents returns a snapshot of every agent currently registered
  // with the control plane. Used by `clawker controlplane agents` and
  // diagnostic tooling. Read-only; uniform admin scope.
  rpc ListAgents(ListAgentsRequest) returns (ListAgentsResult);
}

// Route is one entry in the global route_map.
message Route {
  uint32 domain_hash = 1;
  uint32 dst_port = 2;
  uint32 envoy_port = 3;
}

// EgressRule is one egress firewall rule (mirrors config.EgressRule).
message EgressRule {
  string dst = 1;
  string proto = 2;     // "tls" | "tcp" | "http" | "ssh" | "ip" | "cidr"
  uint32 port = 3;
  string action = 4;    // "allow" | "deny"
  repeated PathRule path_rules = 5;
  string path_default = 6;
}

message PathRule {
  string path = 1;
  string action = 2;
}

// ---------------------------------------------------------------------------
// Global firewall lifecycle
// ---------------------------------------------------------------------------

message FirewallInitRequest {}
message FirewallInitResult {
  string envoy_ip = 1;
  string coredns_ip = 2;
  string network_id = 3;
}

message FirewallRemoveRequest {}
message FirewallRemoveResult {}

// ---------------------------------------------------------------------------
// Per-container enforcement — CLI sends only container_id. CP resolves
// cgroup path internally via Docker API + EBPFCgroupPath with drift guard
// (INV-B2-016).
// ---------------------------------------------------------------------------

message FirewallEnableRequest {
  string container_id = 1;
}
message FirewallEnableResult {}

message FirewallDisableRequest {
  string container_id = 1;
}
message FirewallDisableResult {}

message FirewallBypassRequest {
  string container_id = 1;
  uint32 timeout_seconds = 2;
}
message FirewallBypassResult {}

// ---------------------------------------------------------------------------
// Rule management
// ---------------------------------------------------------------------------

message FirewallAddRulesRequest {
  repeated EgressRule rules = 1;
}
message FirewallAddRulesResult {
  int32 added_count = 1;
  // stack_restarted is true when the live stack was reloaded; false when
  // the stack was down at queue-time and only the on-disk rule change
  // applied. In both cases err==nil — the rule is durable.
  bool stack_restarted = 2;
}

message FirewallRemoveRuleRequest {
  string dst = 1;
  string proto = 2;     // "tls" | "tcp" | "http" | "ssh" | "ip" | "cidr"
  uint32 port = 3;
}
message FirewallRemoveRuleResult {
  // stack_restarted is true when the live stack was reloaded; false when
  // the stack was down at queue-time and only the on-disk rule change
  // applied. In both cases err==nil — the rule is durable.
  bool stack_restarted = 1;
}

message FirewallListRulesRequest {}
message FirewallListRulesResult {
  repeated EgressRule rules = 1;
}

message FirewallReloadRequest {}
message FirewallReloadResult {
  bool stack_restarted = 1;
}

// ---------------------------------------------------------------------------
// Status + CA + low-level utilities
// ---------------------------------------------------------------------------

message FirewallStatusRequest {}
message FirewallStatusResult {
  bool running = 1;
  bool envoy_health = 2;
  bool coredns_health = 3;
  int32 rule_count = 4;
  string envoy_ip = 5;
  string coredns_ip = 6;
  string network_id = 7;
  string cidr = 8;
}

message FirewallRotateCARequest {}
message FirewallRotateCAResult {
  // stack_restarted mirrors the AddRules/Reload distinction: true when
  // the live stack was reloaded with the fresh cert chain; false when
  // regen completed but the stack was down so no restart fired.
  bool stack_restarted = 1;
}

message FirewallSyncRoutesRequest {
  // routes is retained for wire compatibility but is IGNORED by the
  // server. Post-queue the server rebuilds the route_map from the
  // current rules store — see
  // internal/controlplane/firewall.Handler.FirewallSyncRoutes for the
  // rationale (coalescing with concurrent AddRules/Reload would
  // silently discard a caller's stale route set).
  repeated Route routes = 1;
}
message FirewallSyncRoutesResult {
  uint32 applied = 1;
}

message FirewallResolveHostnameRequest {
  string hostname = 1;
}
message FirewallResolveHostnameResult {
  repeated string addresses = 1;
}

message ListAgentsRequest {}
message ListAgentsResult {
  repeated Agent agents = 1;
}

message Agent {
  // agent_name is the short agent name as the user types it on the CLI
  // (e.g. "dev"). The canonical "clawker.project.agent" form is composed
  // CP-side from (NamePrefix, project, agent_name) — kept off the wire to
  // avoid duplication and parsing.
  string agent_name = 1;
  // container_id is the long Docker container ID the agent runs in.
  string container_id = 2;
  // cert_thumbprint is the lowercase-hex SHA-256 over the agent's mTLS
  // cert DER — the channel-bound identity key.
  string cert_thumbprint = 3;
  // registered_at_unix is the wall-clock timestamp at AgentService.Connect.
  int64 registered_at_unix = 4;
  // last_seen_unix is updated on every successful per-agent RPC. Equal
  // to registered_at_unix for B4 because Connect is the only per-agent
  // RPC.
  int64 last_seen_unix = 5;
  // project is the clawker project slug the agent registered under (empty
  // for 2-segment naming). Composite with agent_name when callers need a
  // unique key across projects.
  string project = 6;
}
