Skip to main content

clawft_kernel/workload_ctl/
host_node_admin.rs

1//! Node-admin methods on the `workload.ctl` wire (`dashboard.status`,
2//! `dashboard.token.rotate`): the same signed envelope, controller-key check,
3//! expiry and nonce replay guard as every other method, answered by a hook the
4//! daemon installs instead of by a workload adapter.
5//!
6//! Trust is the host's own: only a key in this node's controller list reaches
7//! the hook at all (`workload-host.json`), and the hook may refuse further.
8//! Both ends chain the call (`node_admin.request` / `node_admin.result`) with
9//! the method, the requester and the outcome, never the body or any secret.
10
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use serde_json::{Value, json};
15
16use super::host_service::{WorkloadHostService, refuse};
17use super::msg::{CtlRequest, Refusal, RefusalCode};
18
19/// Chain kind of an accepted node-admin request on the target.
20pub const EVENT_NODE_ADMIN_REQUEST: &str = "node_admin.request";
21/// Chain kind of its outcome.
22pub const EVENT_NODE_ADMIN_RESULT: &str = "node_admin.result";
23
24/// What a node does when an authorised controller asks it to run a node-admin
25/// method. The result is returned to the controller verbatim, so it must not
26/// contain secrets.
27#[async_trait]
28pub trait NodeAdmin: Send + Sync {
29    /// Run `method` for `requester` (its verified node id). `Err` is the
30    /// reason shown to the controller.
31    async fn call(&self, method: &str, requester: &str, body: &Value) -> Result<Value, String>;
32}
33
34impl WorkloadHostService {
35    /// Install the node-admin hook (first call wins).
36    pub fn set_node_admin(&self, hook: Arc<dyn NodeAdmin>) -> bool {
37        self.node_admin.set(hook).is_ok()
38    }
39
40    pub(super) async fn node_admin_op(&self, req: &CtlRequest) -> Result<Value, Refusal> {
41        let Some(hook) = self.node_admin.get() else {
42            return Err(refuse(
43                RefusalCode::UnknownMethod,
44                format!("{}: node administration is not enabled on this node", req.method),
45            ));
46        };
47        self.record(
48            EVENT_NODE_ADMIN_REQUEST,
49            json!({ "node": self.node_id(), "method": req.method, "requester": req.requester,
50                    "decision_id": req.decision_id }),
51        );
52        let out = hook.call(&req.method, &req.requester, &req.body).await;
53        self.record(
54            EVENT_NODE_ADMIN_RESULT,
55            json!({ "node": self.node_id(), "method": req.method, "requester": req.requester,
56                    "decision_id": req.decision_id, "ok": out.is_ok(),
57                    "error": out.as_ref().err() }),
58        );
59        out.map_err(|e| refuse(RefusalCode::Runtime, e))
60    }
61}