Skip to main content

clawft_kernel/workload_kind/
project.rs

1//! The `project` kind (ADR-103 A6, Phase 2 package G): a per-project child
2//! kernel run by the user-daemon supervisor through the `logical` adapter.
3//!
4//! Authority comes from a project certificate chained to the user key, not
5//! from a signed package, so [`WorkloadKind::load`] always refuses: there is
6//! no package directory to verify. The supervisor builds the workload with
7//! [`prepare_project`] instead. Governance: the kind has no permit in any
8//! operator rule set; the only permit is [`project_supervisor_permit`],
9//! limited to the supervisor principal, so `workload.place` of a `project`
10//! stays denied for everyone else.
11//!
12//! [`project_supervisor_permit`]: crate::workload_governance::project_supervisor_permit
13
14#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
15use std::path::Path;
16
17#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
18use clawft_types::placement::engine::{PlacementPolicy, WorkloadRequirements, WorkloadSpec};
19#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
20use clawft_types::placement::{CapabilityId, MemoryDemand, Requirement};
21
22#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
23use crate::workload_ctl::PlaneError;
24#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
25use crate::workload_pkg::TrustAnchors;
26use crate::workload_pkg::{ManifestEnvelope, ManifestError};
27#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
28use crate::workload_runtime::{CAP_PROJECT_LOGICAL, ProjectPayload, VerifiedWorkload, WorkloadSource};
29
30use super::WorkloadKind;
31#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
32use super::KindRegistry;
33
34/// Manifest kind string.
35pub const KIND_PROJECT: &str = "project";
36
37/// Per-project kernels.
38#[derive(Debug, Clone, Copy, Default)]
39pub struct ProjectKind;
40
41impl WorkloadKind for ProjectKind {
42    fn id(&self) -> &'static str {
43        KIND_PROJECT
44    }
45
46    fn health(&self) -> super::HealthSpec {
47        // A child kernel answers its handshake quickly; two misses are enough.
48        super::HealthSpec {
49            interval_ms: 5_000,
50            miss_limit: 2,
51        }
52    }
53
54    /// A project kernel is bound to its user daemon's host: never moved.
55    fn migratable(&self) -> bool {
56        false
57    }
58
59    fn validate(&self, _envelope: &ManifestEnvelope) -> Result<(), ManifestError> {
60        Err(ManifestError::Invalid(
61            "project workloads have no signed package; the user daemon supervisor starts them".into(),
62        ))
63    }
64
65    #[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
66    fn load(
67        &self,
68        _package_dir: &Path,
69        _anchors: &TrustAnchors,
70        _kinds: &KindRegistry,
71    ) -> Result<VerifiedWorkload, PlaneError> {
72        Err(PlaneError::Package(
73            "project workloads are authorised by a project certificate, not a package; \
74             use `project.start` on the user daemon"
75                .into(),
76        ))
77    }
78
79    #[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
80    fn spec(&self, workload: &VerifiedWorkload) -> Result<WorkloadSpec, String> {
81        let capability = match &workload.source {
82            WorkloadSource::Project(p) if p.adapter == "wasmtime-project-v1" => "runtime.project.wasmtime",
83            WorkloadSource::Project(p) if p.adapter == "logical" => CAP_PROJECT_LOGICAL,
84            _ => return Err("unsupported project adapter".into()),
85        };
86        let id = CapabilityId::new(capability).map_err(|e| e.to_string())?;
87        let spec = WorkloadSpec {
88            kind: workload.kind.clone(),
89            name: workload.id.clone(),
90            requirements: WorkloadRequirements {
91                common: vec![Requirement::exact(id)],
92                variants: Vec::new(),
93                memory: MemoryDemand::default(),
94            },
95            policy: PlacementPolicy::default(),
96        };
97        spec.validate().map_err(|e| e.to_string())?;
98        Ok(spec)
99    }
100
101    #[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
102    fn adapters(&self) -> &'static [&'static str] {
103        &["logical", "wasmtime-project-v1"]
104    }
105}
106
107/// Everything [`prepare_project`] checks a project against.
108#[cfg(all(feature = "workload-runtime", feature = "mesh", unix, feature = "native"))]
109pub struct ProjectFacts<'a> {
110    /// The certificate in force for the project. `None` before its first
111    /// registration: the child registers its key on first boot, so the
112    /// workload carries `key_id = "unregistered"` and serial 0.
113    pub cert: Option<&'a clawft_types::project::ProjectCert>,
114    /// The user public key the certificate must chain to.
115    pub user_pubkey: &'a [u8; 32],
116    /// The revocation view over the user chain.
117    pub revocations: &'a crate::project_identity::RevocationView,
118    /// The project id the manifest registered.
119    pub manifest_id: &'a str,
120    /// The manifest's canonical root.
121    pub root: &'a Path,
122    /// Hex hash of the policy in force (`parent_hash`), recorded on the
123    /// workload; may be empty before the first policy export.
124    pub policy_hash: &'a str,
125}
126
127/// Why a project could not be turned into a workload.
128#[cfg(all(feature = "workload-runtime", feature = "mesh", unix, feature = "native"))]
129#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
130pub enum ProjectPrepareError {
131    /// Certificate signature, key id or expiry check failed.
132    #[error("certificate: {0}")]
133    Cert(String),
134    /// The key was revoked or replaced.
135    #[error("identity: {0}")]
136    Identity(String),
137    /// The certificate names another project than the manifest.
138    #[error("certificate is for project {cert}, manifest is {manifest}")]
139    WrongProject {
140        /// Id in the certificate.
141        cert: String,
142        /// Id in the manifest.
143        manifest: String,
144    },
145    /// The root is not a directory.
146    #[error("project root {0} is not a directory")]
147    RootGone(String),
148}
149
150/// Verify the certificate chain and the manifest binding, then build the
151/// workload the `logical` adapter runs. Refuses a revoked or replaced key,
152/// a certificate for another project and a missing root.
153#[cfg(all(feature = "workload-runtime", feature = "mesh", unix, feature = "native"))]
154pub fn prepare_project(f: &ProjectFacts<'_>) -> Result<VerifiedWorkload, ProjectPrepareError> {
155    let (project_id, key_id, serial, user_key_id) = match f.cert {
156        Some(cert) => {
157            crate::project_identity::verify_cert(cert, f.user_pubkey)
158                .map_err(|e| ProjectPrepareError::Cert(e.to_string()))?;
159            f.revocations
160                .check_cert(cert)
161                .map_err(|e| ProjectPrepareError::Identity(e.to_string()))?;
162            if cert.project_id != f.manifest_id {
163                return Err(ProjectPrepareError::WrongProject {
164                    cert: cert.project_id.clone(),
165                    manifest: f.manifest_id.to_owned(),
166                });
167            }
168            (
169                cert.project_id.clone(),
170                cert.project_key_id.clone(),
171                cert.serial,
172                cert.user_key_id.clone(),
173            )
174        }
175        None => (
176            f.manifest_id.to_owned(),
177            "unregistered".to_owned(),
178            0,
179            clawft_types::project::cert::key_id(f.user_pubkey),
180        ),
181    };
182    if !f.root.is_dir() {
183        return Err(ProjectPrepareError::RootGone(f.root.display().to_string()));
184    }
185    Ok(VerifiedWorkload {
186        kind: KIND_PROJECT.into(),
187        id: project_id.clone(),
188        version: format!("cert-{serial}"),
189        source: WorkloadSource::Project(ProjectPayload {
190            adapter: "logical".into(),
191            project_id,
192            key_id,
193            cert_serial: serial,
194            user_key_id,
195            policy_hash: f.policy_hash.to_owned(),
196            root: f.root.to_path_buf(),
197        }),
198    })
199}
200
201/// Health of a running project kernel: its handshake must name the expected
202/// project, and not every shared service may be down.
203pub fn project_healthy(expected_id: &str, handshake_project_id: Option<&str>, shared_all_down: bool) -> bool {
204    handshake_project_id == Some(expected_id) && !shared_all_down
205}