Skip to main content

pf_core/
manifest.rs

1// SPDX-License-Identifier: MIT
2//! `.pfimg` manifest format (schema v1).
3//!
4//! See `agent_docs/architecture.md` §4.2 for the canonical schema.
5
6use crate::digest::Digest256;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Wire mediatype for the v1 manifest.
11pub const MEDIATYPE_V1: &str = "application/vnd.processfork.image.v1+json";
12
13/// Top-level `.pfimg` manifest.
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Manifest {
16    /// Manifest schema version. Always `1` for now.
17    pub schema_version: u32,
18    /// OCI-style mediatype, see [`MEDIATYPE_V1`].
19    pub media_type: String,
20    /// Which agent kind produced this image (`claude-code`, `langgraph`, …).
21    pub agent: AgentInfo,
22    /// Pointers to the four content-addressed layer blobs.
23    pub model: ModelLayer,
24    /// KV-cache layer descriptor.
25    pub cache: CacheLayer,
26    /// World layer descriptor (FS, env, processes).
27    pub world: WorldLayer,
28    /// Effect-ledger descriptor.
29    pub effects: EffectsLayer,
30    /// Reasoning trace (chat messages, tool-call log).
31    pub trace: TraceLayer,
32    /// When this image was sealed.
33    pub created_at: DateTime<Utc>,
34    /// Parent image digests (zero, one, or two — two means a merge).
35    #[serde(default)]
36    pub parents: Vec<Digest256>,
37}
38
39/// Identifies the agent runtime that produced this image.
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct AgentInfo {
42    /// Runtime kind (`claude-code`, `langgraph`, `vllm`, …).
43    pub kind: String,
44    /// Runtime version string.
45    pub version: String,
46    /// Opaque fingerprint of the runtime build (used to gate restore).
47    pub fingerprint: String,
48}
49
50/// Pointers to the model-layer blobs.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct ModelLayer {
53    /// Digest of the base-model weights (often shared across many images).
54    pub base: Digest256,
55    /// Digest of the diff blob (LoRA / IA³ / full delta).
56    pub diff: Digest256,
57}
58
59/// Pointers to the KV-cache layer blobs.
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct CacheLayer {
62    /// On-disk page layout identifier; default `paged-batchinvariant-v1`.
63    pub layout: String,
64    /// Digest of the page-manifest blob.
65    pub manifest: Digest256,
66}
67
68/// Pointers to the world-layer (FS / env / processes) blobs.
69#[derive(Clone, Debug, Serialize, Deserialize)]
70pub struct WorldLayer {
71    /// Digest of the filesystem snapshot.
72    pub fs: Digest256,
73    /// Digest of the captured environment (env vars + cwd).
74    pub env: Digest256,
75    /// Digest of the captured in-flight process state (CRIU dump).
76    pub procs: Digest256,
77}
78
79/// Pointer to the effects-ledger blob.
80#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct EffectsLayer {
82    /// Digest of the append-only effect ledger.
83    pub ledger: Digest256,
84}
85
86/// Pointer to the reasoning-trace blob.
87#[derive(Clone, Debug, Serialize, Deserialize)]
88pub struct TraceLayer {
89    /// Digest of the chat / tool-call message log.
90    pub messages: Digest256,
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn round_trip_through_json() {
99        let d = Digest256::of(b"x");
100        let m = Manifest {
101            schema_version: 1,
102            media_type: MEDIATYPE_V1.to_owned(),
103            agent: AgentInfo {
104                kind: "claude-code".into(),
105                version: "0.1.0".into(),
106                fingerprint: "test".into(),
107            },
108            model: ModelLayer {
109                base: d.clone(),
110                diff: d.clone(),
111            },
112            cache: CacheLayer {
113                layout: "paged-batchinvariant-v1".into(),
114                manifest: d.clone(),
115            },
116            world: WorldLayer {
117                fs: d.clone(),
118                env: d.clone(),
119                procs: d.clone(),
120            },
121            effects: EffectsLayer { ledger: d.clone() },
122            trace: TraceLayer {
123                messages: d.clone(),
124            },
125            created_at: Utc::now(),
126            parents: vec![],
127        };
128        let s = serde_json::to_string(&m).unwrap();
129        let back: Manifest = serde_json::from_str(&s).unwrap();
130        assert_eq!(back.schema_version, 1);
131        assert_eq!(back.media_type, MEDIATYPE_V1);
132    }
133}