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}
63
64/// Response from internal add-peer operation
65#[derive(Debug, Serialize, ToSchema)]
66pub struct InternalAddPeerResponse {
67 /// Whether the operation succeeded
68 pub success: bool,
69 /// Optional message
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub message: Option<String>,
72}
73
74/// Op type for the secrets Raft state machine.
75///
76/// Replicated through openraft alongside the existing scheduler ops.
77/// `zlayer-consensus` carries the bytes; `zlayer-secrets`'s `raft_sm.rs`
78/// applies them. The variants intentionally mirror the structure of
79/// [`crate::storage::NodeIdentity`], [`crate::storage::WrappedDek`], and
80/// [`crate::storage::ReplicatedSecret`] so the wire shape is identical to
81/// the stored shape.
82#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
83#[serde(rename_all = "snake_case")]
84pub enum SecretsRaftOp {
85 /// Register a new node. Triggers an automatic re-wrap of the current
86 /// DEK so the new node can decrypt secrets going forward.
87 RegisterNode {
88 /// Identity payload (uuid, X25519 pubkey, WG pubkey, `joined_at`).
89 identity: crate::storage::NodeIdentity,
90 },
91
92 /// Soft-revoke a node. Followers stop including it in DEK wraps; the
93 /// next `RotateDek` excludes it permanently.
94 RevokeNode {
95 /// Cluster-wide node UUID being revoked.
96 node_id: String,
97 },
98
99 /// Rotate the cluster DEK. The leader proposes a new generation with
100 /// fresh per-node wraps; followers re-encrypt every `ReplicatedSecret`
101 /// from the previous generation to the new one.
102 RotateDek {
103 /// New wrapped-DEK envelope (generation + per-node wraps).
104 new_wraps: crate::storage::WrappedDek,
105 },
106
107 /// Insert or update a secret. The ciphertext is encrypted under the
108 /// `dek_generation` recorded inside the payload.
109 PutSecret {
110 /// The full replicated secret record.
111 secret: crate::storage::ReplicatedSecret,
112 },
113
114 /// Remove a secret entirely. Hard delete — re-encryption skips it.
115 DeleteSecret {
116 /// `"{scope}:{name}"` storage key, same shape as elsewhere.
117 storage_key: String,
118 },
119
120 /// Revoke a specific issued join token (cannot be unrevoked).
121 ///
122 /// The token is identified by `token_hash`, which is the lowercase
123 /// hex SHA-256 of the full token envelope b64 string (same hash form
124 /// regardless of token format — Ed25519-signed envelope, HS256-JWT,
125 /// or future EdDSA-JWT). The entry auto-expires at `expires_at` so
126 /// the revocation table stays bounded by the un-expired token horizon.
127 RevokeToken {
128 /// Lowercase hex SHA-256 of the full token b64 envelope string.
129 token_hash: String,
130 /// Wall-clock instant at which the revocation entry may be pruned.
131 /// Should match the token's own `exp` claim so the entry is no
132 /// longer needed once the token would have expired anyway.
133 #[schema(value_type = String, format = "date-time")]
134 expires_at: chrono::DateTime<chrono::Utc>,
135 },
136
137 /// Import a foreign cluster's trust bundle so its tokens can be
138 /// accepted by validators on this cluster.
139 ///
140 /// Idempotent: re-importing the same `cluster_domain` overwrites
141 /// the previous entry. Keyed by `cluster_domain` to enforce one
142 /// trust relationship per foreign cluster.
143 ImportTrustBundle {
144 /// The bundle to record in `SecretsState::trusted_bundles`.
145 bundle: crate::api::cluster::TrustBundle,
146 },
147
148 /// Remove a previously-imported trust bundle.
149 ///
150 /// No-op if `cluster_domain` was not present. Used by the operator
151 /// when revoking trust in a federated cluster.
152 RemoveTrustBundle {
153 /// Cluster domain of the bundle to remove.
154 cluster_domain: String,
155 },
156
157 /// Set the cluster-wide JWT algorithm policy.
158 ///
159 /// Replicated through Raft so every node enforces the same policy
160 /// within one commit. Idempotent — re-applying with the same value
161 /// is a no-op.
162 SetJwtAlgorithm {
163 /// New policy.
164 algorithm: crate::api::cluster::JwtAlgorithm,
165 },
166
167 /// Mark `{data_dir}/join_secret` as wiped on every node.
168 ///
169 /// Operator-driven cleanup after migrating to `eddsa`. The actual
170 /// file-system delete happens locally on each node when this op
171 /// applies; the state machine records the wipe timestamp so
172 /// re-applies are no-ops. Idempotent.
173 WipeJoinSecret,
174}