Skip to main content

acyclic_fs/
wire_service.rs

1//! Canonical generated-wire translation for every remote filesystem deployment.
2
3use crate::kernel::{
4    FileKind as EngineFileKind, FileMetadata, FilePayload, LogicalName, MetadataField,
5    NameEncoding, TreeEntry,
6};
7use crate::model::{FilesystemProfile as EngineProfile, Lifecycle, VolumeConfig};
8use crate::wire::{filesystem::v2 as wire, harness::v1 as harness};
9use crate::{
10    ApplyOptions, AsyncAuthorityStore, AsyncObjectStore, ByteRange, CancellationToken, Digest,
11    DurableCommit, ForkOptions, Fs, Generation, GenerationId, IdempotencyKey, JoinHistory,
12    JoinOutcome, MergeConflict, ObjectId, ObjectKind, Sequence, Transaction, TransactionCommit,
13    TransactionConflict, TransactionConflictRegion, TransactionDependencyUse, TransactionRebase,
14    TransactionSparseSeek, Workspace, WorkspaceDelete, WorkspaceError, WorkspaceExtentKind,
15    WorkspaceMetadata, WorkspaceRebase,
16};
17use bytes::Bytes;
18use futures::{StreamExt, stream};
19use prost::Message;
20use std::pin::Pin;
21use std::sync::Arc;
22use tonic::{Request, Response, Status};
23
24type WireStream<T> = Pin<Box<dyn futures::Stream<Item = Result<T, Status>> + Send + 'static>>;
25
26enum JoinTargetHead<A, O> {
27    Expected {
28        head: crate::Head,
29        existing_operation: bool,
30    },
31    Stale(Generation<A, O>),
32}
33
34/// Hard bounds and explicitly available optional hosted capabilities.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct FilesystemWireLimits {
37    /// Largest accepted encoded request.
38    pub maximum_request_bytes: u64,
39    /// Largest response body or aggregate page.
40    pub maximum_response_bytes: u64,
41    /// Largest atomic transaction.
42    pub maximum_transaction_mutations: u32,
43    /// Largest directory, diff, or transfer page.
44    pub maximum_page_items: u32,
45    /// Longest accepted lifetime for a scoped deployment credential.
46    pub maximum_credential_seconds: u64,
47}
48
49impl Default for FilesystemWireLimits {
50    fn default() -> Self {
51        Self {
52            maximum_request_bytes: 16 * 1024 * 1024,
53            maximum_response_bytes: 16 * 1024 * 1024,
54            maximum_transaction_mutations: 2_048,
55            maximum_page_items: 1_024,
56            maximum_credential_seconds: 3_600,
57        }
58    }
59}
60
61/// Deployment-owned credential surface selected after SDK validation.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum CredentialKind {
64    /// Native mount transport credential.
65    Mount,
66    /// Filesystem S3 compatibility credential.
67    S3,
68}
69
70/// Fully validated scope passed to the authenticated deployment edge.
71#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct CredentialGrantRequest {
73    /// Requested surface.
74    pub kind: CredentialKind,
75    /// Stable workspace identity.
76    pub workspace_id: crate::WorkspaceId,
77    /// Canonical workspace name.
78    pub workspace_name: crate::WorkspaceName,
79    /// Exact generation visible through the credential.
80    pub generation_id: GenerationId,
81    /// Whether mutation is requested.
82    pub writable: bool,
83    /// Requested bounded lifetime.
84    pub expires_after_seconds: u64,
85    /// Stable caller retry identity.
86    pub idempotency_key: IdempotencyKey,
87}
88
89/// Deployment-produced scoped credential material.
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub enum CredentialGrant {
92    /// Opaque bearer capability for the native mount transport.
93    Bearer {
94        /// Customer endpoint for the mount transport.
95        endpoint: String,
96        /// Opaque bearer capability.
97        token: String,
98        /// Absolute Unix expiry enforced by the endpoint.
99        expires_at_unix_seconds: i64,
100    },
101    /// Standard S3 `SigV4` coordinates scoped to one workspace.
102    S3 {
103        /// Customer S3 endpoint.
104        endpoint: String,
105        /// Opaque bucket coordinate.
106        bucket: String,
107        /// Signing region.
108        region: String,
109        /// Scoped access-key identity.
110        access_key_id: String,
111        /// Scoped secret access key.
112        secret_access_key: String,
113        /// Optional STS-compatible session token; empty when unused.
114        session_token: String,
115        /// Absolute Unix expiry enforced by the endpoint.
116        expires_at_unix_seconds: i64,
117    },
118}
119
120/// Authenticated deployment extension for private credential issuance.
121#[tonic::async_trait]
122pub trait FilesystemCredentialIssuer: Send + Sync + 'static {
123    /// Whether native mount credentials are available.
124    fn mount_enabled(&self) -> bool;
125
126    /// Whether S3 credentials are available.
127    fn s3_enabled(&self) -> bool;
128
129    /// Issues one opaque credential for an already validated exact scope.
130    async fn issue(&self, request: CredentialGrantRequest) -> Result<CredentialGrant, Status>;
131}
132
133/// Deployment-owned source identity after the SDK authenticates its workspace.
134#[derive(Clone, Debug, Eq, PartialEq)]
135pub struct HostedSourceScope {
136    /// Stable workspace identity.
137    pub workspace_id: crate::WorkspaceId,
138    /// Canonical workspace name.
139    pub workspace_name: crate::WorkspaceName,
140}
141
142/// Stable operation identity for a hosted source mutation.
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct HostedSourceOperation {
145    /// Authenticated source workspace.
146    pub scope: HostedSourceScope,
147    /// Caller retry identity retained across ambiguous outcomes.
148    pub idempotency_key: IdempotencyKey,
149}
150
151/// Why a hosted source can no longer prove a contiguous change interval.
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub enum HostedSourceInvalidation {
154    /// No authenticated baseline has been established.
155    InitialSnapshotRequired,
156    /// The bounded change queue overflowed.
157    QueueOverflow,
158    /// The provider explicitly requires a full rescan.
159    NativeRescanRequired,
160    /// The provider reported an asynchronous backend failure.
161    BackendError,
162    /// A source path cannot be represented by filesystem semantics.
163    UnrepresentablePath,
164    /// A rename could not be paired exactly.
165    AmbiguousRename,
166    /// The source root changed identity.
167    RootChanged,
168}
169
170/// Canonical state returned by a deployment source provider.
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172pub enum HostedSourceState {
173    /// Source and workspace agree.
174    Clean,
175    /// A bounded capture is in progress.
176    PendingCapture,
177    /// Continuity was lost.
178    NeedsRescan(HostedSourceInvalidation),
179    /// Source and workspace publications overlap.
180    Conflict,
181    /// The generation is independent of the source.
182    Sealed,
183}
184
185/// One provider result, optionally naming the exact resulting generation.
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub struct HostedSourceResult {
188    /// Resulting semantic state.
189    pub state: HostedSourceState,
190    /// Exact workspace generation for clean or sealed outcomes.
191    pub generation_id: Option<GenerationId>,
192}
193
194/// Deployment extension for a remotely owned attached source.
195#[tonic::async_trait]
196pub trait FilesystemSourceProvider: Send + Sync + 'static {
197    /// Returns the current source state without mutation.
198    async fn state(&self, scope: HostedSourceScope) -> Result<HostedSourceResult, Status>;
199    /// Captures one bounded contiguous source interval.
200    async fn reconcile(
201        &self,
202        operation: HostedSourceOperation,
203    ) -> Result<HostedSourceResult, Status>;
204    /// Rebuilds a complete authenticated source baseline.
205    async fn rescan(&self, operation: HostedSourceOperation) -> Result<HostedSourceResult, Status>;
206    /// Produces a generation independent of the attached source.
207    async fn seal(&self, operation: HostedSourceOperation) -> Result<HostedSourceResult, Status>;
208}
209
210/// One SDK-owned protocol adapter over the canonical filesystem engine.
211///
212/// Deployments wrap this adapter with authentication, quota, credential
213/// issuance, and transport policy. Filesystem semantics stay here.
214#[derive(Clone)]
215pub struct FilesystemWireService<A, O> {
216    filesystem: Fs<A, O>,
217    limits: FilesystemWireLimits,
218    credential_issuer: Option<Arc<dyn FilesystemCredentialIssuer>>,
219    source_provider: Option<Arc<dyn FilesystemSourceProvider>>,
220}
221
222impl<A, O> FilesystemWireService<A, O> {
223    /// Binds one canonical engine and validates finite protocol bounds.
224    ///
225    /// # Errors
226    ///
227    /// Rejects a zero bound.
228    pub fn new(filesystem: Fs<A, O>, limits: FilesystemWireLimits) -> Result<Self, Status> {
229        if limits.maximum_request_bytes == 0
230            || limits.maximum_response_bytes == 0
231            || limits.maximum_transaction_mutations == 0
232            || limits.maximum_page_items == 0
233            || limits.maximum_credential_seconds == 0
234        {
235            return Err(Status::invalid_argument(
236                "filesystem wire bounds must be nonzero",
237            ));
238        }
239        Ok(Self {
240            filesystem,
241            limits,
242            credential_issuer: None,
243            source_provider: None,
244        })
245    }
246
247    /// Borrows the exact engine served by this adapter.
248    #[must_use]
249    pub const fn filesystem(&self) -> &Fs<A, O> {
250        &self.filesystem
251    }
252
253    /// Installs the authenticated deployment's scoped credential issuer.
254    #[must_use]
255    pub fn with_credential_issuer(mut self, issuer: Arc<dyn FilesystemCredentialIssuer>) -> Self {
256        self.credential_issuer = Some(issuer);
257        self
258    }
259
260    /// Installs the deployment's authenticated hosted-source provider.
261    #[must_use]
262    pub fn with_source_provider(mut self, provider: Arc<dyn FilesystemSourceProvider>) -> Self {
263        self.source_provider = Some(provider);
264        self
265    }
266}
267
268impl<A: AsyncAuthorityStore, O: AsyncObjectStore> FilesystemWireService<A, O> {
269    async fn workspace(
270        &self,
271        reference: Option<wire::WorkspaceRef>,
272    ) -> Result<Workspace<A, O>, Status> {
273        let reference = required(reference, "workspace")?;
274        let workspace = self
275            .filesystem
276            .open_workspace(&reference.name)
277            .await
278            .map_err(|error| status(&error))?;
279        if workspace.id().into_bytes().as_slice() != reference.workspace_id {
280            return Err(Status::failed_precondition(
281                "workspace name and identity disagree",
282            ));
283        }
284        Ok(workspace)
285    }
286
287    async fn source_scope(
288        &self,
289        reference: Option<wire::WorkspaceRef>,
290    ) -> Result<(Workspace<A, O>, HostedSourceScope), Status> {
291        let workspace = self.workspace(reference).await?;
292        let scope = HostedSourceScope {
293            workspace_id: workspace.id(),
294            workspace_name: workspace.name().clone(),
295        };
296        Ok((workspace, scope))
297    }
298
299    fn source_provider(&self) -> Result<&Arc<dyn FilesystemSourceProvider>, Status> {
300        self.source_provider
301            .as_ref()
302            .ok_or_else(|| Status::unimplemented("hosted source reconciliation is unavailable"))
303    }
304
305    async fn source_response(
306        &self,
307        workspace: &Workspace<A, O>,
308        result: HostedSourceResult,
309    ) -> Result<wire::SourceResponse, Status> {
310        let generation = match result.generation_id {
311            Some(id) => Some(generation_ref(
312                &workspace
313                    .generation(id)
314                    .await
315                    .map_err(|error| status(&error))?,
316            )),
317            None => None,
318        };
319        let (state, reason) = source_state_message(result.state);
320        let requires_generation = matches!(
321            result.state,
322            HostedSourceState::Clean | HostedSourceState::Sealed
323        );
324        if requires_generation != generation.is_some() {
325            return Err(Status::data_loss(
326                "hosted source result generation does not match its state",
327            ));
328        }
329        Ok(wire::SourceResponse {
330            state: state as i32,
331            reason: reason as i32,
332            generation,
333        })
334    }
335
336    async fn generation(
337        &self,
338        reference: Option<wire::GenerationRef>,
339    ) -> Result<Generation<A, O>, Status> {
340        let reference = required(reference, "generation")?;
341        let workspace = self.workspace(reference.workspace).await?;
342        let id = generation_id(&reference.generation_id)?;
343        workspace
344            .generation(id)
345            .await
346            .map_err(|error| status(&error))
347    }
348
349    async fn join_target_head(
350        &self,
351        target: &Workspace<A, O>,
352        expected: GenerationId,
353        operation: IdempotencyKey,
354    ) -> Result<JoinTargetHead<A, O>, Status> {
355        let committed = self
356            .filesystem
357            .observe_volume_operation(
358                target.id().volume_id(),
359                operation.operation_id(),
360                crate::WorkBudget::UNBOUNDED,
361                &CancellationToken::new(),
362            )
363            .await
364            .map_err(|error| status(&WorkspaceError::engine(error)))?;
365        if let Some(commit) = committed.value {
366            let sequence = commit
367                .sequence
368                .get()
369                .checked_sub(1)
370                .ok_or_else(|| Status::data_loss("operation has invalid authority sequence"))?;
371            return Ok(JoinTargetHead::Expected {
372                head: crate::Head {
373                    epoch: commit.epoch,
374                    sequence: Sequence::new(sequence),
375                    digest: commit.previous_digest,
376                },
377                existing_operation: true,
378            });
379        }
380        let (current, head) = self
381            .filesystem
382            .workspace_head_state(&target.volume)
383            .await
384            .map_err(|error| status(&error))?;
385        if current == expected {
386            Ok(JoinTargetHead::Expected {
387                head,
388                existing_operation: false,
389            })
390        } else {
391            Ok(JoinTargetHead::Stale(
392                target
393                    .generation(current)
394                    .await
395                    .map_err(|error| status(&error))?,
396            ))
397        }
398    }
399
400    fn admit<M: Message>(&self, request: &Request<M>) -> Result<(), Status> {
401        self.admit_message(request.get_ref())
402    }
403
404    fn admit_message<M: Message>(&self, message: &M) -> Result<(), Status> {
405        let bytes = u64::try_from(message.encoded_len()).unwrap_or(u64::MAX);
406        if bytes > self.limits.maximum_request_bytes {
407            return Err(Status::resource_exhausted(
408                "encoded request exceeds service bound",
409            ));
410        }
411        Ok(())
412    }
413}
414
415#[tonic::async_trait]
416impl<A, O> wire::filesystem_service_server::FilesystemService for FilesystemWireService<A, O>
417where
418    A: AsyncAuthorityStore + Send + Sync + 'static,
419    O: AsyncObjectStore + Send + Sync + 'static,
420{
421    async fn handshake(
422        &self,
423        request: Request<wire::HandshakeRequest>,
424    ) -> Result<Response<wire::HandshakeResponse>, Status> {
425        self.admit(&request)?;
426        if let Some(requested) = request
427            .into_inner()
428            .harness
429            .and_then(|value| value.protocol)
430        {
431            if !requested.version.is_empty() && requested.version != "1" {
432                return Err(Status::failed_precondition(
433                    "unsupported filesystem contract version",
434                ));
435            }
436            let digest = crate::descriptor_digest();
437            if !requested.descriptor_digest.is_empty() && requested.descriptor_digest != digest {
438                return Err(Status::failed_precondition(
439                    "filesystem descriptor digest mismatch",
440                ));
441            }
442        }
443        let protocol = harness::ProtocolIdentity {
444            version: "1".to_owned(),
445            descriptor_digest: crate::descriptor_digest(),
446        };
447        Ok(Response::new(wire::HandshakeResponse {
448            harness: Some(harness::HandshakeResponse {
449                protocol: Some(protocol),
450                supported: Some(harness::CapabilitySet {
451                    capabilities: vec![harness::Capability {
452                        name: "filesystem".to_owned(),
453                        version: "1".to_owned(),
454                    }],
455                }),
456            }),
457            capabilities: Some(wire::Capabilities {
458                contract_version: "1".to_owned(),
459                profiles: vec![
460                    wire::FilesystemProfile::Portable as i32,
461                    wire::FilesystemProfile::Posix as i32,
462                    wire::FilesystemProfile::Windows as i32,
463                    wire::FilesystemProfile::Browser as i32,
464                ],
465                maximum_request_bytes: self.limits.maximum_request_bytes,
466                maximum_response_bytes: self.limits.maximum_response_bytes,
467                maximum_transaction_mutations: self.limits.maximum_transaction_mutations,
468                maximum_page_items: self.limits.maximum_page_items,
469                native_mount_credentials: self
470                    .credential_issuer
471                    .as_ref()
472                    .is_some_and(|issuer| issuer.mount_enabled()),
473                s3_credentials: self
474                    .credential_issuer
475                    .as_ref()
476                    .is_some_and(|issuer| issuer.s3_enabled()),
477                source_reconciliation: self.source_provider.is_some(),
478            }),
479        }))
480    }
481
482    async fn create_workspace(
483        &self,
484        request: Request<wire::CreateWorkspaceRequest>,
485    ) -> Result<Response<wire::WorkspaceResponse>, Status> {
486        self.admit(&request)?;
487        let request = request.into_inner();
488        let operation_id = operation(request.operation)?.operation_id();
489        let profile = profile(request.profile)?;
490        let lifecycle = if self.filesystem.capabilities().durable {
491            Lifecycle::Durable
492        } else {
493            Lifecycle::Ephemeral
494        };
495        let mut config = VolumeConfig::portable(lifecycle);
496        config.profile = profile;
497        let workspace = self
498            .filesystem
499            .create_workspace_with_config_operation(request.name, config, Some(operation_id))
500            .await
501            .map_err(|error| status(&error))?;
502        Ok(Response::new(wire::WorkspaceResponse {
503            workspace: Some(workspace_message(&workspace).await?),
504            status: wire::MutationStatus::Committed as i32,
505        }))
506    }
507
508    async fn open_workspace(
509        &self,
510        request: Request<wire::OpenWorkspaceRequest>,
511    ) -> Result<Response<wire::WorkspaceResponse>, Status> {
512        self.admit(&request)?;
513        let workspace = match required(request.into_inner().selector, "workspace selector")? {
514            wire::open_workspace_request::Selector::Workspace(reference) => {
515                self.workspace(Some(reference)).await?
516            }
517            wire::open_workspace_request::Selector::Name(name) => self
518                .filesystem
519                .open_workspace(name)
520                .await
521                .map_err(|error| status(&error))?,
522        };
523        Ok(Response::new(wire::WorkspaceResponse {
524            workspace: Some(workspace_message(&workspace).await?),
525            status: wire::MutationStatus::Committed as i32,
526        }))
527    }
528
529    async fn delete_workspace(
530        &self,
531        request: Request<wire::DeleteWorkspaceRequest>,
532    ) -> Result<Response<wire::MutationResponse>, Status> {
533        self.admit(&request)?;
534        let request = request.into_inner();
535        let workspace = self.workspace(request.workspace).await?;
536        let outcome = workspace
537            .delete(operation(request.operation)?)
538            .await
539            .map_err(|error| status(&error))?;
540        let status = match outcome {
541            WorkspaceDelete::Deleted => wire::MutationStatus::Committed,
542            WorkspaceDelete::AlreadyDeleted => wire::MutationStatus::AlreadyCommitted,
543            WorkspaceDelete::Conflict => wire::MutationStatus::Conflict,
544            WorkspaceDelete::IdempotencyConflict => wire::MutationStatus::IdempotencyConflict,
545        };
546        Ok(Response::new(mutation(status, None, None)))
547    }
548
549    async fn get_head(
550        &self,
551        request: Request<wire::GetHeadRequest>,
552    ) -> Result<Response<wire::GenerationResponse>, Status> {
553        self.admit(&request)?;
554        let workspace = self.workspace(request.into_inner().workspace).await?;
555        let generation = workspace.head().await.map_err(|error| status(&error))?;
556        Ok(Response::new(generation_message(&generation).await?))
557    }
558
559    async fn get_generation(
560        &self,
561        request: Request<wire::GetGenerationRequest>,
562    ) -> Result<Response<wire::GenerationResponse>, Status> {
563        self.admit(&request)?;
564        let generation = self.generation(request.into_inner().generation).await?;
565        Ok(Response::new(generation_message(&generation).await?))
566    }
567
568    async fn read(
569        &self,
570        request: Request<wire::ReadRequest>,
571    ) -> Result<Response<wire::ReadResponse>, Status> {
572        self.admit(&request)?;
573        let request = request.into_inner();
574        if request.maximum_bytes == 0 || request.maximum_bytes > self.limits.maximum_response_bytes
575        {
576            return Err(Status::invalid_argument(
577                "maximum_bytes is outside service bounds",
578            ));
579        }
580        let generation = self.generation(request.generation).await?;
581        let bytes = match request.range {
582            Some(range) => {
583                if range.length > request.maximum_bytes {
584                    return Err(Status::invalid_argument("range exceeds maximum_bytes"));
585                }
586                generation
587                    .read_range(&request.path, range.offset, range.length)
588                    .await
589            }
590            None => generation.read(&request.path, request.maximum_bytes).await,
591        }
592        .map_err(|error| status(&error))?;
593        Ok(Response::new(wire::ReadResponse {
594            contents: bytes.to_vec(),
595        }))
596    }
597
598    async fn stat(
599        &self,
600        request: Request<wire::StatRequest>,
601    ) -> Result<Response<wire::StatResponse>, Status> {
602        self.admit(&request)?;
603        let request = request.into_inner();
604        let value = self
605            .generation(request.generation)
606            .await?
607            .stat(&request.path)
608            .await
609            .map_err(|error| status(&error))?;
610        Ok(Response::new(wire::StatResponse {
611            stat: Some(stat_message(&value)),
612        }))
613    }
614
615    async fn list_directory(
616        &self,
617        request: Request<wire::ListDirectoryRequest>,
618    ) -> Result<Response<wire::ListDirectoryResponse>, Status> {
619        self.admit(&request)?;
620        let request = request.into_inner();
621        let page = required(request.page, "page")?;
622        if page.maximum_items == 0 || page.maximum_items > self.limits.maximum_page_items {
623            return Err(Status::invalid_argument("directory page bound is invalid"));
624        }
625        let generation = self.generation(request.generation).await?;
626        let after = page.after.map(input_logical_name).transpose()?;
627        let listed = generation
628            .list_directory(&request.path, after.as_ref(), page.maximum_items)
629            .await
630            .map_err(|error| status(&error))?;
631        let next = listed
632            .has_more
633            .then(|| listed.entries.last().map(|entry| logical_name(&entry.name)))
634            .flatten();
635        let mut entries = Vec::new();
636        entries
637            .try_reserve_exact(listed.entries.len())
638            .map_err(|_| Status::resource_exhausted("directory page allocation failed"))?;
639        for entry in listed.entries {
640            let name = std::str::from_utf8(entry.name.as_bytes())
641                .map_err(|_| Status::data_loss("portable directory name is not UTF-8"))?;
642            let child = if request.path == "/" {
643                format!("/{name}")
644            } else {
645                format!("{}/{name}", request.path.trim_end_matches('/'))
646            };
647            let stat = generation
648                .stat(&child)
649                .await
650                .map_err(|error| status(&error))?;
651            if stat.file_id != entry.file_id || stat.kind != entry.kind {
652                return Err(Status::data_loss(
653                    "directory binding changed within immutable generation",
654                ));
655            }
656            entries.push(wire::DirectoryEntry {
657                name: Some(logical_name(&entry.name)),
658                stat: Some(stat_message(&stat)),
659            });
660        }
661        Ok(Response::new(wire::ListDirectoryResponse {
662            page: Some(wire::DirectoryPage { entries, next }),
663        }))
664    }
665
666    async fn read_link(
667        &self,
668        request: Request<wire::ReadLinkRequest>,
669    ) -> Result<Response<wire::ReadResponse>, Status> {
670        self.admit(&request)?;
671        let request = request.into_inner();
672        let contents = self
673            .generation(request.generation)
674            .await?
675            .read_symbolic_link(&request.path)
676            .await
677            .map_err(|error| status(&error))?;
678        if u64::try_from(contents.len()).unwrap_or(u64::MAX) > request.maximum_bytes
679            || request.maximum_bytes > self.limits.maximum_response_bytes
680        {
681            return Err(Status::resource_exhausted(
682                "symbolic link exceeds response bound",
683            ));
684        }
685        Ok(Response::new(wire::ReadResponse {
686            contents: contents.to_vec(),
687        }))
688    }
689
690    async fn plan_extents(
691        &self,
692        request: Request<wire::PlanExtentsRequest>,
693    ) -> Result<Response<wire::PlanExtentsResponse>, Status> {
694        self.admit(&request)?;
695        let request = request.into_inner();
696        if request.maximum_extents == 0 || request.maximum_extents > self.limits.maximum_page_items
697        {
698            return Err(Status::invalid_argument("extent bound is invalid"));
699        }
700        let range = required(request.range, "range")?;
701        let plan = self
702            .generation(request.generation)
703            .await?
704            .plan_extents(
705                &request.path,
706                range.offset,
707                range.length,
708                request.maximum_extents,
709            )
710            .await
711            .map_err(|error| status(&error))?;
712        let truncated =
713            u32::try_from(plan.spans.len()).unwrap_or(u32::MAX) == request.maximum_extents;
714        Ok(Response::new(wire::PlanExtentsResponse {
715            extents: plan
716                .spans
717                .into_iter()
718                .map(|span| wire::Extent {
719                    range: Some(wire::ByteRange {
720                        offset: span.offset,
721                        length: span.length,
722                    }),
723                    kind: match span.kind {
724                        WorkspaceExtentKind::Hole => wire::ExtentKind::Hole as i32,
725                        WorkspaceExtentKind::AllocatedZero => {
726                            wire::ExtentKind::AllocatedZero as i32
727                        }
728                        WorkspaceExtentKind::Content => wire::ExtentKind::Content as i32,
729                    },
730                })
731                .collect(),
732            truncated,
733        }))
734    }
735
736    async fn apply_transaction(
737        &self,
738        request: Request<wire::ApplyTransactionRequest>,
739    ) -> Result<Response<wire::MutationResponse>, Status> {
740        self.admit(&request)?;
741        let request = request.into_inner();
742        if request.mutations.is_empty()
743            || u32::try_from(request.mutations.len()).unwrap_or(u32::MAX)
744                > self.limits.maximum_transaction_mutations
745        {
746            return Err(Status::invalid_argument(
747                "transaction mutation count is invalid",
748            ));
749        }
750        if request.maximum_conflicts == 0
751            || request.maximum_conflicts > self.limits.maximum_page_items
752        {
753            return Err(Status::invalid_argument(
754                "transaction conflict bound is invalid",
755            ));
756        }
757        let base = self.generation(request.base).await?;
758        let workspace = self.workspace(generation_ref(&base).workspace).await?;
759        let mut transaction = workspace
760            .begin_transaction_at(&base, operation(request.operation)?)
761            .await
762            .map_err(|error| status(&error))?;
763        for mutation in request.mutations {
764            apply_mutation(&mut transaction, mutation).await?;
765        }
766        let first = transaction.commit().await.map_err(|error| status(&error))?;
767        if !matches!(first, TransactionCommit::Conflict { .. }) {
768            return Ok(Response::new(commit_message(first)));
769        }
770        match transaction
771            .rebase(request.maximum_conflicts)
772            .await
773            .map_err(|error| status(&error))?
774        {
775            TransactionRebase::Rebased(_) => Ok(Response::new(commit_message(
776                transaction.commit().await.map_err(|error| status(&error))?,
777            ))),
778            TransactionRebase::Conflicted {
779                conflicts,
780                truncated,
781            } => Ok(Response::new(mutation_with_conflicts(
782                wire::MutationStatus::Conflict,
783                conflicts.into_iter().map(transaction_conflict).collect(),
784                truncated,
785            ))),
786        }
787    }
788
789    async fn rebase_transaction(
790        &self,
791        request: Request<wire::RebaseTransactionRequest>,
792    ) -> Result<Response<wire::RebaseTransactionResponse>, Status> {
793        self.admit(&request)?;
794        let request = request.into_inner();
795        if request.mutations.is_empty()
796            || u32::try_from(request.mutations.len()).unwrap_or(u32::MAX)
797                > self.limits.maximum_transaction_mutations
798            || request.maximum_conflicts == 0
799            || request.maximum_conflicts > self.limits.maximum_page_items
800        {
801            return Err(Status::invalid_argument(
802                "transaction rebase bounds are invalid",
803            ));
804        }
805        let base = self.generation(request.base).await?;
806        let workspace = self.workspace(generation_ref(&base).workspace).await?;
807        let mut transaction = workspace
808            .begin_transaction_at(&base, operation(request.operation)?)
809            .await
810            .map_err(|error| status(&error))?;
811        for mutation in request.mutations {
812            apply_mutation(&mut transaction, mutation).await?;
813        }
814        match transaction
815            .rebase(request.maximum_conflicts)
816            .await
817            .map_err(|error| status(&error))?
818        {
819            TransactionRebase::Rebased(generation) => {
820                Ok(Response::new(wire::RebaseTransactionResponse {
821                    base: Some(generation_ref(&generation)),
822                    conflicts: Vec::new(),
823                    truncated: false,
824                }))
825            }
826            TransactionRebase::Conflicted {
827                conflicts,
828                truncated,
829            } => Ok(Response::new(wire::RebaseTransactionResponse {
830                base: Some(generation_ref(&base)),
831                conflicts: conflicts.into_iter().map(transaction_conflict).collect(),
832                truncated,
833            })),
834        }
835    }
836
837    async fn fork_workspace(
838        &self,
839        request: Request<wire::ForkWorkspaceRequest>,
840    ) -> Result<Response<wire::WorkspaceResponse>, Status> {
841        self.admit(&request)?;
842        let request = request.into_inner();
843        let idempotency_key = operation(request.operation)?;
844        let source = self.generation(request.source).await?;
845        let source_workspace = source.workspace.clone();
846        let destination = source_workspace
847            .fork(
848                request.destination_name,
849                ForkOptions::from_generation(source, idempotency_key),
850            )
851            .await
852            .map_err(|error| status(&error))?;
853        Ok(Response::new(wire::WorkspaceResponse {
854            workspace: Some(workspace_message(&destination).await?),
855            status: wire::MutationStatus::Committed as i32,
856        }))
857    }
858
859    async fn diff(
860        &self,
861        request: Request<wire::DiffRequest>,
862    ) -> Result<Response<wire::DiffResponse>, Status> {
863        self.admit(&request)?;
864        let request = request.into_inner();
865        if request.maximum_changes == 0 || request.maximum_changes > self.limits.maximum_page_items
866        {
867            return Err(Status::invalid_argument("diff bound is invalid"));
868        }
869        let from = self.generation(request.from).await?;
870        let to = self.generation(request.to).await?;
871        let workspace = from.workspace.clone();
872        let changes = workspace
873            .diff(&from, &to, request.maximum_changes)
874            .await
875            .map_err(|error| status(&error))?;
876        Ok(Response::new(wire::DiffResponse {
877            from: Some(generation_ref(changes.from())),
878            to: Some(generation_ref(changes.to())),
879            files: changes
880                .changes()
881                .files
882                .iter()
883                .map(file_record_change)
884                .collect(),
885            bindings: changes
886                .changes()
887                .bindings
888                .iter()
889                .map(directory_binding_change)
890                .collect(),
891            truncated: changes.changes().truncated,
892            work: Some(work_counters(changes.work())),
893        }))
894    }
895
896    async fn rebase(
897        &self,
898        request: Request<wire::RebaseRequest>,
899    ) -> Result<Response<wire::RebaseResponse>, Status> {
900        self.admit(&request)?;
901        let request = request.into_inner();
902        if request.maximum_generations == 0
903            || request.maximum_generations > self.limits.maximum_page_items
904            || request.maximum_changes == 0
905            || request.maximum_changes > self.limits.maximum_page_items
906            || request.maximum_conflicts == 0
907            || request.maximum_conflicts > self.limits.maximum_page_items
908        {
909            return Err(Status::invalid_argument("rebase bounds are invalid"));
910        }
911        let workspace = self.workspace(request.workspace).await?;
912        let outcome = workspace
913            .live_rebase(
914                operation(request.operation)?,
915                request.maximum_generations,
916                request.maximum_changes,
917                request.maximum_conflicts,
918            )
919            .await
920            .map_err(|error| status(&error))?;
921        Ok(Response::new(rebase_message(outcome)))
922    }
923
924    async fn plan_join(
925        &self,
926        request: Request<wire::PlanJoinRequest>,
927    ) -> Result<Response<wire::JoinPlan>, Status> {
928        self.admit(&request)?;
929        let request = request.into_inner();
930        if request.maximum_generations == 0
931            || request.maximum_generations > self.limits.maximum_page_items
932            || request.maximum_changes == 0
933            || request.maximum_changes > self.limits.maximum_page_items
934            || request.maximum_conflicts == 0
935            || request.maximum_conflicts > self.limits.maximum_page_items
936        {
937            return Err(Status::invalid_argument("join bounds are invalid"));
938        }
939        let history = join_history(request.history)?;
940        let source = self.generation(request.source).await?;
941        let target = self.generation(request.target).await?;
942        let source_workspace = source.workspace.clone();
943        let target_workspace = target.workspace.clone();
944        if source_workspace
945            .head()
946            .await
947            .map_err(|error| status(&error))?
948            .id()
949            != source.id()
950            || target_workspace
951                .head()
952                .await
953                .map_err(|error| status(&error))?
954                .id()
955                != target.id()
956        {
957            return Err(Status::failed_precondition(
958                "join endpoints must be current workspace heads",
959            ));
960        }
961        let plan = source_workspace
962            .join_into(&target_workspace)
963            .history(history)
964            .bounds(
965                request.maximum_generations,
966                request.maximum_changes,
967                request.maximum_conflicts,
968            )
969            .plan()
970            .await
971            .map_err(|error| status(&error))?;
972        if plan.source_head() != source.id() || plan.target_head() != target.id() {
973            return Err(Status::failed_precondition(
974                "join endpoints changed while the plan was computed",
975            ));
976        }
977        // The builder re-reads both live heads. Bind the wire plan to that exact
978        // snapshot rather than the endpoint generations observed before planning.
979        let source_ref = generation_ref_for(&source_workspace, plan.source_head());
980        let target_ref = generation_ref_for(&target_workspace, plan.target_head());
981        let base_ref = join_common_ancestor_ref(&plan, &source_workspace, &target_workspace);
982        let plan_id = join_plan_id(
983            &source_ref,
984            &target_ref,
985            &base_ref,
986            history,
987            request.maximum_generations,
988            request.maximum_changes,
989            request.maximum_conflicts,
990        );
991        let changes = plan
992            .source_changes()
993            .await
994            .map_err(|error| status(&error))?;
995        let file_changes = changes.value.files.iter().map(file_record_change).collect();
996        let binding_changes = changes
997            .value
998            .bindings
999            .iter()
1000            .map(directory_binding_change)
1001            .collect();
1002        Ok(Response::new(wire::JoinPlan {
1003            plan_id: plan_id.to_vec(),
1004            source: Some(source_ref),
1005            expected_target: Some(target_ref),
1006            file_changes,
1007            conflicts: Vec::new(),
1008            truncated: changes.value.truncated,
1009            common_ancestor: Some(base_ref),
1010            maximum_generations: request.maximum_generations,
1011            maximum_changes: request.maximum_changes,
1012            maximum_conflicts: request.maximum_conflicts,
1013            history: request.history,
1014            binding_changes,
1015        }))
1016    }
1017
1018    async fn apply_join(
1019        &self,
1020        request: Request<wire::ApplyJoinRequest>,
1021    ) -> Result<Response<wire::JoinResponse>, Status> {
1022        self.admit(&request)?;
1023        let request = request.into_inner();
1024        let supplied = required(request.plan, "join plan")?;
1025        let source_ref = required(supplied.source.clone(), "join source")?;
1026        let target_ref = required(supplied.expected_target.clone(), "join target")?;
1027        let base_ref = required(supplied.common_ancestor.clone(), "join common ancestor")?;
1028        let history = join_history(supplied.history)?;
1029        if supplied.maximum_generations == 0
1030            || supplied.maximum_generations > self.limits.maximum_page_items
1031            || supplied.maximum_changes == 0
1032            || supplied.maximum_changes > self.limits.maximum_page_items
1033            || supplied.maximum_conflicts == 0
1034            || supplied.maximum_conflicts > self.limits.maximum_page_items
1035        {
1036            return Err(Status::invalid_argument("join bounds are invalid"));
1037        }
1038        if supplied.plan_id
1039            != join_plan_id(
1040                &source_ref,
1041                &target_ref,
1042                &base_ref,
1043                history,
1044                supplied.maximum_generations,
1045                supplied.maximum_changes,
1046                supplied.maximum_conflicts,
1047            )
1048        {
1049            return Err(Status::invalid_argument("join plan identity mismatch"));
1050        }
1051        let source = self.generation(Some(source_ref)).await?;
1052        let target = self.generation(Some(target_ref)).await?;
1053        let source_workspace = source.workspace.clone();
1054        let target_workspace = target.workspace.clone();
1055        let source_id = source.id();
1056        let target_id = target.id();
1057        let operation = operation(request.operation)?;
1058        let (target_authority_head, existing_operation) = match self
1059            .join_target_head(&target_workspace, target_id, operation)
1060            .await?
1061        {
1062            JoinTargetHead::Expected {
1063                head,
1064                existing_operation,
1065            } => (head, existing_operation),
1066            JoinTargetHead::Stale(actual) => {
1067                return Ok(Response::new(join_response(
1068                    wire::JoinStatus::StaleTarget,
1069                    Some(&actual),
1070                )));
1071            }
1072        };
1073        let plan = source_workspace
1074            .join_into(&target_workspace)
1075            .history(history)
1076            .bounds(
1077                supplied.maximum_generations,
1078                supplied.maximum_changes,
1079                supplied.maximum_conflicts,
1080            )
1081            .plan_generations(source, target, target_authority_head)
1082            .await
1083            .map_err(|error| status(&error))?;
1084        let expected_base_ref =
1085            join_common_ancestor_ref(&plan, &source_workspace, &target_workspace);
1086        if plan.source_head() != source_id
1087            || plan.target_head() != target_id
1088            || base_ref != expected_base_ref
1089        {
1090            let actual = target_workspace
1091                .head()
1092                .await
1093                .map_err(|error| status(&error))?;
1094            return Ok(Response::new(wire::JoinResponse {
1095                status: wire::JoinStatus::StaleTarget as i32,
1096                generation: Some(generation_ref(&actual)),
1097                conflicts: Vec::new(),
1098                truncated: false,
1099            }));
1100        }
1101        let outcome = plan
1102            .apply(ApplyOptions {
1103                if_target: target_id,
1104                idempotency_key: operation,
1105            })
1106            .await
1107            .map_err(|error| status(&error))?;
1108        let outcome = if existing_operation
1109            && !matches!(
1110                &outcome,
1111                JoinOutcome::AlreadyApplied(_) | JoinOutcome::IdempotencyConflict
1112            ) {
1113            JoinOutcome::IdempotencyConflict
1114        } else {
1115            outcome
1116        };
1117        Ok(Response::new(join_message(outcome)))
1118    }
1119
1120    async fn checkpoint(
1121        &self,
1122        request: Request<wire::RetainGenerationRequest>,
1123    ) -> Result<Response<wire::RetainGenerationResponse>, Status> {
1124        self.retain(request, true).await
1125    }
1126
1127    async fn pin(
1128        &self,
1129        request: Request<wire::RetainGenerationRequest>,
1130    ) -> Result<Response<wire::RetainGenerationResponse>, Status> {
1131        self.retain(request, false).await
1132    }
1133
1134    type ExportStream = WireStream<wire::ExportChunk>;
1135
1136    async fn export(
1137        &self,
1138        request: Request<wire::ExportRequest>,
1139    ) -> Result<Response<Self::ExportStream>, Status> {
1140        self.admit(&request)?;
1141        let request = request.into_inner();
1142        if request.maximum_objects == 0
1143            || request.maximum_objects > self.limits.maximum_page_items
1144            || request.maximum_bytes == 0
1145            || request.maximum_bytes > self.limits.maximum_response_bytes
1146        {
1147            return Err(Status::invalid_argument("export bounds are invalid"));
1148        }
1149        let generation = self.generation(request.generation).await?;
1150        let checkout = generation
1151            .workspace
1152            .engine_checkout(
1153                crate::model::GenerationSelector::Exact(generation.id()),
1154                crate::model::CheckoutMode::read_only_pinned(),
1155            )
1156            .await
1157            .map_err(|error| status(&error))?;
1158        let manifest = checkout
1159            .export_manifest(crate::WorkBudget::UNBOUNDED, &CancellationToken::new())
1160            .await
1161            .map_err(|failure| Status::unavailable(failure.error.to_string()))?
1162            .value;
1163        let encoded = crate::encode_generation_export_manifest(&manifest)
1164            .map_err(|error| Status::data_loss(error.to_string()))?;
1165        let retained = u64::try_from(encoded.len()).unwrap_or(u64::MAX);
1166        if retained > request.maximum_bytes {
1167            return Err(Status::resource_exhausted(
1168                "export manifest exceeds byte bound",
1169            ));
1170        }
1171        let start = transfer_cursor(&request.after)?;
1172        let start_index = usize::try_from(start)
1173            .map_err(|_| Status::invalid_argument("export cursor is out of range"))?;
1174        if start_index > manifest.objects.len() {
1175            return Err(Status::invalid_argument("export cursor is out of range"));
1176        }
1177        let manifest_chunk = (start == 0).then(|| wire::ExportChunk {
1178            cursor: 0_u64.to_le_bytes().to_vec(),
1179            object_id: Vec::new(),
1180            contents: encoded,
1181            terminal: manifest.objects.is_empty(),
1182        });
1183        let filesystem = self.filesystem.clone();
1184        let objects = manifest.objects;
1185        let maximum_objects = request.maximum_objects;
1186        let maximum_bytes = request.maximum_bytes;
1187        let object_stream = stream::try_unfold(
1188            (filesystem, objects, start_index, start, retained, 0_u32),
1189            move |(filesystem, objects, index, cursor, retained, emitted)| async move {
1190                if index == objects.len() || emitted == maximum_objects || retained == maximum_bytes
1191                {
1192                    return Ok(None);
1193                }
1194                let Some(&object) = objects.get(index) else {
1195                    return Ok(None);
1196                };
1197                let remaining = maximum_bytes.saturating_sub(retained);
1198                let body = filesystem
1199                    .export_object(
1200                        object,
1201                        remaining,
1202                        crate::WorkBudget::UNBOUNDED,
1203                        &CancellationToken::new(),
1204                    )
1205                    .await
1206                    .map_err(|failure| Status::resource_exhausted(failure.error.to_string()))?
1207                    .value
1208                    .bytes;
1209                let retained = retained
1210                    .checked_add(u64::try_from(body.len()).unwrap_or(u64::MAX))
1211                    .ok_or_else(|| Status::resource_exhausted("export byte count overflow"))?;
1212                let next_index = index.saturating_add(1);
1213                let next_cursor = cursor.saturating_add(1);
1214                let chunk = wire::ExportChunk {
1215                    cursor: next_cursor.to_le_bytes().to_vec(),
1216                    object_id: encode_object_id(object),
1217                    contents: body.to_vec(),
1218                    terminal: next_index == objects.len(),
1219                };
1220                Ok(Some((
1221                    chunk,
1222                    (
1223                        filesystem,
1224                        objects,
1225                        next_index,
1226                        next_cursor,
1227                        retained,
1228                        emitted.saturating_add(1),
1229                    ),
1230                )))
1231            },
1232        );
1233        let output = stream::iter(manifest_chunk.map(Ok)).chain(object_stream);
1234        Ok(Response::new(Box::pin(output)))
1235    }
1236
1237    async fn import(
1238        &self,
1239        request: Request<tonic::Streaming<wire::ImportChunk>>,
1240    ) -> Result<Response<wire::ImportResponse>, Status> {
1241        let mut stream = request.into_inner();
1242        let first = stream
1243            .message()
1244            .await?
1245            .ok_or_else(|| Status::invalid_argument("import stream is empty"))?;
1246        self.admit_message(&first)?;
1247        if !first.object_id.is_empty() || first.contents.is_empty() {
1248            return Err(Status::invalid_argument(
1249                "import must begin with a manifest chunk",
1250            ));
1251        }
1252        let workspace_ref = required(first.workspace, "workspace")?;
1253        let operation_bytes = first.operation_id.clone();
1254        let operation_id = operation(Some(wire::OperationOptions {
1255            idempotency_key: operation_bytes.clone(),
1256        }))?
1257        .operation_id();
1258        let manifest = crate::decode_generation_export_manifest(
1259            &first.contents,
1260            self.limits.maximum_response_bytes,
1261            u64::from(self.limits.maximum_page_items),
1262        )
1263        .map_err(|error| Status::invalid_argument(error.to_string()))?;
1264        let expected_workspace = self
1265            .filesystem
1266            .workspace_id(&workspace_ref.name)
1267            .map_err(|error| Status::invalid_argument(error.to_string()))?;
1268        if expected_workspace.into_bytes().as_slice() != workspace_ref.workspace_id
1269            || expected_workspace.volume_id() != manifest.volume_id
1270        {
1271            return Err(Status::failed_precondition(
1272                "import workspace identity mismatch",
1273            ));
1274        }
1275        let mut expected_index = 0_usize;
1276        let mut terminal = first.terminal;
1277        while let Some(chunk) = stream.message().await? {
1278            self.admit_message(&chunk)?;
1279            if expected_index >= self.limits.maximum_page_items as usize {
1280                return Err(Status::resource_exhausted(
1281                    "import object count exceeds service bound",
1282                ));
1283            }
1284            if chunk.workspace.as_ref() != Some(&workspace_ref)
1285                || chunk.operation_id != operation_bytes
1286                || terminal
1287            {
1288                return Err(Status::invalid_argument(
1289                    "import stream continuity mismatch",
1290                ));
1291            }
1292            let object = decode_object_id(&chunk.object_id)?;
1293            if manifest.objects.get(expected_index).copied() != Some(object) {
1294                return Err(Status::invalid_argument("import object order mismatch"));
1295            }
1296            self.filesystem
1297                .import_object(
1298                    object,
1299                    Bytes::from(chunk.contents),
1300                    crate::WorkBudget::UNBOUNDED,
1301                    &CancellationToken::new(),
1302                )
1303                .await
1304                .map_err(|failure| Status::unavailable(failure.error.to_string()))?;
1305            expected_index = expected_index.saturating_add(1);
1306            if transfer_cursor(&chunk.cursor)? != expected_index as u64 {
1307                return Err(Status::invalid_argument("import cursor mismatch"));
1308            }
1309            terminal = chunk.terminal;
1310        }
1311        if !terminal || expected_index != manifest.objects.len() {
1312            return Err(Status::invalid_argument(
1313                "import stream ended before its closure",
1314            ));
1315        }
1316        self.filesystem
1317            .restore_volume(
1318                &manifest,
1319                operation_id,
1320                crate::WorkBudget::UNBOUNDED,
1321                &CancellationToken::new(),
1322            )
1323            .await
1324            .map_err(|failure| Status::unavailable(failure.error.to_string()))?;
1325        let workspace = self
1326            .filesystem
1327            .open_workspace(&workspace_ref.name)
1328            .await
1329            .map_err(|error| status(&error))?;
1330        Ok(Response::new(wire::ImportResponse {
1331            outcome: Some(mutation(
1332                wire::MutationStatus::Committed,
1333                Some(generation_ref(
1334                    &workspace.head().await.map_err(|error| status(&error))?,
1335                )),
1336                None,
1337            )),
1338        }))
1339    }
1340
1341    async fn issue_mount_credential(
1342        &self,
1343        request: Request<wire::CredentialRequest>,
1344    ) -> Result<Response<wire::CredentialResponse>, Status> {
1345        self.issue_credential(request, CredentialKind::Mount).await
1346    }
1347
1348    async fn issue_s3_credential(
1349        &self,
1350        request: Request<wire::CredentialRequest>,
1351    ) -> Result<Response<wire::CredentialResponse>, Status> {
1352        self.issue_credential(request, CredentialKind::S3).await
1353    }
1354
1355    async fn get_source_state(
1356        &self,
1357        request: Request<wire::SourceStateRequest>,
1358    ) -> Result<Response<wire::SourceResponse>, Status> {
1359        self.admit(&request)?;
1360        let (workspace, scope) = self.source_scope(request.into_inner().workspace).await?;
1361        let result = self.source_provider()?.state(scope).await?;
1362        Ok(Response::new(
1363            self.source_response(&workspace, result).await?,
1364        ))
1365    }
1366
1367    async fn reconcile_source(
1368        &self,
1369        request: Request<wire::SourceOperationRequest>,
1370    ) -> Result<Response<wire::SourceResponse>, Status> {
1371        self.admit(&request)?;
1372        let request = request.into_inner();
1373        let idempotency_key = operation(request.operation)?;
1374        let (workspace, scope) = self.source_scope(request.workspace).await?;
1375        let result = self
1376            .source_provider()?
1377            .reconcile(HostedSourceOperation {
1378                scope,
1379                idempotency_key,
1380            })
1381            .await?;
1382        Ok(Response::new(
1383            self.source_response(&workspace, result).await?,
1384        ))
1385    }
1386
1387    async fn rescan_source(
1388        &self,
1389        request: Request<wire::SourceOperationRequest>,
1390    ) -> Result<Response<wire::SourceResponse>, Status> {
1391        self.admit(&request)?;
1392        let request = request.into_inner();
1393        let idempotency_key = operation(request.operation)?;
1394        let (workspace, scope) = self.source_scope(request.workspace).await?;
1395        let result = self
1396            .source_provider()?
1397            .rescan(HostedSourceOperation {
1398                scope,
1399                idempotency_key,
1400            })
1401            .await?;
1402        Ok(Response::new(
1403            self.source_response(&workspace, result).await?,
1404        ))
1405    }
1406
1407    async fn seal_source(
1408        &self,
1409        request: Request<wire::SourceOperationRequest>,
1410    ) -> Result<Response<wire::SourceResponse>, Status> {
1411        self.admit(&request)?;
1412        let request = request.into_inner();
1413        let idempotency_key = operation(request.operation)?;
1414        let (workspace, scope) = self.source_scope(request.workspace).await?;
1415        let result = self
1416            .source_provider()?
1417            .seal(HostedSourceOperation {
1418                scope,
1419                idempotency_key,
1420            })
1421            .await?;
1422        Ok(Response::new(
1423            self.source_response(&workspace, result).await?,
1424        ))
1425    }
1426
1427    async fn observe(
1428        &self,
1429        request: Request<wire::ObserveRequest>,
1430    ) -> Result<Response<wire::ObserveResponse>, Status> {
1431        self.admit(&request)?;
1432        let request = request.into_inner();
1433        let workspace = self.workspace(request.workspace).await?;
1434        let operation_id = raw_operation(&request.operation_id)?;
1435        let observed = self
1436            .filesystem
1437            .observe_volume_operation(
1438                workspace.id().volume_id(),
1439                operation_id,
1440                crate::WorkBudget::UNBOUNDED,
1441                &CancellationToken::new(),
1442            )
1443            .await
1444            .map_err(|failure| Status::unavailable(failure.error.to_string()))?;
1445        let response = match observed.value {
1446            Some(commit) => wire::ObserveResponse {
1447                state: "completed".to_owned(),
1448                outcome: Some(observed_mutation(&workspace, &commit)?),
1449            },
1450            None => wire::ObserveResponse {
1451                state: "unknown".to_owned(),
1452                outcome: None,
1453            },
1454        };
1455        Ok(Response::new(response))
1456    }
1457
1458    async fn cancel(
1459        &self,
1460        request: Request<wire::CancelRequest>,
1461    ) -> Result<Response<wire::CancelResponse>, Status> {
1462        self.admit(&request)?;
1463        let request = request.into_inner();
1464        let observed = self
1465            .observe(Request::new(wire::ObserveRequest {
1466                workspace: request.workspace,
1467                operation_id: request.operation_id,
1468            }))
1469            .await?
1470            .into_inner();
1471        Ok(Response::new(wire::CancelResponse {
1472            operation: Some(observed),
1473        }))
1474    }
1475}
1476
1477impl<A: AsyncAuthorityStore, O: AsyncObjectStore> FilesystemWireService<A, O> {
1478    async fn issue_credential(
1479        &self,
1480        request: Request<wire::CredentialRequest>,
1481        kind: CredentialKind,
1482    ) -> Result<Response<wire::CredentialResponse>, Status> {
1483        self.admit(&request)?;
1484        let request = request.into_inner();
1485        if request.expires_after_seconds == 0
1486            || request.expires_after_seconds > self.limits.maximum_credential_seconds
1487        {
1488            return Err(Status::invalid_argument(
1489                "credential lifetime exceeds the configured bound",
1490            ));
1491        }
1492        let workspace = self.workspace(request.workspace).await?;
1493        let generation = match request.generation {
1494            Some(reference) => {
1495                let generation = self.generation(Some(reference)).await?;
1496                if generation.workspace.id() != workspace.id() {
1497                    return Err(Status::failed_precondition(
1498                        "credential generation belongs to another workspace",
1499                    ));
1500                }
1501                generation
1502            }
1503            None => workspace.head().await.map_err(|error| status(&error))?,
1504        };
1505        let issuer = self.credential_issuer.as_ref().ok_or_else(|| {
1506            Status::unimplemented("credential issuance is unavailable in this deployment")
1507        })?;
1508        let enabled = match kind {
1509            CredentialKind::Mount => issuer.mount_enabled(),
1510            CredentialKind::S3 => issuer.s3_enabled(),
1511        };
1512        if !enabled {
1513            return Err(Status::unimplemented(
1514                "requested credential surface is unavailable",
1515            ));
1516        }
1517        let grant = issuer
1518            .issue(CredentialGrantRequest {
1519                kind,
1520                workspace_id: workspace.id(),
1521                workspace_name: workspace.name().clone(),
1522                generation_id: generation.id(),
1523                writable: request.writable,
1524                expires_after_seconds: request.expires_after_seconds,
1525                idempotency_key: operation(request.operation)?,
1526            })
1527            .await?;
1528        let response = match grant {
1529            CredentialGrant::Bearer {
1530                endpoint,
1531                token,
1532                expires_at_unix_seconds,
1533            } if kind == CredentialKind::Mount
1534                && !endpoint.is_empty()
1535                && !token.is_empty()
1536                && expires_at_unix_seconds > 0 =>
1537            {
1538                wire::CredentialResponse {
1539                    endpoint,
1540                    expires_at_unix_seconds,
1541                    credential: Some(wire::credential_response::Credential::BearerToken(token)),
1542                }
1543            }
1544            CredentialGrant::S3 {
1545                endpoint,
1546                bucket,
1547                region,
1548                access_key_id,
1549                secret_access_key,
1550                session_token,
1551                expires_at_unix_seconds,
1552            } if kind == CredentialKind::S3
1553                && !endpoint.is_empty()
1554                && !bucket.is_empty()
1555                && !region.is_empty()
1556                && !access_key_id.is_empty()
1557                && !secret_access_key.is_empty()
1558                && expires_at_unix_seconds > 0 =>
1559            {
1560                wire::CredentialResponse {
1561                    endpoint,
1562                    expires_at_unix_seconds,
1563                    credential: Some(wire::credential_response::Credential::S3(
1564                        wire::S3Credential {
1565                            bucket,
1566                            region,
1567                            access_key_id,
1568                            secret_access_key,
1569                            session_token,
1570                        },
1571                    )),
1572                }
1573            }
1574            _ => {
1575                return Err(Status::internal(
1576                    "credential issuer returned an invalid grant",
1577                ));
1578            }
1579        };
1580        Ok(Response::new(response))
1581    }
1582
1583    async fn retain(
1584        &self,
1585        request: Request<wire::RetainGenerationRequest>,
1586        checkpoint: bool,
1587    ) -> Result<Response<wire::RetainGenerationResponse>, Status> {
1588        self.admit(&request)?;
1589        let request = request.into_inner();
1590        let _ = operation(request.operation)?;
1591        let generation = self.generation(request.generation).await?;
1592        if checkpoint {
1593            let workspace = self
1594                .workspace(generation_ref(&generation).workspace)
1595                .await?;
1596            let head = workspace.head().await.map_err(|error| status(&error))?;
1597            if head.id() != generation.id() {
1598                return Err(Status::failed_precondition(
1599                    "checkpoint requires the current head",
1600                ));
1601            }
1602            workspace
1603                .checkpoint(&request.identity)
1604                .await
1605                .map_err(|error| status(&error))?;
1606        } else {
1607            generation
1608                .pin(&request.identity)
1609                .await
1610                .map_err(|error| status(&error))?;
1611        }
1612        Ok(Response::new(wire::RetainGenerationResponse {
1613            generation: Some(generation_ref(&generation)),
1614            identity: request.identity,
1615            status: wire::MutationStatus::Committed as i32,
1616        }))
1617    }
1618}
1619
1620async fn apply_mutation<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1621    transaction: &mut Transaction<A, O>,
1622    value: wire::Mutation,
1623) -> Result<(), Status> {
1624    use wire::mutation::Mutation;
1625    match required(value.mutation, "mutation")? {
1626        Mutation::CreateFile(value) => {
1627            transaction
1628                .create_file(
1629                    &value.path,
1630                    Bytes::from(value.contents),
1631                    metadata(value.metadata)?,
1632                )
1633                .await
1634        }
1635        Mutation::CreateDirectory(value) => {
1636            transaction
1637                .create_directory(&value.path)
1638                .await
1639                .map_err(|error| status(&error))?;
1640            transaction
1641                .set_metadata(&value.path, metadata(value.metadata)?)
1642                .await
1643        }
1644        Mutation::CreateSymbolicLink(value) => {
1645            transaction
1646                .create_symbolic_link(&value.path, Bytes::from(value.target))
1647                .await
1648                .map_err(|error| status(&error))?;
1649            transaction
1650                .set_metadata(&value.path, metadata(value.metadata)?)
1651                .await
1652        }
1653        Mutation::Remove(value) => transaction.remove(&value.path).await,
1654        Mutation::Rename(value) => {
1655            transaction
1656                .rename_with_replace(&value.source, &value.destination, value.replace)
1657                .await
1658        }
1659        Mutation::HardLink(value) => {
1660            transaction
1661                .hard_link(&value.source, &value.destination)
1662                .await
1663        }
1664        Mutation::Write(value) => {
1665            transaction
1666                .write_range(&value.path, value.offset, Bytes::from(value.contents))
1667                .await
1668        }
1669        Mutation::Resize(value) => transaction.resize(&value.path, value.logical_bytes).await,
1670        Mutation::ZeroRange(value) => {
1671            let range = required(value.range, "range")?;
1672            transaction
1673                .zero_range(
1674                    &value.path,
1675                    ByteRange {
1676                        offset: range.offset,
1677                        length: range.length,
1678                    },
1679                    value.allocated,
1680                    value.extend,
1681                )
1682                .await
1683        }
1684        Mutation::Preallocate(value) => {
1685            let range = required(value.range, "range")?;
1686            transaction
1687                .preallocate(
1688                    &value.path,
1689                    ByteRange {
1690                        offset: range.offset,
1691                        length: range.length,
1692                    },
1693                    value.keep_size,
1694                )
1695                .await
1696        }
1697        Mutation::CloneRange(value) => {
1698            transaction
1699                .clone_range(
1700                    &value.source,
1701                    value.source_offset,
1702                    &value.destination,
1703                    value.destination_offset,
1704                    value.length,
1705                )
1706                .await
1707        }
1708        Mutation::SetMetadata(value) => {
1709            transaction
1710                .set_metadata(&value.path, metadata(value.metadata)?)
1711                .await
1712        }
1713        Mutation::CreateDirectories(value) => transaction.create_dir_all(&value.path).await,
1714        Mutation::PutFile(value) => {
1715            transaction
1716                .write(&value.path, Bytes::from(value.contents))
1717                .await
1718        }
1719        Mutation::CopyFile(value) => transaction.copy(&value.source, &value.destination).await,
1720    }
1721    .map_err(|error| status(&error))
1722}
1723
1724fn metadata(value: Option<wire::Metadata>) -> Result<FileMetadata, Status> {
1725    let value = value.unwrap_or_default();
1726    if value.has_named_attributes || value.has_acl || value.has_security_descriptor {
1727        return Err(Status::invalid_argument(
1728            "opaque metadata bodies are required separately",
1729        ));
1730    }
1731    Ok(FileMetadata {
1732        posix_mode: input_u32(value.posix_mode)?,
1733        posix_uid: input_u32(value.posix_uid)?,
1734        posix_gid: input_u32(value.posix_gid)?,
1735        posix_flags: input_u64(value.posix_flags)?,
1736        windows_attributes: input_u32(value.windows_attributes)?,
1737        created_ns: input_i64(value.created_ns)?,
1738        modified_ns: input_i64(value.modified_ns)?,
1739        accessed_ns: input_i64(value.accessed_ns)?,
1740        changed_ns: input_i64(value.changed_ns)?,
1741        named_attributes: MetadataField::Unavailable,
1742        acl: MetadataField::Unavailable,
1743        security_descriptor: MetadataField::Unavailable,
1744    })
1745}
1746
1747fn input_u32(value: Option<wire::OptionalU32>) -> Result<MetadataField<u32>, Status> {
1748    match value.and_then(|value| value.value) {
1749        None | Some(wire::optional_u32::Value::Unavailable(true)) => Ok(MetadataField::Unavailable),
1750        Some(wire::optional_u32::Value::Present(value)) => Ok(MetadataField::Value(value)),
1751        Some(wire::optional_u32::Value::Unavailable(false)) => {
1752            Err(Status::invalid_argument("unavailable marker must be true"))
1753        }
1754    }
1755}
1756
1757fn input_u64(value: Option<wire::OptionalU64>) -> Result<MetadataField<u64>, Status> {
1758    match value.and_then(|value| value.value) {
1759        None | Some(wire::optional_u64::Value::Unavailable(true)) => Ok(MetadataField::Unavailable),
1760        Some(wire::optional_u64::Value::Present(value)) => Ok(MetadataField::Value(value)),
1761        Some(wire::optional_u64::Value::Unavailable(false)) => {
1762            Err(Status::invalid_argument("unavailable marker must be true"))
1763        }
1764    }
1765}
1766
1767fn input_i64(value: Option<wire::OptionalI64>) -> Result<MetadataField<i64>, Status> {
1768    match value.and_then(|value| value.value) {
1769        None | Some(wire::optional_i64::Value::Unavailable(true)) => Ok(MetadataField::Unavailable),
1770        Some(wire::optional_i64::Value::Present(value)) => Ok(MetadataField::Value(value)),
1771        Some(wire::optional_i64::Value::Unavailable(false)) => {
1772            Err(Status::invalid_argument("unavailable marker must be true"))
1773        }
1774    }
1775}
1776
1777fn profile(value: i32) -> Result<EngineProfile, Status> {
1778    match wire::FilesystemProfile::try_from(value).ok() {
1779        Some(wire::FilesystemProfile::Portable) => Ok(EngineProfile::Portable),
1780        Some(wire::FilesystemProfile::Posix) => Ok(EngineProfile::Posix),
1781        Some(wire::FilesystemProfile::Windows) => Ok(EngineProfile::Windows),
1782        Some(wire::FilesystemProfile::Browser) => Ok(EngineProfile::Browser),
1783        Some(wire::FilesystemProfile::Unspecified) | None => {
1784            Err(Status::invalid_argument("filesystem profile is required"))
1785        }
1786    }
1787}
1788
1789fn profile_message(value: EngineProfile) -> i32 {
1790    match value {
1791        EngineProfile::Portable => wire::FilesystemProfile::Portable as i32,
1792        EngineProfile::Posix => wire::FilesystemProfile::Posix as i32,
1793        EngineProfile::Windows => wire::FilesystemProfile::Windows as i32,
1794        EngineProfile::Browser => wire::FilesystemProfile::Browser as i32,
1795    }
1796}
1797
1798fn source_state_message(
1799    value: HostedSourceState,
1800) -> (wire::SourceState, wire::SourceInvalidationReason) {
1801    let none = wire::SourceInvalidationReason::Unspecified;
1802    match value {
1803        HostedSourceState::Clean => (wire::SourceState::Clean, none),
1804        HostedSourceState::PendingCapture => (wire::SourceState::PendingCapture, none),
1805        HostedSourceState::Conflict => (wire::SourceState::Conflict, none),
1806        HostedSourceState::Sealed => (wire::SourceState::Sealed, none),
1807        HostedSourceState::NeedsRescan(reason) => (
1808            wire::SourceState::NeedsRescan,
1809            match reason {
1810                HostedSourceInvalidation::InitialSnapshotRequired => {
1811                    wire::SourceInvalidationReason::InitialSnapshotRequired
1812                }
1813                HostedSourceInvalidation::QueueOverflow => {
1814                    wire::SourceInvalidationReason::QueueOverflow
1815                }
1816                HostedSourceInvalidation::NativeRescanRequired => {
1817                    wire::SourceInvalidationReason::NativeRescanRequired
1818                }
1819                HostedSourceInvalidation::BackendError => {
1820                    wire::SourceInvalidationReason::BackendError
1821                }
1822                HostedSourceInvalidation::UnrepresentablePath => {
1823                    wire::SourceInvalidationReason::UnrepresentablePath
1824                }
1825                HostedSourceInvalidation::AmbiguousRename => {
1826                    wire::SourceInvalidationReason::AmbiguousRename
1827                }
1828                HostedSourceInvalidation::RootChanged => {
1829                    wire::SourceInvalidationReason::RootChanged
1830                }
1831            },
1832        ),
1833    }
1834}
1835
1836fn operation(value: Option<wire::OperationOptions>) -> Result<IdempotencyKey, Status> {
1837    let value = required(value, "operation")?;
1838    Ok(IdempotencyKey::from_bytes(raw_operation_bytes(
1839        &value.idempotency_key,
1840    )?))
1841}
1842
1843fn raw_operation(value: &[u8]) -> Result<crate::OperationId, Status> {
1844    Ok(crate::OperationId::from_bytes(raw_operation_bytes(value)?))
1845}
1846
1847fn raw_operation_bytes(value: &[u8]) -> Result<[u8; 16], Status> {
1848    value
1849        .try_into()
1850        .map_err(|_| Status::invalid_argument("idempotency_key must contain 16 bytes"))
1851}
1852
1853fn generation_id(value: &[u8]) -> Result<GenerationId, Status> {
1854    let bytes: [u8; 32] = value
1855        .try_into()
1856        .map_err(|_| Status::invalid_argument("generation_id must contain 32 bytes"))?;
1857    Ok(GenerationId::new(Digest::from_bytes(bytes)))
1858}
1859
1860fn workspace_ref<A, O>(workspace: &Workspace<A, O>) -> wire::WorkspaceRef {
1861    wire::WorkspaceRef {
1862        workspace_id: workspace.id().into_bytes().to_vec(),
1863        name: workspace.name().as_str().to_owned(),
1864    }
1865}
1866
1867fn generation_ref<A, O>(generation: &Generation<A, O>) -> wire::GenerationRef {
1868    generation_ref_for(&generation.workspace, generation.id())
1869}
1870
1871fn generation_ref_for<A, O>(
1872    workspace: &Workspace<A, O>,
1873    generation: GenerationId,
1874) -> wire::GenerationRef {
1875    wire::GenerationRef {
1876        workspace: Some(workspace_ref(workspace)),
1877        generation_id: generation.digest().into_bytes().to_vec(),
1878    }
1879}
1880
1881fn join_common_ancestor_ref<A, O>(
1882    plan: &crate::JoinPlan<A, O>,
1883    source: &Workspace<A, O>,
1884    target: &Workspace<A, O>,
1885) -> wire::GenerationRef {
1886    let volume = plan.common_ancestor_volume();
1887    let workspace = if volume == source.id().volume_id() {
1888        Some(workspace_ref(source))
1889    } else if volume == target.id().volume_id() {
1890        Some(workspace_ref(target))
1891    } else {
1892        None
1893    };
1894    wire::GenerationRef {
1895        workspace,
1896        generation_id: plan.common_ancestor().digest().into_bytes().to_vec(),
1897    }
1898}
1899
1900fn observed_mutation<A, O>(
1901    workspace: &Workspace<A, O>,
1902    commit: &DurableCommit,
1903) -> Result<wire::MutationResponse, Status> {
1904    const MAXIMUM_EVENT_BYTES: u64 = 4 * 1024;
1905    let expected = workspace.id().volume_id();
1906    if let Ok(deleted) =
1907        crate::kernel::decode_workspace_deleted(&commit.payload, MAXIMUM_EVENT_BYTES)
1908    {
1909        if deleted != expected {
1910            return Err(Status::data_loss("operation workspace identity mismatch"));
1911        }
1912        return Ok(mutation(wire::MutationStatus::Committed, None, None));
1913    }
1914    let root = if let Ok(publication) =
1915        crate::kernel::decode_published_generation(&commit.payload, MAXIMUM_EVENT_BYTES)
1916    {
1917        if publication.volume_id != expected {
1918            return Err(Status::data_loss("operation workspace identity mismatch"));
1919        }
1920        publication.generation_root
1921    } else if let Ok(created) =
1922        crate::kernel::decode_volume_created(&commit.payload, MAXIMUM_EVENT_BYTES)
1923    {
1924        if created.volume_id != expected {
1925            return Err(Status::data_loss("operation workspace identity mismatch"));
1926        }
1927        created.initial_generation_root
1928    } else {
1929        return Err(Status::data_loss(
1930            "operation does not contain a workspace mutation",
1931        ));
1932    };
1933    Ok(mutation(
1934        wire::MutationStatus::Committed,
1935        Some(wire::GenerationRef {
1936            workspace: Some(workspace_ref(workspace)),
1937            generation_id: root.digest.into_bytes().to_vec(),
1938        }),
1939        None,
1940    ))
1941}
1942
1943async fn workspace_message<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1944    workspace: &Workspace<A, O>,
1945) -> Result<wire::Workspace, Status> {
1946    let head = workspace.head().await.map_err(|error| status(&error))?;
1947    Ok(wire::Workspace {
1948        workspace: Some(workspace_ref(workspace)),
1949        name: workspace.name().as_str().to_owned(),
1950        profile: profile_message(workspace.profile()),
1951        head: Some(generation_ref(&head)),
1952        deleted: false,
1953    })
1954}
1955
1956async fn generation_message<A: AsyncAuthorityStore, O: AsyncObjectStore>(
1957    generation: &Generation<A, O>,
1958) -> Result<wire::GenerationResponse, Status> {
1959    let reference = generation_ref(generation);
1960    let workspace = reference.workspace.clone();
1961    let parents = generation
1962        .parents()
1963        .await
1964        .map_err(|error| status(&error))?
1965        .into_iter()
1966        .map(|id| wire::GenerationRef {
1967            workspace: workspace.clone(),
1968            generation_id: id.digest().into_bytes().to_vec(),
1969        })
1970        .collect();
1971    Ok(wire::GenerationResponse {
1972        generation: Some(reference),
1973        parents,
1974    })
1975}
1976
1977fn commit_message<A, O>(value: TransactionCommit<A, O>) -> wire::MutationResponse {
1978    match value {
1979        TransactionCommit::Committed(generation) => mutation(
1980            wire::MutationStatus::Committed,
1981            Some(generation_ref(&generation)),
1982            None,
1983        ),
1984        TransactionCommit::AlreadyCommitted(generation) => mutation(
1985            wire::MutationStatus::AlreadyCommitted,
1986            Some(generation_ref(&generation)),
1987            None,
1988        ),
1989        TransactionCommit::Conflict { actual } => mutation(
1990            wire::MutationStatus::Conflict,
1991            None,
1992            Some(generation_ref(&actual)),
1993        ),
1994        TransactionCommit::Fenced => mutation(wire::MutationStatus::Fenced, None, None),
1995        TransactionCommit::IdempotencyConflict => {
1996            mutation(wire::MutationStatus::IdempotencyConflict, None, None)
1997        }
1998    }
1999}
2000
2001fn join_message<A, O>(value: JoinOutcome<A, O>) -> wire::JoinResponse {
2002    match value {
2003        JoinOutcome::Applied(generation) => {
2004            join_response(wire::JoinStatus::Applied, Some(&generation))
2005        }
2006        JoinOutcome::AlreadyApplied(generation) => {
2007            join_response(wire::JoinStatus::AlreadyApplied, Some(&generation))
2008        }
2009        JoinOutcome::NoChanges(generation) => {
2010            join_response(wire::JoinStatus::NoChanges, Some(&generation))
2011        }
2012        JoinOutcome::StaleTarget(actual) => {
2013            join_response(wire::JoinStatus::StaleTarget, Some(&actual))
2014        }
2015        JoinOutcome::Conflicted {
2016            conflicts,
2017            truncated,
2018        } => wire::JoinResponse {
2019            status: wire::JoinStatus::Conflicted as i32,
2020            generation: None,
2021            conflicts: conflicts.into_iter().map(merge_conflict).collect(),
2022            truncated,
2023        },
2024        JoinOutcome::Fenced => join_response::<A, O>(wire::JoinStatus::Fenced, None),
2025        JoinOutcome::IdempotencyConflict => {
2026            join_response::<A, O>(wire::JoinStatus::IdempotencyConflict, None)
2027        }
2028    }
2029}
2030
2031fn join_response<A, O>(
2032    status: wire::JoinStatus,
2033    generation: Option<&Generation<A, O>>,
2034) -> wire::JoinResponse {
2035    wire::JoinResponse {
2036        status: status as i32,
2037        generation: generation.map(generation_ref),
2038        conflicts: Vec::new(),
2039        truncated: false,
2040    }
2041}
2042
2043fn file_record_change(change: &crate::FileRecordChange) -> wire::FileRecordChange {
2044    wire::FileRecordChange {
2045        file_id: change.file_id.into_bytes().to_vec(),
2046        before: change.before.map(file_record_snapshot),
2047        after: change.after.map(file_record_snapshot),
2048    }
2049}
2050
2051fn file_record_snapshot(record: crate::kernel::FileRecord) -> wire::FileRecordSnapshot {
2052    let mut value = wire::FileRecordSnapshot {
2053        file_id: record.file_id.into_bytes().to_vec(),
2054        file_kind: file_kind(record.kind),
2055        link_count: record.link_count,
2056        metadata_object: encode_object_id(record.metadata),
2057        payload_kind: String::new(),
2058        logical_bytes: None,
2059        payload_object: Vec::new(),
2060        inline_bytes: Vec::new(),
2061        device_major: None,
2062        device_minor: None,
2063    };
2064    match record.payload {
2065        FilePayload::InlineRegular(bytes) => {
2066            value.payload_kind = "inline-regular".to_owned();
2067            value.logical_bytes = Some(output_u64(Some(
2068                u64::try_from(bytes.as_bytes().len()).unwrap_or(u64::MAX),
2069            )));
2070            value.inline_bytes = bytes.as_bytes().to_vec();
2071        }
2072        FilePayload::Regular {
2073            logical_bytes,
2074            extents,
2075        } => {
2076            value.payload_kind = "regular".to_owned();
2077            value.logical_bytes = Some(output_u64(Some(logical_bytes)));
2078            value.payload_object = encode_object_id(extents);
2079        }
2080        FilePayload::Directory { entries } => {
2081            value.payload_kind = "directory".to_owned();
2082            value.payload_object = encode_object_id(entries);
2083        }
2084        FilePayload::SymbolicLink {
2085            target_bytes,
2086            target,
2087        } => {
2088            value.payload_kind = "symbolic-link".to_owned();
2089            value.logical_bytes = Some(output_u64(Some(target_bytes)));
2090            value.payload_object = encode_object_id(target);
2091        }
2092        FilePayload::Empty => value.payload_kind = "empty".to_owned(),
2093        FilePayload::Device { major, minor } => {
2094            value.payload_kind = "device".to_owned();
2095            value.device_major = Some(output_u32(Some(major)));
2096            value.device_minor = Some(output_u32(Some(minor)));
2097        }
2098        FilePayload::ReparsePoint {
2099            payload_bytes,
2100            payload,
2101        } => {
2102            value.payload_kind = "reparse-point".to_owned();
2103            value.logical_bytes = Some(output_u64(Some(payload_bytes)));
2104            value.payload_object = encode_object_id(payload);
2105        }
2106    }
2107    value
2108}
2109
2110fn directory_binding_change(
2111    change: &crate::DirectoryBindingChange,
2112) -> wire::DirectoryBindingChange {
2113    wire::DirectoryBindingChange {
2114        directory_id: change.directory_id.into_bytes().to_vec(),
2115        name: Some(logical_name(&change.name)),
2116        before: change.before.as_ref().map(tree_entry_snapshot),
2117        after: change.after.as_ref().map(tree_entry_snapshot),
2118    }
2119}
2120
2121fn tree_entry_snapshot(entry: &TreeEntry) -> wire::TreeEntrySnapshot {
2122    wire::TreeEntrySnapshot {
2123        name: Some(logical_name(&entry.name)),
2124        file_id: entry.file_id.into_bytes().to_vec(),
2125        file_kind: file_kind(entry.kind),
2126    }
2127}
2128
2129fn logical_name(name: &LogicalName) -> wire::LogicalName {
2130    let encoding = match name.encoding() {
2131        NameEncoding::Utf8 => wire::NameEncoding::Utf8,
2132        NameEncoding::PosixBytes => wire::NameEncoding::PosixBytes,
2133        NameEncoding::WindowsUtf16Le => wire::NameEncoding::WindowsUtf16le,
2134    };
2135    wire::LogicalName {
2136        encoding: encoding as i32,
2137        bytes: name.as_bytes().to_vec(),
2138    }
2139}
2140
2141fn input_logical_name(name: wire::LogicalName) -> Result<LogicalName, Status> {
2142    let encoding = match wire::NameEncoding::try_from(name.encoding)
2143        .map_err(|_| Status::invalid_argument("name encoding is invalid"))?
2144    {
2145        wire::NameEncoding::Utf8 => NameEncoding::Utf8,
2146        wire::NameEncoding::PosixBytes => NameEncoding::PosixBytes,
2147        wire::NameEncoding::WindowsUtf16le => NameEncoding::WindowsUtf16Le,
2148        wire::NameEncoding::Unspecified => {
2149            return Err(Status::invalid_argument("name encoding is required"));
2150        }
2151    };
2152    LogicalName::new(encoding, name.bytes, 255)
2153        .map_err(|error| Status::invalid_argument(error.to_string()))
2154}
2155
2156fn work_counters(work: crate::WorkCounters) -> wire::WorkCounters {
2157    wire::WorkCounters {
2158        authority_records_read: work.authority_records_read,
2159        authority_records_appended: work.authority_records_appended,
2160        authority_bytes_read: work.authority_bytes_read,
2161        authority_bytes_written: work.authority_bytes_written,
2162        object_probes: work.object_probes,
2163        backend_read_operations: work.backend_read_operations,
2164        backend_write_operations: work.backend_write_operations,
2165        durability_operations: work.durability_operations,
2166        page_reads: work.page_reads,
2167        page_writes: work.page_writes,
2168        object_bytes_read: work.object_bytes_read,
2169        object_bytes_written: work.object_bytes_written,
2170        bytes_hashed: work.bytes_hashed,
2171        bytes_copied: work.bytes_copied,
2172        bytes_encoded: work.bytes_encoded,
2173        source_bytes_read: work.source_bytes_read,
2174        output_bytes: work.output_bytes,
2175        items_examined: work.items_examined,
2176        items_returned: work.items_returned,
2177        allocation_operations: work.allocation_operations,
2178        peak_allocation_bytes: work.peak_allocation_bytes,
2179        materializations: work.materializations,
2180    }
2181}
2182
2183fn join_plan_id(
2184    source: &wire::GenerationRef,
2185    target: &wire::GenerationRef,
2186    base: &wire::GenerationRef,
2187    history: JoinHistory,
2188    maximum_generations: u32,
2189    maximum_changes: u32,
2190    maximum_conflicts: u32,
2191) -> [u8; 32] {
2192    let mut hasher = blake3::Hasher::new();
2193    hasher.update(b"acyclic-fs-wire-join-plan-v2\0");
2194    for reference in [source, target, base] {
2195        if let Some(workspace) = &reference.workspace {
2196            hasher.update(&(workspace.workspace_id.len() as u64).to_le_bytes());
2197            hasher.update(&workspace.workspace_id);
2198            hasher.update(&(workspace.name.len() as u64).to_le_bytes());
2199            hasher.update(workspace.name.as_bytes());
2200        } else {
2201            hasher.update(&0_u64.to_le_bytes());
2202            hasher.update(&0_u64.to_le_bytes());
2203        }
2204        hasher.update(&(reference.generation_id.len() as u64).to_le_bytes());
2205        hasher.update(&reference.generation_id);
2206    }
2207    hasher.update(&[join_history_code(history)]);
2208    hasher.update(&maximum_generations.to_le_bytes());
2209    hasher.update(&maximum_changes.to_le_bytes());
2210    hasher.update(&maximum_conflicts.to_le_bytes());
2211    *hasher.finalize().as_bytes()
2212}
2213
2214fn join_history(value: i32) -> Result<JoinHistory, Status> {
2215    match wire::JoinHistory::try_from(value)
2216        .map_err(|_| Status::invalid_argument("join history is invalid"))?
2217    {
2218        wire::JoinHistory::Merge => Ok(JoinHistory::Merge),
2219        wire::JoinHistory::Rebase => Ok(JoinHistory::Rebase),
2220        wire::JoinHistory::Squash => Ok(JoinHistory::Squash),
2221        wire::JoinHistory::CherryPick => Ok(JoinHistory::CherryPick),
2222        wire::JoinHistory::Unspecified => {
2223            Err(Status::invalid_argument("join history must be specified"))
2224        }
2225    }
2226}
2227
2228const fn join_history_code(value: JoinHistory) -> u8 {
2229    match value {
2230        JoinHistory::Merge => 1,
2231        JoinHistory::Rebase => 2,
2232        JoinHistory::Squash => 3,
2233        JoinHistory::CherryPick => 4,
2234    }
2235}
2236
2237fn encode_object_id(object: ObjectId) -> Vec<u8> {
2238    let mut bytes = Vec::with_capacity(33);
2239    bytes.push(object.kind.canonical_tag());
2240    bytes.extend_from_slice(object.digest.as_bytes());
2241    bytes
2242}
2243
2244fn decode_object_id(bytes: &[u8]) -> Result<ObjectId, Status> {
2245    if bytes.len() != 33 {
2246        return Err(Status::invalid_argument(
2247            "object identity must contain kind and digest",
2248        ));
2249    }
2250    let (&tag, digest_bytes) = bytes
2251        .split_first()
2252        .ok_or_else(|| Status::invalid_argument("object identity must contain kind and digest"))?;
2253    let kind = ObjectKind::from_canonical_tag(tag)
2254        .map_err(|error| Status::invalid_argument(error.to_string()))?;
2255    let digest: [u8; 32] = digest_bytes
2256        .try_into()
2257        .map_err(|_| Status::invalid_argument("object digest is malformed"))?;
2258    Ok(ObjectId {
2259        kind,
2260        digest: Digest::from_bytes(digest),
2261    })
2262}
2263
2264fn transfer_cursor(bytes: &[u8]) -> Result<u64, Status> {
2265    if bytes.is_empty() {
2266        return Ok(0);
2267    }
2268    let cursor: [u8; 8] = bytes
2269        .try_into()
2270        .map_err(|_| Status::invalid_argument("transfer cursor must contain 8 bytes"))?;
2271    Ok(u64::from_le_bytes(cursor))
2272}
2273
2274fn mutation(
2275    status: wire::MutationStatus,
2276    generation: Option<wire::GenerationRef>,
2277    actual_head: Option<wire::GenerationRef>,
2278) -> wire::MutationResponse {
2279    wire::MutationResponse {
2280        status: status as i32,
2281        generation,
2282        actual_head,
2283        conflicts: Vec::new(),
2284        truncated: false,
2285    }
2286}
2287
2288fn mutation_with_conflicts(
2289    status: wire::MutationStatus,
2290    conflicts: Vec<wire::Conflict>,
2291    truncated: bool,
2292) -> wire::MutationResponse {
2293    wire::MutationResponse {
2294        status: status as i32,
2295        generation: None,
2296        actual_head: None,
2297        conflicts,
2298        truncated,
2299    }
2300}
2301
2302fn transaction_conflict(value: TransactionConflict) -> wire::Conflict {
2303    use wire::conflict::Region;
2304    let region = match value.region {
2305        TransactionConflictRegion::FileRecord(file_id) => Region::FileRecord(wire::FileConflict {
2306            file_id: file_id.into_bytes().to_vec(),
2307        }),
2308        TransactionConflictRegion::Metadata(file_id) => Region::Metadata(wire::FileConflict {
2309            file_id: file_id.into_bytes().to_vec(),
2310        }),
2311        TransactionConflictRegion::FileLength(file_id) => Region::FileLength(wire::FileConflict {
2312            file_id: file_id.into_bytes().to_vec(),
2313        }),
2314        TransactionConflictRegion::ContentRange {
2315            file_id,
2316            offset,
2317            length,
2318        } => Region::ContentRange(wire::ContentConflict {
2319            file_id: file_id.into_bytes().to_vec(),
2320            range: Some(wire::ByteRange { offset, length }),
2321        }),
2322        TransactionConflictRegion::SparseSeek {
2323            file_id,
2324            offset,
2325            target,
2326        } => Region::SparseSeek(wire::SparseConflict {
2327            file_id: file_id.into_bytes().to_vec(),
2328            offset,
2329            target: match target {
2330                TransactionSparseSeek::Data => wire::SparseTarget::Data as i32,
2331                TransactionSparseSeek::Hole => wire::SparseTarget::Hole as i32,
2332            },
2333        }),
2334        TransactionConflictRegion::DirectoryName { directory_id, name } => {
2335            Region::DirectoryName(wire::DirectoryNameConflict {
2336                directory_id: directory_id.into_bytes().to_vec(),
2337                name: Some(logical_name(&name)),
2338            })
2339        }
2340        TransactionConflictRegion::DirectoryRange {
2341            directory_id,
2342            after,
2343            maximum_entries,
2344        } => Region::DirectoryRange(wire::DirectoryRangeConflict {
2345            directory_id: directory_id.into_bytes().to_vec(),
2346            after: after.as_ref().map(logical_name),
2347            maximum_entries,
2348        }),
2349    };
2350    wire::Conflict {
2351        region: Some(region),
2352        r#use: match value.usage {
2353            TransactionDependencyUse::Observation => wire::ConflictUse::Observation as i32,
2354            TransactionDependencyUse::Mutation => wire::ConflictUse::Mutation as i32,
2355            TransactionDependencyUse::ObservationAndMutation => {
2356                wire::ConflictUse::ObservationAndMutation as i32
2357            }
2358        },
2359        expected_digest: value
2360            .expected
2361            .map_or_else(Vec::new, |value| value.as_bytes().to_vec()),
2362        actual_digest: value
2363            .actual
2364            .map_or_else(Vec::new, |value| value.as_bytes().to_vec()),
2365    }
2366}
2367
2368fn merge_conflict(value: MergeConflict) -> wire::Conflict {
2369    let region = match value {
2370        MergeConflict::File(file_id) => wire::conflict::Region::FileRecord(wire::FileConflict {
2371            file_id: file_id.into_bytes().to_vec(),
2372        }),
2373        MergeConflict::Binding { directory_id, name } => {
2374            wire::conflict::Region::DirectoryName(wire::DirectoryNameConflict {
2375                directory_id: directory_id.into_bytes().to_vec(),
2376                name: Some(logical_name(&name)),
2377            })
2378        }
2379    };
2380    wire::Conflict {
2381        region: Some(region),
2382        r#use: wire::ConflictUse::Unspecified as i32,
2383        expected_digest: Vec::new(),
2384        actual_digest: Vec::new(),
2385    }
2386}
2387
2388fn rebase_message<A, O>(value: WorkspaceRebase<A, O>) -> wire::RebaseResponse {
2389    match value {
2390        WorkspaceRebase::Rebased(generation) => wire::RebaseResponse {
2391            status: wire::RebaseStatus::Rebased as i32,
2392            generation: Some(generation_ref(&generation)),
2393            conflicts: Vec::new(),
2394            truncated: false,
2395        },
2396        WorkspaceRebase::AlreadyRebased(generation) => wire::RebaseResponse {
2397            status: wire::RebaseStatus::AlreadyRebased as i32,
2398            generation: Some(generation_ref(&generation)),
2399            conflicts: Vec::new(),
2400            truncated: false,
2401        },
2402        WorkspaceRebase::Current(generation) => wire::RebaseResponse {
2403            status: wire::RebaseStatus::Current as i32,
2404            generation: Some(generation_ref(&generation)),
2405            conflicts: Vec::new(),
2406            truncated: false,
2407        },
2408        WorkspaceRebase::Stale(generation) => wire::RebaseResponse {
2409            status: wire::RebaseStatus::Stale as i32,
2410            generation: Some(generation_ref(&generation)),
2411            conflicts: Vec::new(),
2412            truncated: false,
2413        },
2414        WorkspaceRebase::Conflicted {
2415            conflicts,
2416            truncated,
2417        } => wire::RebaseResponse {
2418            status: wire::RebaseStatus::Conflicted as i32,
2419            generation: None,
2420            conflicts: conflicts.into_iter().map(merge_conflict).collect(),
2421            truncated,
2422        },
2423        WorkspaceRebase::Fenced => wire::RebaseResponse {
2424            status: wire::RebaseStatus::Fenced as i32,
2425            generation: None,
2426            conflicts: Vec::new(),
2427            truncated: false,
2428        },
2429        WorkspaceRebase::IdempotencyConflict => wire::RebaseResponse {
2430            status: wire::RebaseStatus::IdempotencyConflict as i32,
2431            generation: None,
2432            conflicts: Vec::new(),
2433            truncated: false,
2434        },
2435    }
2436}
2437
2438fn stat_message(value: &crate::WorkspaceStat) -> wire::FileStat {
2439    wire::FileStat {
2440        file_id: value.file_id.into_bytes().to_vec(),
2441        kind: file_kind(value.kind),
2442        link_count: value.link_count,
2443        logical_bytes: Some(output_u64(value.logical_bytes)),
2444        metadata: Some(metadata_message(value.metadata)),
2445    }
2446}
2447
2448fn metadata_message(value: WorkspaceMetadata) -> wire::Metadata {
2449    wire::Metadata {
2450        posix_mode: Some(output_u32(value.posix_mode)),
2451        posix_uid: Some(output_u32(value.posix_uid)),
2452        posix_gid: Some(output_u32(value.posix_gid)),
2453        posix_flags: Some(output_u64(value.posix_flags)),
2454        windows_attributes: Some(output_u32(value.windows_attributes)),
2455        created_ns: Some(output_i64(value.created_ns)),
2456        modified_ns: Some(output_i64(value.modified_ns)),
2457        accessed_ns: Some(output_i64(value.accessed_ns)),
2458        changed_ns: Some(output_i64(value.changed_ns)),
2459        has_named_attributes: value.has_named_attributes,
2460        has_acl: value.has_acl,
2461        has_security_descriptor: value.has_security_descriptor,
2462    }
2463}
2464
2465fn output_u32(value: Option<u32>) -> wire::OptionalU32 {
2466    wire::OptionalU32 {
2467        value: Some(value.map_or(
2468            wire::optional_u32::Value::Unavailable(true),
2469            wire::optional_u32::Value::Present,
2470        )),
2471    }
2472}
2473
2474fn output_u64(value: Option<u64>) -> wire::OptionalU64 {
2475    wire::OptionalU64 {
2476        value: Some(value.map_or(
2477            wire::optional_u64::Value::Unavailable(true),
2478            wire::optional_u64::Value::Present,
2479        )),
2480    }
2481}
2482
2483fn output_i64(value: Option<i64>) -> wire::OptionalI64 {
2484    wire::OptionalI64 {
2485        value: Some(value.map_or(
2486            wire::optional_i64::Value::Unavailable(true),
2487            wire::optional_i64::Value::Present,
2488        )),
2489    }
2490}
2491
2492fn file_kind(value: EngineFileKind) -> i32 {
2493    (match value {
2494        EngineFileKind::Regular => wire::FileKind::Regular,
2495        EngineFileKind::Directory => wire::FileKind::Directory,
2496        EngineFileKind::SymbolicLink => wire::FileKind::SymbolicLink,
2497        EngineFileKind::Fifo => wire::FileKind::Fifo,
2498        EngineFileKind::Socket => wire::FileKind::Socket,
2499        EngineFileKind::CharacterDevice => wire::FileKind::CharacterDevice,
2500        EngineFileKind::BlockDevice => wire::FileKind::BlockDevice,
2501        EngineFileKind::ReparsePoint => wire::FileKind::ReparsePoint,
2502        EngineFileKind::MountBoundary => wire::FileKind::MountBoundary,
2503    }) as i32
2504}
2505
2506fn required<T>(value: Option<T>, name: &'static str) -> Result<T, Status> {
2507    value.ok_or_else(|| Status::invalid_argument(format!("{name} is required")))
2508}
2509
2510fn status(error: &WorkspaceError) -> Status {
2511    match error {
2512        WorkspaceError::NotFound => Status::not_found(error.to_string()),
2513        WorkspaceError::Name(_)
2514        | WorkspaceError::Path(_)
2515        | WorkspaceError::ReadLimitExceeded
2516        | WorkspaceError::InvalidMergeResolution
2517        | WorkspaceError::NotRegularFile
2518        | WorkspaceError::NotDirectory
2519        | WorkspaceError::EmptyContentSet
2520        | WorkspaceError::ContentLengthOverflow => Status::invalid_argument(error.to_string()),
2521        WorkspaceError::ForeignGeneration
2522        | WorkspaceError::StaleGeneration
2523        | WorkspaceError::StaleIdentity
2524        | WorkspaceError::IncompatibleWorkspace
2525        | WorkspaceError::ChangeSetContinuity
2526        | WorkspaceError::RetentionConflict
2527        | WorkspaceError::NoCommonAncestor
2528        | WorkspaceError::LineageLimit
2529        | WorkspaceError::JoinLimit
2530        | WorkspaceError::ChangedPathLimit
2531        | WorkspaceError::NotFork => Status::failed_precondition(error.to_string()),
2532        WorkspaceError::Cancelled(_) => Status::cancelled(error.to_string()),
2533        WorkspaceError::Work(_) => Status::resource_exhausted(error.to_string()),
2534        WorkspaceError::Engine(_) => Status::unavailable(error.to_string()),
2535    }
2536}
2537
2538#[cfg(test)]
2539mod tests {
2540    use super::*;
2541    use crate::wire::filesystem::v2::filesystem_service_server::FilesystemService as _;
2542
2543    struct TestIssuer;
2544
2545    struct TestSourceProvider {
2546        generation: std::sync::Mutex<Option<GenerationId>>,
2547    }
2548
2549    #[tonic::async_trait]
2550    impl FilesystemSourceProvider for TestSourceProvider {
2551        async fn state(&self, _scope: HostedSourceScope) -> Result<HostedSourceResult, Status> {
2552            Ok(HostedSourceResult {
2553                state: HostedSourceState::NeedsRescan(HostedSourceInvalidation::QueueOverflow),
2554                generation_id: None,
2555            })
2556        }
2557
2558        async fn reconcile(
2559            &self,
2560            operation: HostedSourceOperation,
2561        ) -> Result<HostedSourceResult, Status> {
2562            if operation.idempotency_key != IdempotencyKey::from_bytes([9; 16]) {
2563                return Err(Status::invalid_argument(
2564                    "unexpected reconciliation identity",
2565                ));
2566            }
2567            Ok(HostedSourceResult {
2568                state: HostedSourceState::Clean,
2569                generation_id: *self
2570                    .generation
2571                    .lock()
2572                    .map_err(|_| Status::internal("lock"))?,
2573            })
2574        }
2575
2576        async fn rescan(
2577            &self,
2578            _operation: HostedSourceOperation,
2579        ) -> Result<HostedSourceResult, Status> {
2580            Ok(HostedSourceResult {
2581                state: HostedSourceState::Clean,
2582                generation_id: *self
2583                    .generation
2584                    .lock()
2585                    .map_err(|_| Status::internal("lock"))?,
2586            })
2587        }
2588
2589        async fn seal(
2590            &self,
2591            _operation: HostedSourceOperation,
2592        ) -> Result<HostedSourceResult, Status> {
2593            Ok(HostedSourceResult {
2594                state: HostedSourceState::Sealed,
2595                generation_id: *self
2596                    .generation
2597                    .lock()
2598                    .map_err(|_| Status::internal("lock"))?,
2599            })
2600        }
2601    }
2602
2603    #[tonic::async_trait]
2604    impl FilesystemCredentialIssuer for TestIssuer {
2605        fn mount_enabled(&self) -> bool {
2606            true
2607        }
2608
2609        fn s3_enabled(&self) -> bool {
2610            true
2611        }
2612
2613        async fn issue(&self, request: CredentialGrantRequest) -> Result<CredentialGrant, Status> {
2614            Ok(match request.kind {
2615                CredentialKind::Mount => CredentialGrant::Bearer {
2616                    endpoint: "mount://test".to_owned(),
2617                    token: hex::encode(request.workspace_id.into_bytes()),
2618                    expires_at_unix_seconds: 1_900_000_000,
2619                },
2620                CredentialKind::S3 => CredentialGrant::S3 {
2621                    endpoint: "https://s3.test".to_owned(),
2622                    bucket: "opaque-bucket".to_owned(),
2623                    region: "auto".to_owned(),
2624                    access_key_id: "access".to_owned(),
2625                    secret_access_key: "secret".to_owned(),
2626                    session_token: String::new(),
2627                    expires_at_unix_seconds: 1_900_000_000,
2628                },
2629            })
2630        }
2631    }
2632
2633    fn operation(value: u8) -> Option<wire::OperationOptions> {
2634        Some(wire::OperationOptions {
2635            idempotency_key: operation_bytes(value),
2636        })
2637    }
2638
2639    fn operation_bytes(value: u8) -> Vec<u8> {
2640        let mut id = [0_u8; 16];
2641        id[15] = value;
2642        id.to_vec()
2643    }
2644
2645    fn mutation(value: wire::mutation::Mutation) -> wire::Mutation {
2646        wire::Mutation {
2647            mutation: Some(value),
2648        }
2649    }
2650
2651    #[tokio::test]
2652    async fn handshake_and_creation_expose_every_canonical_profile()
2653    -> Result<(), Box<dyn std::error::Error>> {
2654        let service = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?;
2655        let advertised = service
2656            .handshake(Request::new(wire::HandshakeRequest { harness: None }))
2657            .await?
2658            .into_inner()
2659            .capabilities
2660            .ok_or("missing capabilities")?;
2661        let profiles = [
2662            wire::FilesystemProfile::Portable,
2663            wire::FilesystemProfile::Posix,
2664            wire::FilesystemProfile::Windows,
2665            wire::FilesystemProfile::Browser,
2666        ];
2667        assert_eq!(advertised.profiles, profiles.map(|profile| profile as i32));
2668        for (index, selected) in profiles.into_iter().enumerate() {
2669            let created = service
2670                .create_workspace(Request::new(wire::CreateWorkspaceRequest {
2671                    name: format!("profile-{index}"),
2672                    profile: selected as i32,
2673                    operation: operation(u8::try_from(index + 1)?),
2674                }))
2675                .await?
2676                .into_inner()
2677                .workspace
2678                .ok_or("missing workspace")?;
2679            assert_eq!(created.profile, selected as i32);
2680            let observed = service
2681                .observe(Request::new(wire::ObserveRequest {
2682                    workspace: created.workspace.clone(),
2683                    operation_id: operation_bytes(u8::try_from(index + 1)?),
2684                }))
2685                .await?
2686                .into_inner();
2687            assert_eq!(observed.state, "completed");
2688            assert_eq!(
2689                observed.outcome.and_then(|outcome| outcome.generation),
2690                created.head
2691            );
2692        }
2693        Ok(())
2694    }
2695
2696    #[tokio::test]
2697    async fn source_provider_controls_capability_and_all_lifecycle_rpcs()
2698    -> Result<(), Box<dyn std::error::Error>> {
2699        let absent = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?;
2700        let capabilities = absent
2701            .handshake(Request::new(wire::HandshakeRequest { harness: None }))
2702            .await?
2703            .into_inner()
2704            .capabilities
2705            .ok_or("missing capabilities")?;
2706        assert!(!capabilities.source_reconciliation);
2707
2708        let provider = Arc::new(TestSourceProvider {
2709            generation: std::sync::Mutex::new(None),
2710        });
2711        let service = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?
2712            .with_source_provider(provider.clone());
2713        let capabilities = service
2714            .handshake(Request::new(wire::HandshakeRequest { harness: None }))
2715            .await?
2716            .into_inner()
2717            .capabilities
2718            .ok_or("missing capabilities")?;
2719        assert!(capabilities.source_reconciliation);
2720        let created = service
2721            .create_workspace(Request::new(wire::CreateWorkspaceRequest {
2722                name: "source".to_owned(),
2723                profile: wire::FilesystemProfile::Portable as i32,
2724                operation: operation(1),
2725            }))
2726            .await?
2727            .into_inner()
2728            .workspace
2729            .ok_or("missing workspace")?;
2730        let workspace = created.workspace.ok_or("missing workspace reference")?;
2731        let head = created.head.ok_or("missing head")?;
2732        *provider
2733            .generation
2734            .lock()
2735            .map_err(|_| "provider lock poisoned")? = Some(generation_id(&head.generation_id)?);
2736
2737        let state = service
2738            .get_source_state(Request::new(wire::SourceStateRequest {
2739                workspace: Some(workspace.clone()),
2740            }))
2741            .await?
2742            .into_inner();
2743        assert_eq!(state.state, wire::SourceState::NeedsRescan as i32);
2744        assert_eq!(
2745            state.reason,
2746            wire::SourceInvalidationReason::QueueOverflow as i32
2747        );
2748        assert!(state.generation.is_none());
2749
2750        let reconciled = service
2751            .reconcile_source(Request::new(wire::SourceOperationRequest {
2752                workspace: Some(workspace.clone()),
2753                operation: Some(wire::OperationOptions {
2754                    idempotency_key: vec![9; 16],
2755                }),
2756            }))
2757            .await?
2758            .into_inner();
2759        assert_eq!(reconciled.state, wire::SourceState::Clean as i32);
2760        assert_eq!(reconciled.generation.as_ref(), Some(&head));
2761
2762        for response in [
2763            service
2764                .rescan_source(Request::new(wire::SourceOperationRequest {
2765                    workspace: Some(workspace.clone()),
2766                    operation: operation(10),
2767                }))
2768                .await?
2769                .into_inner(),
2770            service
2771                .seal_source(Request::new(wire::SourceOperationRequest {
2772                    workspace: Some(workspace),
2773                    operation: operation(11),
2774                }))
2775                .await?
2776                .into_inner(),
2777        ] {
2778            assert_eq!(response.generation.as_ref(), Some(&head));
2779        }
2780        Ok(())
2781    }
2782
2783    #[tokio::test]
2784    async fn wire_adapter_uses_one_engine_for_bounded_customer_semantics()
2785    -> Result<(), Box<dyn std::error::Error>> {
2786        let service = FilesystemWireService::new(Fs::memory(), FilesystemWireLimits::default())?
2787            .with_credential_issuer(Arc::new(TestIssuer));
2788        let created = service
2789            .create_workspace(Request::new(wire::CreateWorkspaceRequest {
2790                name: "main".to_owned(),
2791                profile: wire::FilesystemProfile::Portable as i32,
2792                operation: operation(1),
2793            }))
2794            .await?
2795            .into_inner()
2796            .workspace
2797            .ok_or("missing workspace")?;
2798        let initial = created.head.ok_or("missing head")?;
2799        let workspace = created.workspace.ok_or("missing workspace ref")?;
2800
2801        let credential = service
2802            .issue_mount_credential(Request::new(wire::CredentialRequest {
2803                workspace: Some(workspace.clone()),
2804                generation: Some(initial.clone()),
2805                writable: false,
2806                expires_after_seconds: 60,
2807                operation: operation(8),
2808            }))
2809            .await?
2810            .into_inner();
2811        assert_eq!(credential.endpoint, "mount://test");
2812        assert!(matches!(
2813            credential.credential,
2814            Some(wire::credential_response::Credential::BearerToken(token)) if !token.is_empty()
2815        ));
2816
2817        let committed = service
2818            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
2819                base: Some(initial),
2820                mutations: vec![
2821                    mutation(wire::mutation::Mutation::CreateDirectory(
2822                        wire::CreateDirectory {
2823                            path: "/dir".to_owned(),
2824                            metadata: None,
2825                        },
2826                    )),
2827                    mutation(wire::mutation::Mutation::CreateFile(wire::CreateFile {
2828                        path: "/dir/value".to_owned(),
2829                        contents: b"value".to_vec(),
2830                        metadata: None,
2831                    })),
2832                ],
2833                operation: operation(2),
2834                maximum_conflicts: 32,
2835            }))
2836            .await?
2837            .into_inner();
2838        assert_eq!(committed.status, wire::MutationStatus::Committed as i32);
2839        let generation = committed.generation.ok_or("missing generation")?;
2840        let observed = service
2841            .observe(Request::new(wire::ObserveRequest {
2842                workspace: Some(workspace.clone()),
2843                operation_id: operation_bytes(2),
2844            }))
2845            .await?
2846            .into_inner();
2847        assert_eq!(observed.state, "completed");
2848        assert_eq!(
2849            observed
2850                .outcome
2851                .as_ref()
2852                .and_then(|outcome| outcome.generation.as_ref()),
2853            Some(&generation)
2854        );
2855        let unknown = service
2856            .cancel(Request::new(wire::CancelRequest {
2857                workspace: Some(workspace.clone()),
2858                operation_id: operation_bytes(99),
2859            }))
2860            .await?
2861            .into_inner()
2862            .operation
2863            .ok_or("missing cancellation observation")?;
2864        assert_eq!(unknown.state, "unknown");
2865        assert!(unknown.outcome.is_none());
2866        let read = service
2867            .read(Request::new(wire::ReadRequest {
2868                generation: Some(generation.clone()),
2869                path: "/dir/value".to_owned(),
2870                range: None,
2871                maximum_bytes: 5,
2872            }))
2873            .await?
2874            .into_inner();
2875        assert_eq!(read.contents, b"value");
2876
2877        let listed = service
2878            .list_directory(Request::new(wire::ListDirectoryRequest {
2879                generation: Some(generation.clone()),
2880                path: "/dir".to_owned(),
2881                page: Some(wire::PageOptions {
2882                    maximum_items: 8,
2883                    after: None,
2884                }),
2885            }))
2886            .await?
2887            .into_inner()
2888            .page
2889            .ok_or("missing directory page")?;
2890        assert_eq!(listed.entries.len(), 1);
2891        assert_eq!(
2892            listed.entries[0]
2893                .name
2894                .as_ref()
2895                .map(|name| name.bytes.as_slice()),
2896            Some(b"value".as_slice())
2897        );
2898
2899        let pinned = service
2900            .pin(Request::new(wire::RetainGenerationRequest {
2901                generation: Some(generation.clone()),
2902                identity: "release".to_owned(),
2903                operation: operation(3),
2904            }))
2905            .await?
2906            .into_inner();
2907        assert_eq!(pinned.status, wire::MutationStatus::Committed as i32);
2908
2909        let forked = service
2910            .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
2911                source: Some(generation),
2912                destination_name: "agent".to_owned(),
2913                operation: operation(4),
2914            }))
2915            .await?
2916            .into_inner()
2917            .workspace
2918            .ok_or("missing fork")?;
2919        assert_ne!(
2920            forked
2921                .workspace
2922                .as_ref()
2923                .ok_or("missing fork ref")?
2924                .workspace_id,
2925            workspace.workspace_id
2926        );
2927
2928        let main_head = service
2929            .get_head(Request::new(wire::GetHeadRequest {
2930                workspace: Some(workspace.clone()),
2931            }))
2932            .await?
2933            .into_inner()
2934            .generation
2935            .ok_or("missing main head")?;
2936        let fork_head = forked.head.clone().ok_or("missing fork head")?;
2937        let no_op_plan = service
2938            .plan_join(Request::new(wire::PlanJoinRequest {
2939                source: Some(main_head.clone()),
2940                target: Some(fork_head.clone()),
2941                maximum_changes: 32,
2942                maximum_conflicts: 8,
2943                maximum_generations: 32,
2944                history: wire::JoinHistory::Merge as i32,
2945            }))
2946            .await?
2947            .into_inner();
2948        let changed = service
2949            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
2950                base: Some(main_head),
2951                mutations: vec![mutation(wire::mutation::Mutation::Write(wire::Write {
2952                    path: "/dir/value".to_owned(),
2953                    offset: 0,
2954                    contents: b"new!!".to_vec(),
2955                }))],
2956                operation: operation(5),
2957                maximum_conflicts: 32,
2958            }))
2959            .await?
2960            .into_inner()
2961            .generation
2962            .ok_or("missing changed generation")?;
2963        let plan = service
2964            .plan_join(Request::new(wire::PlanJoinRequest {
2965                source: Some(changed.clone()),
2966                target: Some(fork_head),
2967                maximum_changes: 32,
2968                maximum_conflicts: 8,
2969                maximum_generations: 32,
2970                history: wire::JoinHistory::Merge as i32,
2971            }))
2972            .await?
2973            .into_inner();
2974        let fresh_no_op = service
2975            .apply_join(Request::new(wire::ApplyJoinRequest {
2976                plan: Some(no_op_plan.clone()),
2977                operation: operation(12),
2978            }))
2979            .await?
2980            .into_inner();
2981        assert_eq!(fresh_no_op.status, wire::JoinStatus::NoChanges as i32);
2982        let joined = service
2983            .apply_join(Request::new(wire::ApplyJoinRequest {
2984                plan: Some(plan.clone()),
2985                operation: operation(6),
2986            }))
2987            .await?
2988            .into_inner();
2989        assert_eq!(joined.status, wire::JoinStatus::Applied as i32);
2990        let joined_generation = joined.generation.ok_or("missing joined generation")?;
2991        let no_op_before_advance = service
2992            .apply_join(Request::new(wire::ApplyJoinRequest {
2993                plan: Some(no_op_plan.clone()),
2994                operation: operation(6),
2995            }))
2996            .await?
2997            .into_inner();
2998        assert_eq!(
2999            no_op_before_advance.status,
3000            wire::JoinStatus::IdempotencyConflict as i32
3001        );
3002        let advanced_target = service
3003            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3004                base: Some(joined_generation.clone()),
3005                mutations: vec![mutation(wire::mutation::Mutation::CreateFile(
3006                    wire::CreateFile {
3007                        path: "/target-only".to_owned(),
3008                        contents: b"target".to_vec(),
3009                        metadata: None,
3010                    },
3011                ))],
3012                operation: operation(11),
3013                maximum_conflicts: 32,
3014            }))
3015            .await?
3016            .into_inner()
3017            .generation
3018            .ok_or("missing advanced target generation")?;
3019        let repeated = service
3020            .apply_join(Request::new(wire::ApplyJoinRequest {
3021                plan: Some(plan.clone()),
3022                operation: operation(6),
3023            }))
3024            .await?
3025            .into_inner();
3026        assert_eq!(repeated.status, wire::JoinStatus::AlreadyApplied as i32);
3027        assert_eq!(repeated.generation.as_ref(), Some(&joined_generation));
3028        let no_op_after_advance = service
3029            .apply_join(Request::new(wire::ApplyJoinRequest {
3030                plan: Some(no_op_plan),
3031                operation: operation(6),
3032            }))
3033            .await?
3034            .into_inner();
3035        assert_eq!(
3036            no_op_after_advance.status,
3037            wire::JoinStatus::IdempotencyConflict as i32
3038        );
3039        let mut altered_bounds = plan.clone();
3040        altered_bounds.maximum_generations += 1;
3041        altered_bounds.plan_id = join_plan_id(
3042            altered_bounds.source.as_ref().ok_or("missing source")?,
3043            altered_bounds
3044                .expected_target
3045                .as_ref()
3046                .ok_or("missing target")?,
3047            altered_bounds
3048                .common_ancestor
3049                .as_ref()
3050                .ok_or("missing common ancestor")?,
3051            join_history(altered_bounds.history)?,
3052            altered_bounds.maximum_generations,
3053            altered_bounds.maximum_changes,
3054            altered_bounds.maximum_conflicts,
3055        )
3056        .to_vec();
3057        let altered_retry = service
3058            .apply_join(Request::new(wire::ApplyJoinRequest {
3059                plan: Some(altered_bounds),
3060                operation: operation(6),
3061            }))
3062            .await?
3063            .into_inner();
3064        assert_eq!(
3065            altered_retry.status,
3066            wire::JoinStatus::IdempotencyConflict as i32
3067        );
3068        let stale = service
3069            .apply_join(Request::new(wire::ApplyJoinRequest {
3070                plan: Some(plan),
3071                operation: operation(9),
3072            }))
3073            .await?
3074            .into_inner();
3075        assert_eq!(stale.status, wire::JoinStatus::StaleTarget as i32);
3076        assert_eq!(stale.generation.as_ref(), Some(&advanced_target));
3077
3078        let further_changed = service
3079            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3080                base: Some(changed.clone()),
3081                mutations: vec![mutation(wire::mutation::Mutation::CreateFile(
3082                    wire::CreateFile {
3083                        path: "/dir/next".to_owned(),
3084                        contents: b"next".to_vec(),
3085                        metadata: None,
3086                    },
3087                ))],
3088                operation: operation(10),
3089                maximum_conflicts: 32,
3090            }))
3091            .await?
3092            .into_inner()
3093            .generation
3094            .ok_or("missing further generation")?;
3095        let different_plan = service
3096            .plan_join(Request::new(wire::PlanJoinRequest {
3097                source: Some(further_changed),
3098                target: Some(advanced_target),
3099                maximum_changes: 32,
3100                maximum_conflicts: 8,
3101                maximum_generations: 32,
3102                history: wire::JoinHistory::Merge as i32,
3103            }))
3104            .await?
3105            .into_inner();
3106        let conflicting_retry = service
3107            .apply_join(Request::new(wire::ApplyJoinRequest {
3108                plan: Some(different_plan),
3109                operation: operation(6),
3110            }))
3111            .await?
3112            .into_inner();
3113        assert_eq!(
3114            conflicting_retry.status,
3115            wire::JoinStatus::IdempotencyConflict as i32
3116        );
3117
3118        let conflict_target = service
3119            .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
3120                source: Some(changed.clone()),
3121                destination_name: "conflict-target".to_owned(),
3122                operation: operation(20),
3123            }))
3124            .await?
3125            .into_inner()
3126            .workspace
3127            .ok_or("missing conflict target")?;
3128        let conflict_source = service
3129            .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
3130                source: Some(changed.clone()),
3131                destination_name: "conflict-source".to_owned(),
3132                operation: operation(21),
3133            }))
3134            .await?
3135            .into_inner()
3136            .workspace
3137            .ok_or("missing conflict source")?;
3138        let benign_source = service
3139            .fork_workspace(Request::new(wire::ForkWorkspaceRequest {
3140                source: Some(changed.clone()),
3141                destination_name: "benign-source".to_owned(),
3142                operation: operation(22),
3143            }))
3144            .await?
3145            .into_inner()
3146            .workspace
3147            .ok_or("missing benign source")?;
3148
3149        let conflict_target_head = service
3150            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3151                base: conflict_target.head,
3152                mutations: vec![mutation(wire::mutation::Mutation::Write(wire::Write {
3153                    path: "/dir/value".to_owned(),
3154                    offset: 0,
3155                    contents: b"target".to_vec(),
3156                }))],
3157                operation: operation(23),
3158                maximum_conflicts: 32,
3159            }))
3160            .await?
3161            .into_inner()
3162            .generation
3163            .ok_or("missing conflict target generation")?;
3164        let conflict_source_head = service
3165            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3166                base: conflict_source.head,
3167                mutations: vec![mutation(wire::mutation::Mutation::Write(wire::Write {
3168                    path: "/dir/value".to_owned(),
3169                    offset: 0,
3170                    contents: b"source".to_vec(),
3171                }))],
3172                operation: operation(24),
3173                maximum_conflicts: 32,
3174            }))
3175            .await?
3176            .into_inner()
3177            .generation
3178            .ok_or("missing conflict source generation")?;
3179        let conflict_plan = service
3180            .plan_join(Request::new(wire::PlanJoinRequest {
3181                source: Some(conflict_source_head),
3182                target: Some(conflict_target_head.clone()),
3183                maximum_changes: 32,
3184                maximum_conflicts: 8,
3185                maximum_generations: 32,
3186                history: wire::JoinHistory::Merge as i32,
3187            }))
3188            .await?
3189            .into_inner();
3190        let fresh_conflict = service
3191            .apply_join(Request::new(wire::ApplyJoinRequest {
3192                plan: Some(conflict_plan.clone()),
3193                operation: operation(25),
3194            }))
3195            .await?
3196            .into_inner();
3197        assert_eq!(fresh_conflict.status, wire::JoinStatus::Conflicted as i32);
3198
3199        let benign_source_head = service
3200            .apply_transaction(Request::new(wire::ApplyTransactionRequest {
3201                base: benign_source.head,
3202                mutations: vec![mutation(wire::mutation::Mutation::CreateFile(
3203                    wire::CreateFile {
3204                        path: "/benign".to_owned(),
3205                        contents: b"benign".to_vec(),
3206                        metadata: None,
3207                    },
3208                ))],
3209                operation: operation(26),
3210                maximum_conflicts: 32,
3211            }))
3212            .await?
3213            .into_inner()
3214            .generation
3215            .ok_or("missing benign source generation")?;
3216        let benign_plan = service
3217            .plan_join(Request::new(wire::PlanJoinRequest {
3218                source: Some(benign_source_head),
3219                target: Some(conflict_target_head),
3220                maximum_changes: 32,
3221                maximum_conflicts: 8,
3222                maximum_generations: 32,
3223                history: wire::JoinHistory::Merge as i32,
3224            }))
3225            .await?
3226            .into_inner();
3227        let benign_join = service
3228            .apply_join(Request::new(wire::ApplyJoinRequest {
3229                plan: Some(benign_plan),
3230                operation: operation(27),
3231            }))
3232            .await?
3233            .into_inner();
3234        assert_eq!(benign_join.status, wire::JoinStatus::Applied as i32);
3235        let bound_conflict = service
3236            .apply_join(Request::new(wire::ApplyJoinRequest {
3237                plan: Some(conflict_plan),
3238                operation: operation(27),
3239            }))
3240            .await?
3241            .into_inner();
3242        assert_eq!(
3243            bound_conflict.status,
3244            wire::JoinStatus::IdempotencyConflict as i32
3245        );
3246
3247        let mut exported = service
3248            .export(Request::new(wire::ExportRequest {
3249                generation: Some(changed),
3250                after: Vec::new(),
3251                maximum_objects: 64,
3252                maximum_bytes: 1024 * 1024,
3253            }))
3254            .await?
3255            .into_inner();
3256        let mut export_chunks = Vec::new();
3257        while let Some(chunk) = exported.next().await {
3258            export_chunks.push(chunk?);
3259        }
3260        assert!(export_chunks.len() > 1);
3261        assert!(export_chunks[0].object_id.is_empty());
3262        assert!(
3263            export_chunks
3264                .last()
3265                .ok_or("missing export terminal")?
3266                .terminal
3267        );
3268
3269        let deleted = service
3270            .delete_workspace(Request::new(wire::DeleteWorkspaceRequest {
3271                workspace: Some(workspace),
3272                operation: operation(7),
3273            }))
3274            .await?
3275            .into_inner();
3276        assert_eq!(deleted.status, wire::MutationStatus::Committed as i32);
3277        Ok(())
3278    }
3279}