lazaret/docs/schema.md

26 KiB

Lazaret schemas

This document is the normative design schema for Lazaret records and protocol data shapes. The specification defines their physical use, while the TLA+ models define lifecycle and ordering above the implementation abstraction. The diagrams define logical fields, variants, and relationships; they do not select a programming-language representation.

Domain Store

classDiagram
    class DomainStore

    class StoreMetadata {
        schema_id
        schema_version
    }

    class Domain {
        domain_id
        profile_name
        canonical_workspace
    }

    class Generation {
        domain_id
        generation_id
        configuration_fingerprint
        coordinator_id
        network_sidecar_id
        sandbox_id
        durable_decision
    }

    class Participant {
        generation_id
        role
        state
        ownership
    }

    class ParticipantState {
        <<enumeration>>
        Intent
        LeaseHeld
        Prepared
        Committed
        ReleaseIntent
        Clean
    }

    class ParticipantOwnership {
        <<union>>
        PreparedLower
        StateAnchor
        GitRouting
        Network
        Helper
        Sandbox
        Publication
    }

    class Session {
        generation_id
        session_id
        cli_id
        session_anchor_id
        session_phase
    }

    class SessionTerminal {
        generation_id
        session_id
        state
        container_id
    }

    class SessionTerminalState {
        <<enumeration>>
        Intent
        Running
        ReleaseIntent
        Clean
    }

    class Mailbox {
        generation_id
        identity_path
        inbox_path
        outbox_path
        state
    }

    DomainStore "1" *-- "1" StoreMetadata
    DomainStore "1" *-- "1" Domain
    Domain "1" --> "0..*" Generation
    Generation "1" --> "0..*" Participant
    Participant --> ParticipantState
    Participant --> "0..1" ParticipantOwnership
    Generation "1" --> "0..*" Session
    Session "1" --> "0..1" SessionTerminal
    SessionTerminal --> SessionTerminalState
    Generation "1" --> "0..1" Mailbox
    Mailbox --> ParticipantState

durable_decision and session_phase contain values defined by the application protocol model. This schema records them without redefining either state machine. Each store carries its own schema identity and version. The current Domain Store identity is lazaret-domain-store-v2 with version 2. Domain Store v1 metadata dispatches only to legacy migration handling; it is never decoded as v2. The Storage Catalog remains lazaret-storage-catalog-v1 with version 1.

Host process identity and staged ownership

classDiagram
    class PidfsProcessAuthorityV1 {
        boot_id
        pidfs_st_dev
        pidfs_st_ino
    }

    class PidfsProcessIdentityV1 {
        host_pid
        start_time_ticks
    }

    class LegacyUnverifiedProcessIdentityV1 {
        host_pid
        start_time_ticks
    }

    class HostProcessIdentity {
        <<union>>
        PidfsV1
        LegacyUnverifiedV1
    }

    class PersistedHostProcessOwnership {
        <<union>>
        Unassigned
        Staged
        Committed
    }

    class HostProcessOwnershipSlot {
        <<enumeration>>
        SandboxHostCoordinator
        SandboxRuntimeSupervisor
        SandboxAttachedRunsc
        StateAnchorDaemon
        GitRoutingDaemon
        NetworkRuntimeSupervisor
        HelperConnectivityHelper
        HelperRuntimeSupervisor
        SessionTerminalHostCoordinator
        SessionTerminalExecClient
        EscalationHostProcess
    }

    class ProcessOwnershipMessageVersion {
        <<enumeration>>
        V1
    }

    class StagedProcessOwnership {
        version
        domain_id
        generation_id
        coordinator_id
        slot
        identity
    }

    class ProcessOwnershipCommitAck {
        version
        committed
    }

    PidfsProcessIdentityV1 --> PidfsProcessAuthorityV1 : authority
    HostProcessIdentity --> PidfsProcessIdentityV1 : PidfsV1
    HostProcessIdentity --> LegacyUnverifiedProcessIdentityV1 : LegacyUnverifiedV1
    PersistedHostProcessOwnership --> PidfsProcessIdentityV1 : Staged
    PersistedHostProcessOwnership --> HostProcessIdentity : Committed
    StagedProcessOwnership --> ProcessOwnershipMessageVersion
    StagedProcessOwnership --> HostProcessOwnershipSlot
    StagedProcessOwnership --> PidfsProcessIdentityV1
    ProcessOwnershipCommitAck --> ProcessOwnershipMessageVersion
    ProcessOwnershipCommitAck --> StagedProcessOwnership : committed

boot_id is the canonical 16-byte UUID value from the current Linux boot ID; pidfs_st_dev and pidfs_st_ino are distinct unsigned 64-bit pidfs types. Only their typed PidfsProcessAuthorityV1 tuple is positive durable authority. host_pid is an unsigned 32-bit host-PID-namespace locator and start_time_ticks is unsigned 64-bit negative evidence. Equality of the latter two fields cannot produce or upgrade authority.

Every persisted host-process edge uses PersistedHostProcessOwnership. Unassigned records that no process ownership has been acquired. Staged contains only a current pidfs identity. It is positive authority for exact same-handle cleanup and PidfsRecoveryMatch, including bootstrap recovery, but cannot satisfy readiness, publication, dependent-resource creation, or adoption. Committed may contain a current pidfs identity or a legacy-unverified v1 PID/start-only value. Only committed current pidfs ownership is serviceable; the legacy variant always refuses positive process action. There is no observation-based conversion from legacy data to PidfsProcessIdentityV1.

Participant Intent and ReleaseIntent may retain any process-slot phase. Every active participant state and Session Terminal Running requires current committed ownership in every role-required process slot. Clean requires every process slot to be Unassigned.

StagedProcessOwnership binds one acquisition to its Domain, generation, generation-scoped Session Coordinator ID, exact ownership slot, and complete pidfs identity. ProcessOwnershipCommitAck.committed echoes that exact staged value. The receiver accepts one matching v1 acknowledgment only while that exact launch barrier is pending on the authenticated generation channel. A mismatched, duplicated after release, unknown-version, lost, or unauthenticated acknowledgment cannot release a barrier. These shapes define correlation and the durable boundary; they do not select the authentication or transport codec.

Session terminal ownership

classDiagram
    class SessionTerminal {
        generation_id
        session_id
        state
        container_id
    }

    class PtyIdentity {
        master_fd
        device
        inode
    }

    class GuestProcessIdentity {
        leader_pid
        leader_start_time
        process_group_id
    }

    SessionTerminal --> PersistedHostProcessOwnership : host_coordinator
    SessionTerminal --> PtyIdentity : pty_master
    SessionTerminal --> PersistedHostProcessOwnership : exec_client
    SessionTerminal --> GuestProcessIdentity : guest_process

Host process ownership uses the phase-aware pidfs dispatch above. Guest process identity uses the leader PID and start time observed inside the exact generation-bound runsc container; its process-group ID is relative to that container's root PID namespace and never converts to host process authority. The PTY identity is exact only in combination with its Host Coordinator process identity. SessionTerminalState is a physical recovery state below the application protocol model; it does not redefine session_phase.

SessionTerminal=Intent permits partial acquisition: container_id, pty_master, and guest_process are optional, while both process slots use the three-phase ownership union. Running requires all three non-process identities and current committed HC and exec-client ownership. ReleaseIntent retains all recorded evidence, including staged current pidfs authority. Clean requires all three optional identities absent and both process slots unassigned. A bare Intent may become this explicit cleared snapshot only after runtime absence is proven.

Session terminal protocol

classDiagram
    class SessionTerminalEvent {
        <<union>>
    }

    class TerminalInput {
        session_id
        bytes
    }

    class TerminalResize {
        session_id
        rows
        columns
        pixel_width
        pixel_height
    }

    class TerminalSignal {
        session_id
        signal
    }

    class TerminalSignalKind {
        <<enumeration>>
        Interrupt
        Quit
        Suspend
    }

    class SessionClose {
        session_id
    }

    class SessionTerminalMessage {
        <<union>>
    }

    class TerminalOutput {
        session_id
        bytes
    }

    class SessionExit {
        session_id
        outcome
        exit_code
    }

    class SessionExitOutcome {
        <<enumeration>>
        Exited
        Terminated
        RuntimeLost
    }

    SessionTerminalEvent --> TerminalInput
    SessionTerminalEvent --> TerminalResize
    SessionTerminalEvent --> TerminalSignal
    SessionTerminalEvent --> SessionClose
    TerminalSignal --> TerminalSignalKind
    SessionTerminalMessage --> TerminalOutput
    SessionTerminalMessage --> SessionExit
    SessionExit --> SessionExitOutcome

exit_code is present only for Exited and is the status collected from the non-detached runsc exec client. Interactive PTY output is one merged byte stream; it does not preserve stdout and stderr as separate channels.

Protocol cryptography

classDiagram
    class GenerationIdentity {
        agent_profile
        domain_id
        generation_id
        configuration_fingerprint
        coordinator_id
        sandbox_id
    }

    class TrustManifest {
        protocol_version
        generation_identity
        tls_ca_sha256
        protocol_issuer_public_key
        issuer_signature
    }

    class TlsCredential {
        generation_identity
        role
        subject_id
        certificate_der
        certificate_sha256
    }

    class ProtocolPublicationIdentity {
        protocol_version
        generation_identity
        tls_ca_certificate_der
        trust_manifest_sha256
        coordinator_tls_certificate_sha256
    }

    class SandboxCredentialBundle {
        protocol_version
        generation_identity
        trust_manifest
        coordinator_protocol_credential
        sandbox_protocol_credential
        sandbox_private_key
    }

    class TlsRole {
        <<enumeration>>
        SessionCoordinator
        ClientCLI
    }

    class ProtocolCredential {
        protocol_version
        credential_id
        generation_identity
        role
        subject_id
        public_key
        issuer_signature
    }

    class ProtocolCredentialRole {
        <<enumeration>>
        SessionCoordinator
        AgentSandbox
    }

    class SessionBinding {
        generation_identity
        cli_id
        session_id
        session_anchor_public_key
    }

    class SessionRequestCapability {
        protocol_version
        capability_id
        session_binding
        coordinator_credential_id
        sandbox_credential_id
        coordinator_signature
    }

    TrustManifest --> GenerationIdentity
    TlsCredential --> TlsRole
    TlsCredential --> GenerationIdentity
    ProtocolPublicationIdentity --> GenerationIdentity
    ProtocolPublicationIdentity --> TrustManifest
    ProtocolPublicationIdentity --> TlsCredential : coordinator TLS leaf
    SandboxCredentialBundle --> GenerationIdentity
    SandboxCredentialBundle --> TrustManifest
    SandboxCredentialBundle --> ProtocolCredential : coordinator verifier
    SandboxCredentialBundle --> ProtocolCredential : sandbox signer
    ProtocolCredential --> GenerationIdentity
    ProtocolCredential --> ProtocolCredentialRole
    SessionBinding --> GenerationIdentity
    SessionRequestCapability --> SessionBinding
    SessionRequestCapability --> ProtocolCredential : coordinator signer
    SessionRequestCapability --> ProtocolCredential : sandbox subject

Trust Manifests, credentials, capabilities, and private keys are ephemeral protocol material, not Domain Store records.

classDiagram
    class SignedProtocolRecord {
        <<union>>
        payload_bytes
        signature
    }

    class MailboxRequest {
        protocol_version
        session_binding
        request_id
        session_capability
        sandbox_credential_id
        command_argv
        justification
    }

    class MailboxCancellation {
        protocol_version
        session_binding
        request_id
        session_capability
        sandbox_credential_id
    }

    class MailboxResultAck {
        protocol_version
        session_binding
        request_id
        request_sha256
        session_capability
        sandbox_credential_id
        highest_contiguous_sequence
    }

    class DeveloperDecision {
        protocol_version
        session_binding
        request_id
        request_sha256
        decision
        approved_command_argv
    }

    class DeveloperDecisionKind {
        <<enumeration>>
        Approve
        Deny
    }

    class MailboxResult {
        protocol_version
        session_binding
        request_id
        request_sha256
        coordinator_credential_id
        outcome
        exit_code
        chunk_count
        stdout_sha256
        stderr_sha256
    }

    class MailboxResultChunk {
        protocol_version
        session_binding
        request_id
        request_sha256
        coordinator_credential_id
        sequence
        stream
        bytes
    }

    class ResultStream {
        <<enumeration>>
        Stdout
        Stderr
    }

    class EscalationOutcome {
        <<enumeration>>
        Completed
        Denied
        TimedOut
        Cancelled
        Failed
    }

    SignedProtocolRecord --> MailboxRequest
    SignedProtocolRecord --> MailboxCancellation
    SignedProtocolRecord --> MailboxResultAck
    SignedProtocolRecord --> DeveloperDecision
    SignedProtocolRecord --> MailboxResult
    SignedProtocolRecord --> MailboxResultChunk
    MailboxRequest --> SessionBinding
    MailboxRequest --> SessionRequestCapability
    MailboxCancellation --> SessionBinding
    MailboxCancellation --> SessionRequestCapability
    MailboxResultAck --> SessionBinding
    MailboxResultAck --> SessionRequestCapability
    DeveloperDecision --> SessionBinding
    DeveloperDecision --> DeveloperDecisionKind
    MailboxResult --> SessionBinding
    MailboxResult --> EscalationOutcome
    MailboxResultChunk --> SessionBinding
    MailboxResultChunk --> ResultStream

SessionCoordinator is the logical protocol identity served physically by the Host Coordinator. It is not a distinct Host Coordinator principal. Physical encoding, signing, size, and validation rules are defined by the specification.

Participant ownership

classDiagram
    class ParticipantOwnership {
        <<union>>
    }

    class NamespaceIdentity {
        device
        inode
    }

    class MountIdentity {
        mount_id
        device
        inode
    }

    class PreparedLowerOwnership {
        optional prepared_artifact_id
        optional source_identity
        optional lease_state
        optional path
        optional device
        optional inode
    }

    class StateAnchorOwnership {
        optional lower_artifact_id
        optional upper_path
        optional work_path
        optional merged_path
        daemon
        optional mount
    }

    class GitRoutingOwnership {
        optional mountpoint
        optional host_root
        optional isolated_root
        daemon
        optional mount
    }

    class NetworkOwnership {
        optional derived_policy_digest
        optional readiness_fingerprint
        optional readiness_evidence
        optional namespace
        runtime_supervisor
    }

    class HelperOwnership {
        optional network_sidecar_id
        connectivity_helper
        runtime_supervisor
    }

    class SandboxOwnership {
        optional container_id
        optional runtime_root
        optional bundle_digest
        host_coordinator
        runtime_supervisor
        attached_runsc
    }

    class PublicationOwnership {
        optional socket_path
        optional identity_path
    }

    ParticipantOwnership --> PreparedLowerOwnership
    ParticipantOwnership --> StateAnchorOwnership
    ParticipantOwnership --> GitRoutingOwnership
    ParticipantOwnership --> NetworkOwnership
    ParticipantOwnership --> HelperOwnership
    ParticipantOwnership --> SandboxOwnership
    ParticipantOwnership --> PublicationOwnership
    StateAnchorOwnership --> PersistedHostProcessOwnership : daemon
    StateAnchorOwnership --> MountIdentity : mount
    GitRoutingOwnership --> PersistedHostProcessOwnership : daemon
    GitRoutingOwnership --> MountIdentity : mount
    NetworkOwnership --> NamespaceIdentity : namespace
    NetworkOwnership --> PersistedHostProcessOwnership : runtime_supervisor
    NetworkOwnership --> NetworkReadinessEvidence : readiness_evidence
    HelperOwnership --> PersistedHostProcessOwnership : connectivity_helper
    HelperOwnership --> PersistedHostProcessOwnership : runtime_supervisor
    SandboxOwnership --> PersistedHostProcessOwnership : host_coordinator
    SandboxOwnership --> PersistedHostProcessOwnership : runtime_supervisor
    SandboxOwnership --> PersistedHostProcessOwnership : attached_runsc

Participant.ownership=None is the single encoding of a bare role-bound Intent. Once a role snapshot exists it never returns to None. Intent admits a non-empty partial matching snapshot. ReleaseIntent admits a non-empty partial or complete matching snapshot and retains every exact acquired value. Clean requires a matching explicit empty snapshot: all optional physical fields are absent and all process slots are unassigned. This distinguishes "nothing acquired yet" from "absence was proved and cleanup committed."

The only serviceable role/state pairs are:

Role Active state Required physical evidence
PreparedLower LeaseHeld artifact ID, source, lease_state=LeaseHeld, path, device, inode
StateAnchor Prepared lower artifact, three paths, mount, current committed daemon
GitRouting Prepared three paths, mount, current committed daemon
Network Committed policy digest, fingerprint, readiness evidence, namespace, current committed RS
Helper Committed sidecar ID, current committed helper and RS
Sandbox Committed container, runtime root, bundle digest, current committed HC, RS, and runsc
Publication Committed socket and identity paths

Network readiness evidence must name the enclosing Participant generation and match its policy digest, fingerprint, namespace, and committed RS identity.

Intent acquisition is monotonic: optional fields move only absent-to-present, and newly acquired child slots move only Unassigned to Staged to exact matching current Committed. A current committed owner or supervisor may enter a newly created dependent-role snapshot directly and is then immutable; legacy ownership cannot use this rule. Recorded evidence cannot be replaced or cleared. A transition to ReleaseIntent preserves all evidence; PreparedLower changes only its lease phase to ReleaseIntent. ReleaseIntent becomes Clean only after independent absence proof and atomically clears every physical field. A bare Intent may become Clean only after the same sealed runtime-absence check.

The direct existing-process edges are Network RS, Helper RS, Sandbox HC, and Session Terminal HC. State Anchor daemon, Git-routing daemon, Helper process, Sandbox RS, attached runsc, and Session Terminal exec client are acquired child edges and require staging. No bare Intent moves directly to an active state. PreparedLower must also move from recorded lease_state=Intent to LeaseHeld; it cannot acquire the catalog lease from an absent lease phase.

Storage Catalog

classDiagram
    class StorageCatalog

    class StoreMetadata {
        schema_id
        schema_version
    }

    class PreparedArtifact {
        prepared_artifact_id
        source_identity
        selected_manifest_digest
        policy_id
        id_map_digest
        path
        device
        inode
        state
    }

    class ArtifactState {
        <<enumeration>>
        Available
        Reclaiming
    }

    class ArtifactLease {
        prepared_artifact_id
        domain_id
        generation_id
    }

    class PrepareOperation {
        operation_id
        expected_artifact_id
        source_identity
        stage_path
        device
        inode
    }

    StorageCatalog "1" *-- "1" StoreMetadata
    StorageCatalog "1" *-- "0..*" PreparedArtifact
    StorageCatalog "1" *-- "0..*" ArtifactLease
    StorageCatalog "1" *-- "0..*" PrepareOperation
    PreparedArtifact --> ArtifactState
    PreparedArtifact "1" --> "0..*" ArtifactLease
    PrepareOperation "0..*" --> "0..1" PreparedArtifact : publishes

An artifact lease identifies one prepared artifact, Domain, and generation. Its Domain and generation fields correspond to a Domain Store generation; the stores do not form one transactional database.

ReleaseIntent is a physical recovery state. It does not redefine the modeled lease lifecycle: the specification defines how Domain Store release intent, physical unmount, and Storage Catalog lease removal refine model unmount and release actions. After Host Coordinator loss, the exact Runtime Supervisor may unmount under the retained Prepared ownership record, but the Artifact Lease remains until bootstrap Host Coordinator recovery records release intent.

Derived network policy

classDiagram
    class DerivedNetworkPolicy {
        schema_id
        address_families
        default_actions
        prefix_rules
        resolvers
    }

    class AddressFamilyDisposition {
        family
        disposition
    }

    class DefaultAction {
        family
        action
    }

    class PrefixRule {
        family
        network
        action
    }

    class Resolver {
        family
        address
    }

    class AddressFamily {
        <<enumeration>>
        IPv4
        IPv6
    }

    class PolicyAction {
        <<enumeration>>
        Allow
        Deny
    }

    DerivedNetworkPolicy "1" *-- "2" AddressFamilyDisposition
    DerivedNetworkPolicy "1" *-- "1..2" DefaultAction
    DerivedNetworkPolicy "1" *-- "0..*" PrefixRule
    DerivedNetworkPolicy "1" *-- "1..*" Resolver
    AddressFamilyDisposition --> AddressFamily
    DefaultAction --> AddressFamily
    DefaultAction --> PolicyAction
    PrefixRule --> AddressFamily
    PrefixRule --> PolicyAction
    Resolver --> AddressFamily

The policy has exactly one disposition for each address family. Each enabled family has exactly one default action. Prefixes and addresses are canonical, and equal-prefix action conflicts are invalid. Policy identity is the SHA-256 digest of its canonical form with a schema-specific domain separator.

Network readiness

classDiagram
    class NetworkReadinessEvidence {
        domain_id
        generation_id
        network_sidecar_id
        readiness_fingerprint
    }

    class NetworkReadinessAttestation {
        schema_id
        domain_id
        generation_id
        configuration_fingerprint
        network_sidecar_id
        derived_policy_digest
        enforcement_namespace
        runtime_supervisor
        connectivity_helper
        observation
    }

    class NetworkObservation {
        policy_delivery
        helper_ready
        observed_routes
        routes_attested
    }

    class PolicyDelivery {
        <<enumeration>>
        SealedMemfd
    }

    class NetworkReadinessIdentity {
        schema_id
        domain_id
        generation_id
        configuration_fingerprint
        network_sidecar_id
        derived_policy_digest
        enforcement_namespace
    }

    class NamespaceIdentity {
        device
        inode
    }

    class NetworkReachabilityProbe {
        domain_id
        generation_id
        enforcement_namespace
    }

    class DeniedDestinationObservation {
        destination
        unreachable
    }

    class PermittedDestinationObservation {
        destination
        reachable
    }

    class DnsObservation {
        resolver
        query_name
        resolved_addresses
    }

    NetworkReadinessEvidence --> NetworkReadinessAttestation : accepts
    NetworkReadinessEvidence --> NetworkReadinessIdentity : fingerprints
    NetworkReadinessEvidence --> NetworkReachabilityProbe : includes
    NetworkReadinessAttestation --> NamespaceIdentity : enforcement_namespace
    NetworkReadinessAttestation --> PidfsProcessIdentityV1 : runtime_supervisor
    NetworkReadinessAttestation --> PidfsProcessIdentityV1 : connectivity_helper
    NetworkReadinessAttestation --> NetworkObservation
    NetworkObservation --> PolicyDelivery
    NetworkReadinessIdentity --> NamespaceIdentity : enforcement_namespace
    NetworkReadinessAttestation ..> NetworkReadinessIdentity : projects
    NetworkReachabilityProbe --> NamespaceIdentity : enforcement_namespace
    NetworkReachabilityProbe "1" *-- "1..*" DeniedDestinationObservation
    NetworkReachabilityProbe "1" *-- "1..*" PermittedDestinationObservation
    NetworkReachabilityProbe "1" *-- "1..*" DnsObservation

The readiness fingerprint is the SHA-256 digest of the canonical readiness identity with a schema-specific domain separator. Process identities and network observations are attestation evidence but are not part of that fingerprint. The Host Coordinator binds the accepted attestation and generation-bound reachability probe into Network Readiness Evidence before publication.

NetworkReadinessAttestation.schema_id is exactly lazaret-network-attestation-v1. NetworkReadinessIdentity.schema_id is exactly lazaret-network-readiness-identity-v1. They are independent version-dispatch domains: the v1 attestation projects only to the v1 readiness identity, and any other identifier or version is refused without fallback or cross-domain interpretation.

The readiness-fingerprint domain separator is the UTF-8 bytes of lazaret-network-readiness-identity-v1 followed by exactly one zero byte. This fixes the domain-separation context only; it does not select the byte encoding of the canonical readiness-identity fields.