zlayer_types/api/internal.rs
1//! Internal API DTOs for scheduler-to-agent communication.
2//!
3//! These types describe the request/response payloads for the internal
4//! endpoints used by the distributed scheduler to trigger operations on
5//! agents. They use a shared secret for authentication rather than JWT
6//! tokens.
7
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11/// Re-export the wire-level scale request from `crate::cluster`.
12///
13/// `InternalScaleRequest` was moved to `zlayer_types::cluster` so the same
14/// Rust type can be shared between the HTTP fan-out path (in
15/// `zlayer-scheduler::cluster`) and the `/internal/scale` handler in
16/// `zlayer-api`. This re-export preserves the original
17/// `zlayer_types::api::internal::InternalScaleRequest` path for downstream
18/// callers (and `pub use ...::internal::*` consumers).
19pub use crate::cluster::{InternalScaleRequest, ScaleAssignment};
20
21/// Response from internal scale operation
22#[derive(Debug, Serialize, ToSchema)]
23pub struct InternalScaleResponse {
24 /// Whether the operation succeeded
25 pub success: bool,
26 /// Service name that was scaled
27 pub service: String,
28 /// New replica count
29 pub replicas: u32,
30 /// Optional message
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub message: Option<String>,
33 /// When set, this agent refused the scale because it cannot run the
34 /// workload's OS (H-7 `RouteToPeer` policy). The value is the OCI-canonical
35 /// OS string the workload requires (`linux` / `windows` / `darwin`). The
36 /// scheduler catches this and re-dispatches to a cluster peer whose
37 /// `NodeState.os` matches.
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub reroute_to_os: Option<String>,
40}
41
42/// Request to add a `WireGuard` peer to the local overlay transport.
43///
44/// Sent by the leader to existing nodes when a new node joins the cluster,
45/// so that all nodes learn about the new peer without waiting for periodic
46/// reconciliation.
47#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
48pub struct InternalAddPeerRequest {
49 /// New peer's `WireGuard` public key (base64)
50 pub wg_public_key: String,
51 /// New peer's overlay IP (e.g. "10.200.0.3")
52 pub overlay_ip: String,
53 /// New peer's `WireGuard` endpoint (e.g. "203.0.113.5:51820")
54 pub endpoint: String,
55 /// When set, this peer is for the named service's *dedicated* overlay
56 /// rather than the global cluster overlay.
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub service: Option<String>,
59 /// Service subnet to plumb into the dedicated peer's `AllowedIPs`.
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub service_subnet: Option<String>,
62 /// NAT-traversal candidates the joining node advertises (host /
63 /// server-reflexive / relay addresses it can be reached on). The leader
64 /// fans these out to existing nodes via `/internal/add-peer`; each node
65 /// hands them to overlayd so `NatTraversal::connect_to_peer` can
66 /// hole-punch / relay toward the new peer when its direct endpoint does not
67 /// establish a `WireGuard` handshake. Empty for a pre-NAT sender;
68 /// `#[serde(default)]` keeps such payloads decoding.
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
70 pub candidates: Vec<crate::nat_wire::NatCandidateWire>,
71}
72
73/// Response from internal add-peer operation
74#[derive(Debug, Serialize, ToSchema)]
75pub struct InternalAddPeerResponse {
76 /// Whether the operation succeeded
77 pub success: bool,
78 /// Optional message
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub message: Option<String>,
81}
82
83/// Op type for the secrets Raft state machine.
84///
85/// Replicated through openraft alongside the existing scheduler ops.
86/// `zlayer-consensus` carries the bytes; `zlayer-secrets`'s `raft_sm.rs`
87/// applies them. The variants intentionally mirror the structure of
88/// [`crate::storage::NodeIdentity`], [`crate::storage::WrappedDek`], and
89/// [`crate::storage::ReplicatedSecret`] so the wire shape is identical to
90/// the stored shape.
91#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum SecretsRaftOp {
94 /// Register a new node. Triggers an automatic re-wrap of the current
95 /// DEK so the new node can decrypt secrets going forward.
96 RegisterNode {
97 /// Identity payload (uuid, X25519 pubkey, WG pubkey, `joined_at`).
98 identity: crate::storage::NodeIdentity,
99 },
100
101 /// Soft-revoke a node. Followers stop including it in DEK wraps; the
102 /// next `RotateDek` excludes it permanently.
103 RevokeNode {
104 /// Cluster-wide node UUID being revoked.
105 node_id: String,
106 },
107
108 /// Rotate the cluster DEK. The leader proposes a new generation with
109 /// fresh per-node wraps; followers re-encrypt every `ReplicatedSecret`
110 /// from the previous generation to the new one.
111 RotateDek {
112 /// New wrapped-DEK envelope (generation + per-node wraps).
113 new_wraps: crate::storage::WrappedDek,
114 },
115
116 /// Insert or update a secret. The ciphertext is encrypted under the
117 /// `dek_generation` recorded inside the payload.
118 PutSecret {
119 /// The full replicated secret record.
120 secret: crate::storage::ReplicatedSecret,
121 },
122
123 /// Remove a secret entirely. Hard delete — re-encryption skips it.
124 DeleteSecret {
125 /// `"{scope}:{name}"` storage key, same shape as elsewhere.
126 storage_key: String,
127 },
128
129 /// Revoke a specific issued join token (cannot be unrevoked).
130 ///
131 /// The token is identified by `token_hash`, which is the lowercase
132 /// hex SHA-256 of the full token envelope b64 string (same hash form
133 /// regardless of token format — Ed25519-signed envelope, HS256-JWT,
134 /// or future EdDSA-JWT). The entry auto-expires at `expires_at` so
135 /// the revocation table stays bounded by the un-expired token horizon.
136 RevokeToken {
137 /// Lowercase hex SHA-256 of the full token b64 envelope string.
138 token_hash: String,
139 /// Wall-clock instant at which the revocation entry may be pruned.
140 /// Should match the token's own `exp` claim so the entry is no
141 /// longer needed once the token would have expired anyway.
142 #[schema(value_type = String, format = "date-time")]
143 expires_at: chrono::DateTime<chrono::Utc>,
144 },
145
146 /// Import a foreign cluster's trust bundle so its tokens can be
147 /// accepted by validators on this cluster.
148 ///
149 /// Idempotent: re-importing the same `cluster_domain` overwrites
150 /// the previous entry. Keyed by `cluster_domain` to enforce one
151 /// trust relationship per foreign cluster.
152 ImportTrustBundle {
153 /// The bundle to record in `SecretsState::trusted_bundles`.
154 bundle: crate::api::cluster::TrustBundle,
155 },
156
157 /// Remove a previously-imported trust bundle.
158 ///
159 /// No-op if `cluster_domain` was not present. Used by the operator
160 /// when revoking trust in a federated cluster.
161 RemoveTrustBundle {
162 /// Cluster domain of the bundle to remove.
163 cluster_domain: String,
164 },
165
166 /// Set the cluster-wide JWT algorithm policy.
167 ///
168 /// Replicated through Raft so every node enforces the same policy
169 /// within one commit. Idempotent — re-applying with the same value
170 /// is a no-op.
171 SetJwtAlgorithm {
172 /// New policy.
173 algorithm: crate::api::cluster::JwtAlgorithm,
174 },
175
176 /// Mark `{data_dir}/join_secret` as wiped on every node.
177 ///
178 /// Operator-driven cleanup after migrating to `eddsa`. The actual
179 /// file-system delete happens locally on each node when this op
180 /// applies; the state machine records the wipe timestamp so
181 /// re-applies are no-ops. Idempotent.
182 WipeJoinSecret,
183}