Skip to main content

systemprompt_models/documents/
drive.rs

1//! The bridge drive sync protocol.
2//!
3//! A bridge device mirrors the documents a user can see into a local
4//! folder. It fetches a [`DriveManifest`] once, then polls
5//! [`DriveChanges`] from the manifest's cursor, downloads blobs by
6//! [`ContentHash`], and uploads local edits as a [`DriveCommitMeta`] plus
7//! blob. A commit whose `base_revision` is not the current head is merged
8//! onto the head when it can be ([`DriveCommitResponse::Merged`] names the
9//! merged revision and its content, which the device then pulls) and is
10//! otherwise answered with [`DriveCommitResponse::Conflict`] and never
11//! applied. [`DriveProposals`] lists the open proposals on the user's
12//! documents and the user's own open proposals.
13//!
14//! The sync runs under its own credential: the device trades its private
15//! device token (audience [`BRIDGE_DEVICE_AUDIENCE`], kept by the bridge and
16//! never handed to a host) for a [`DriveSyncToken`] (audience
17//! [`DRIVE_SYNC_AUDIENCE`]), and the Drive routes accept that token alone.
18//!
19//! Copyright (c) systemprompt.io — Business Source License 1.1.
20//! See <https://systemprompt.io> for licensing details.
21
22use chrono::{DateTime, Utc};
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25use systemprompt_identifiers::{
26    ContentHash, DocumentId, DocumentProposalId, DocumentRevisionId, DriveDeviceId, UserId,
27};
28use systemprompt_provider_contracts::documents::DocumentFormat;
29
30use super::access::ActorKind;
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33pub struct DriveEntry {
34    pub document_id: DocumentId,
35    pub path: String,
36    pub format: DocumentFormat,
37    pub head_revision_id: DocumentRevisionId,
38    pub content_sha256: ContentHash,
39    pub size_bytes: u64,
40    pub updated_at: DateTime<Utc>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
44pub struct DriveManifest {
45    pub generated_at: DateTime<Utc>,
46    pub cursor: String,
47    pub entries: Vec<DriveEntry>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51#[serde(tag = "type", rename_all = "snake_case")]
52pub enum DriveChangeKind {
53    Created,
54    Updated,
55    Renamed { from: String },
56    Deleted,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
60pub struct DriveChange {
61    pub cursor: String,
62    pub document_id: DocumentId,
63    pub path: String,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub revision_id: Option<DocumentRevisionId>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub content_sha256: Option<ContentHash>,
68    pub kind: DriveChangeKind,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72pub struct DriveChanges {
73    pub changes: Vec<DriveChange>,
74    pub next_cursor: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub struct DriveCommitMeta {
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub document_id: Option<DocumentId>,
81    pub path: String,
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub base_revision: Option<DocumentRevisionId>,
84    pub content_sha256: ContentHash,
85    pub size_bytes: u64,
86    pub device_id: DriveDeviceId,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub message: Option<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(tag = "status", rename_all = "snake_case")]
93pub enum DriveCommitResponse {
94    Committed {
95        revision_id: DocumentRevisionId,
96    },
97    Merged {
98        revision_id: DocumentRevisionId,
99        content_sha256: ContentHash,
100    },
101    Conflict {
102        head_revision_id: DocumentRevisionId,
103        head_content_sha256: ContentHash,
104        base_revision_id: Option<DocumentRevisionId>,
105    },
106    Proposed {
107        proposal_id: DocumentProposalId,
108        revision_id: DocumentRevisionId,
109    },
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
113#[serde(rename_all = "snake_case")]
114pub enum PresenceState {
115    Viewing,
116    Editing,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
120#[serde(rename_all = "snake_case")]
121pub enum PresenceSource {
122    Bridge,
123    Agent,
124    Web,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
128pub struct PresenceBeacon {
129    pub document_id: DocumentId,
130    pub state: PresenceState,
131    pub source_kind: PresenceSource,
132    pub source_ref: String,
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    // JSON: editor cursor position; the shape belongs to the client's editor.
135    pub cursor: Option<serde_json::Value>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
139pub struct DriveProposal {
140    pub proposal_id: DocumentProposalId,
141    pub document_id: DocumentId,
142    pub proposer_id: UserId,
143    pub actor_kind: ActorKind,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub message: Option<String>,
146    pub created_at: DateTime<Utc>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
150pub struct DriveProposals {
151    pub proposals: Vec<DriveProposal>,
152}
153
154pub const DRIVE_SYNC_AUDIENCE: &str = "bridge.drive_sync";
155
156pub const BRIDGE_DEVICE_AUDIENCE: &str = "bridge.device";
157
158/// The Drive sync's credential, traded for the device's bridge token.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
160pub struct DriveSyncToken {
161    pub token: String,
162    pub expires_at: DateTime<Utc>,
163}