lazaret/docs/schema.md

48 KiB

Lazaret schemas

This document is a mutable coordination schema for Lazaret records and protocol data shapes. The specification defines their authoritative physical use, while the TLA+ models define authoritative lifecycle and ordering above the implementation abstraction. The diagrams coordinate logical fields, variants, relationships, and serialization boundaries across implementation work; they do not select a programming-language representation or override the specification or models.

Every class carries exactly one serialization-category stereotype: <<serialized>> identifies a record that may cross a configuration, durable-storage, or protocol codec boundary, while <<runtime>> identifies an in-memory authority or plan that must not implement Serde serialization or deserialization. Shape stereotypes such as <<enumeration>> and <<union>> are orthogonal and may coexist.

Trusted host configuration

The trusted-host configuration dispatch identifier is exactly lazaret-trusted-host-config-v1. The v1 TOML document has the following closed shape; every table recursively rejects unknown fields, duplicate keys, duplicate tables, field aliases, and unknown enumeration values.

Table Field Presence and value
root schema required; exact dispatch identifier
root storage optional; defaults to an empty table
root network optional; defaults to an empty table
root profiles required; at least one dynamically named profile
storage data_root optional host path; defaults beneath the XDG data home
network default_allow optional list of IPv4 prefixes; defaults empty
network default_deny optional list of IPv4 prefixes; defaults empty
profile image required nonempty OCI image reference
profile isolate_gitignored optional Boolean; defaults true
profile persistent_dirs optional list of sandbox paths; defaults empty
profile allow_egress optional list of IPv4 prefixes; defaults empty
profile deny_egress optional list of IPv4 prefixes; defaults empty
profile readiness required at load for the selected profile; closed denied, permitted, and DNS target table
profile env optional map from sandbox environment name to env: reference; defaults empty
profile volumes optional list of volume tables; defaults empty
profile escalation optional table; defaults to disabled with no rules
readiness denied one to 32 unique { address = IPv4, port = nonzero } targets denied by effective policy
readiness permitted one to 32 unique { address = IPv4, port = nonzero } targets allowed by effective policy
readiness dns one to 32 unique { resolver = IPv4, query = strict DNS name } targets
volume host required host path
volume container required sandbox path
volume kind required: file or directory
volume access optional: read-only or read-write; defaults read-write
volume create optional Boolean; defaults false
escalation enabled optional Boolean; defaults false
escalation auto_approve optional list of auto-approval rules; defaults empty
auto-approval rule executable required host executable path
auto-approval rule args optional ordered list of arguments; defaults empty
auto-approval rule allow_extra_args optional Boolean; defaults false

Profile names are ASCII and match [A-Za-z0-9][A-Za-z0-9._-]{0,63}, excluding . and ... The caller selects one profile by its exact case-sensitive name; the document has no default profile and declaration order never selects one. All configuration strings are UTF-8 and NUL-free. Environment map keys and the names following env: match [A-Za-z_][A-Za-z0-9_]*. An environment value is a reference only; literal values and other reference schemes are invalid.

Host path declarations are absolute or begin with exactly ~/; only a volume host may instead begin with exactly {profile_dir}/. A declaration rejects an empty suffix, an empty component, repeated or trailing separators, . or .., and every other expansion, variable, glob, or shell form. In particular, ~//etc and {profile_dir}//etc are invalid. Sandbox paths are normalized absolute UTF-8 paths with the same component restrictions; the root path is invalid.

Persistent paths are pairwise distinct and neither equal nor ancestral in either direction. Volume container paths are pairwise distinct and non-ancestral. Each volume container path is a strict descendant of exactly one persistent path; equality with that path is invalid. Resolution requires an internal SandboxLayoutV1, whose closed protected-destination set contains /workspace and every control, credential, mailbox, proc, sys, dev, runtime, and other product-owned sandbox mount destination selected by the production layout. Persistent and volume paths must not intersect any protected destination in either ancestry direction. Configuration semantics do not assign destinations other than the specification-fixed /workspace; the production sandbox-layout constructor owns that complete set and cannot construct a layout that omits /workspace. A volume source has the declared kind. create=true is valid only for a directory source anchored strictly beneath {profile_dir}; a file source is never created. Symlinks, magic links, mount crossings, sockets, devices, FIFOs, and other special files cannot satisfy a source.

The v1 network language is IPv4-only. Each prefix is parsed, has host bits cleared, and is deduplicated. Effective allow prefixes are the union of global and selected-profile allows. Effective deny prefixes are the union of global, selected-profile, and the three RFC 1918 prefixes. An exact normalized prefix present in both sets is invalid; overlaps at different prefix lengths are valid and use longest-prefix routing. The Agent Sandbox disables IPv6 for v1.

An auto-approval rule is valid only while its profile escalation table is enabled. Its executable resolves to a canonical regular executable outside the Agent Workspace, every configured volume source, the profile directory, and every Lazaret data, runtime, Domain, and control tree. At load and before every execution, the object must have exactly one hard link, be owned by root or the invoking user, grant no group or other write permission, and be executable by the invoking user. Rule arguments are UTF-8, NUL-free strings. allow_extra_args=false matches only the exact ordered argument list. allow_extra_args=true requires at least one configured argument and matches that nonempty ordered prefix followed by zero or more arguments. Exact duplicate normalized rules are invalid.

MailboxRequest.command_argv is nonempty and every element is UTF-8 and NUL-free. Element zero is an absolute path and must be byte-for-byte equal to the rule's canonical executable path; elements one onward are the arguments matched against args. Lazaret performs no PATH lookup. The structured v1 request language has no command-string or shell-expansion form; such an unsupported input is CommandRequiresShell and cannot auto-approve. The host launcher never inserts or interprets a shell. An explicitly configured exact vector such as /bin/sh, -c, and a fixed command string remains a direct vector and can match only that exact rule. A configured script remains eligible: executing the same opened script object uses ordinary kernel shebang handling without turning its arguments into a Lazaret-interpreted shell command.

classDiagram
    class TrustedHostConfigurationV1 {
        <<serialized>>
        schema_id
        profiles
    }

    class ResolvedDomainConfiguration {
        <<runtime>>
        selected_profile
        domain_id
        configuration_fingerprint
        effective_network_policy
        ephemeral_environment
        confined_authorities
    }

    class SandboxLayoutV1 {
        <<runtime>>
        protected_destinations
    }

    class ConfigurationFingerprintRecordV1 {
        <<serialized>>
        selected_profile
        storage_root
        runtime_root
        image
        isolate_gitignored
        persistent_dirs
        environment_references
        volumes
        effective_allow_prefixes
        effective_deny_prefixes
        network_readiness
        escalation_enabled
        auto_approval_rules
    }

    class EnvironmentReference {
        <<serialized>>
        sandbox_name
        host_name
    }

    class ResolvedVolume {
        <<serialized>>
        canonical_host_source
        container_path
        kind
        access
        create
    }

    class ResolvedAutoApprovalRule {
        <<serialized>>
        canonical_executable
        args
        allow_extra_args
    }

    class ResolvedNetworkReadinessV1 {
        <<serialized>>
        denied
        permitted
        dns
    }

    class ConfinedPathPlan {
        <<runtime>>
        retained_ancestor
        ancestor_device
        ancestor_inode
        ancestor_mount_id
        relative_suffix
        canonical_label
    }

    TrustedHostConfigurationV1 --> ResolvedDomainConfiguration : resolves selected profile
    SandboxLayoutV1 --> ResolvedDomainConfiguration : constrains sandbox destinations
    ResolvedDomainConfiguration --> ConfigurationFingerprintRecordV1
    ConfigurationFingerprintRecordV1 --> EnvironmentReference
    ConfigurationFingerprintRecordV1 --> ResolvedVolume
    ConfigurationFingerprintRecordV1 --> ResolvedAutoApprovalRule
    ConfigurationFingerprintRecordV1 --> ResolvedNetworkReadinessV1
    ResolvedDomainConfiguration --> ConfinedPathPlan

ResolvedDomainConfiguration is one move-owned, non-serializable value. It is the only successful output of loading a selected profile and owns all retained path descriptors, normalized selected-profile data, confined use plans, and resolved environment values. Resolved environment values have no field in ConfigurationFingerprintRecordV1; they are never serialized, hashed, logged, or persisted. The parsed source and nonselected profiles do not become runtime configuration authority.

SandboxLayoutV1 is an internal, non-serializable product value with no public unchecked constructor. Configuration resolution cannot succeed without it. Its production constructor is owned by sandbox-layout construction and supplies the complete normalized protected-destination set; configuration loading neither guesses unnamed destinations nor permits a caller to remove /workspace. Every member is a normalized, absolute, non-root sandbox path using the sandbox-path grammar above; the finite set is nonempty and contains no duplicate member.

ConfinedPathPlan owns a retained ancestor directory descriptor and exposes to consumers only its typed device, inode, and mount identity plus a normalized relative suffix. canonical_label is diagnostic or fingerprint input, not open authority. Consumers reopen relative to the retained descriptor and revalidate the intended type and identity constraints. They do not receive an ambient absolute path with which to repeat authorization.

Domain and configuration digests

DomainIdV1 is the 32-byte SHA-256 digest of the following preimage:

"lazaret-domain-id-v1\0"
|| bytes(selected_profile_utf8)
|| bytes(canonical_workspace_path)

ConfigurationFingerprintV1 is the 32-byte SHA-256 digest of the following canonical record:

"lazaret-configuration-fingerprint-v1\0"
|| bytes(selected_profile_utf8)
|| bytes(canonical_storage_root)
|| bytes(canonical_runtime_root)
|| bytes(image_utf8)
|| boolean(isolate_gitignored)
|| list(persistent_dirs)
|| list(environment_reference_records)
|| list(volume_records)
|| list(effective_allow_prefixes)
|| list(effective_deny_prefixes)
|| boolean(escalation_enabled)
|| list(auto_approval_rule_records)

The Domain-ID separator is the exact 20 ASCII bytes lazaret-domain-id-v1 followed by one NUL byte. Both variable fields use bytes; concatenating their unframed bytes is not a valid v1 preimage. bytes(x) is u32be(length(x)) || x; lengths and list counts must fit in u32. A Boolean is one byte, 0x00 or 0x01. Each list is u32be(item_count) followed by bytes(item_encoding) for every item. Every listed collection is order-independent and sorts complete item encodings by unsigned bytewise order before framing, except that arguments inside one auto-approval record retain their declared order. Strings use UTF-8 bytes and canonical host paths use raw Linux pathname bytes.

An environment-reference record is bytes(sandbox_name) || bytes(host_name). A persistent-path item is bytes(path), and an argument item is bytes(arg). A volume record is bytes(canonical_host_source) || bytes(container_path) || kind || access || boolean(create), where file/directory are 0x00/0x01 and read-only/read-write are 0x00/0x01. An IPv4 prefix is its four network-order address bytes followed by its one-byte prefix length. An auto-approval record is bytes(canonical_executable) || list_in_declared_order(args) || boolean(allow_extra_args). A declared-order argument list uses the same list framing without sorting.

Source comments, TOML declaration order, defaults versus their explicit equal values, nonselected profiles, configuration source pathname, host environment values, retained descriptor numbers, filesystem inode metadata, and executable contents are excluded. Every resolved selected-profile field above is included. Neither digest is an authentication credential.

Domain Store

classDiagram
    class DomainStore {
        <<serialized>>
    }

    class StoreMetadata {
        <<serialized>>
        schema_id
        schema_version
    }

    class Domain {
        <<serialized>>
        domain_id
        profile_name
        canonical_workspace
    }

    class Generation {
        <<serialized>>
        domain_id
        generation_id
        configuration_fingerprint
        coordinator_id
        network_sidecar_id
        sandbox_id
        durable_decision
    }

    class Participant {
        <<serialized>>
        generation_id
        role
        state
        ownership
    }

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

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

    class Session {
        <<serialized>>
        generation_id
        session_id
        cli_id
        session_anchor_id
        session_phase
    }

    class SessionTerminal {
        <<serialized>>
        generation_id
        session_id
        state
        container_id
    }

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

    class Mailbox {
        <<serialized>>
        generation_id
        identity_path
        inbox_path
        outbox_path
        state
    }

    class StateResetIntentV1 {
        <<serialized>>
        schema
        domain_id
        scope
    }

    class StateResetScopeV1 {
        <<serialized>>
        <<union>>
        All
        Selected
    }

    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
    DomainStore "1" *-- "0..1" StateResetIntentV1
    StateResetIntentV1 --> StateResetScopeV1

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-v1 with version 1. Unknown, unversioned, older, and newer metadata refuses before any row is interpreted. The Storage Catalog remains lazaret-storage-catalog-v1 with version 1.

The reset-intent schema is exactly lazaret-state-reset-v1. Selected contains one exact configured persistent container path as pathname bytes. The singleton intent is committed before physical quarantine begins and removed only after confined cleanup completes. Unknown schemas and mismatched Domain or scope bindings refuse before physical state is interpreted.

State maintenance output

Machine output uses schema lazaret-state-output-v1. It contains the operation, Domain ID, profile, hexadecimal canonical-workspace bytes, a list of configured State Anchors, structural change metadata, and contents_included = false. Each change contains hexadecimal pathname bytes, entry kind, mode, owner UID and GID, and size. It never contains persisted file contents.

Host process identity and staged ownership

classDiagram
    class PidfsProcessAuthorityV1 {
        <<serialized>>
        boot_id
        pidfs_st_dev
        pidfs_st_ino
    }

    class PidfsProcessIdentityV1 {
        <<serialized>>
        host_pid
        start_time_ticks
    }

    class HostProcessIdentity {
        <<serialized>>
        identity
    }

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

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

    class ProcessOwnershipMessageVersion {
        <<serialized>>
        <<enumeration>>
        V1
    }

    class StagedProcessOwnership {
        <<serialized>>
        version
        domain_id
        generation_id
        coordinator_id
        slot
        identity
    }

    class ProcessOwnershipCommitAck {
        <<serialized>>
        version
        committed
    }

    PidfsProcessIdentityV1 --> PidfsProcessAuthorityV1 : authority
    HostProcessIdentity --> PidfsProcessIdentityV1 : current authority
    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 contains the current pidfs identity and is the only serviceable process-ownership phase.

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 {
        <<serialized>>
        generation_id
        session_id
        state
        container_id
    }

    class PtyIdentity {
        <<serialized>>
        master_fd
        device
        inode
    }

    class GuestProcessIdentity {
        <<serialized>>
        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 {
        <<serialized>>
        <<union>>
    }

    class TerminalInput {
        <<serialized>>
        session_id
        bytes
    }

    class TerminalResize {
        <<serialized>>
        session_id
        rows
        columns
        pixel_width
        pixel_height
    }

    class TerminalSignal {
        <<serialized>>
        session_id
        signal
    }

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

    class SessionClose {
        <<serialized>>
        session_id
    }

    class SessionTerminalMessage {
        <<serialized>>
        <<union>>
    }

    class TerminalOutput {
        <<serialized>>
        session_id
        bytes
    }

    class SessionExit {
        <<serialized>>
        session_id
        outcome
        exit_code
    }

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

    class NonInteractiveSessionRequest {
        <<serialized>>
        protocol_version
        session_id
        argv
    }

    class NonInteractiveOutput {
        <<serialized>>
        session_id
        bytes
    }

    class NonInteractiveSessionMessage {
        <<serialized>>
        <<union>>
        Stdout
        Stderr
        Exit
    }

    class InteractiveSessionRequest {
        <<serialized>>
        protocol_version
        session_id
        argv
        initial_size
    }

    class InteractiveSessionEvent {
        <<serialized>>
        <<union>>
        Terminal
        PromptDecision
        Poll
    }

    class TerminalPrompt {
        <<serialized>>
        session_id
        request_id
        request_sha256
        agent_profile
        command_argv
        justification
    }

    class InteractiveSessionMessage {
        <<serialized>>
        <<union>>
        Terminal
        Prompt
    }

    class InteractiveSessionBatch {
        <<serialized>>
        session_id
        messages
    }

    SessionTerminalEvent --> TerminalInput
    SessionTerminalEvent --> TerminalResize
    SessionTerminalEvent --> TerminalSignal
    SessionTerminalEvent --> SessionClose
    TerminalSignal --> TerminalSignalKind
    SessionTerminalMessage --> TerminalOutput
    SessionTerminalMessage --> SessionExit
    SessionExit --> SessionExitOutcome
    NonInteractiveSessionMessage --> NonInteractiveOutput : Stdout / Stderr
    NonInteractiveSessionMessage --> SessionExit : Exit
    InteractiveSessionRequest --> TerminalResize : initial_size
    InteractiveSessionEvent --> SessionTerminalEvent : Terminal
    InteractiveSessionMessage --> SessionTerminalMessage : Terminal
    InteractiveSessionMessage --> TerminalPrompt : Prompt
    InteractiveSessionBatch --> InteractiveSessionMessage

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 {
        <<serialized>>
        agent_profile
        domain_id
        generation_id
        configuration_fingerprint
        coordinator_id
        sandbox_id
    }

    class TrustManifest {
        <<serialized>>
        protocol_version
        generation_identity
        tls_ca_sha256
        protocol_issuer_public_key
        issuer_signature
    }

    class TlsCredential {
        <<serialized>>
        generation_identity
        role
        subject_id
        certificate_der
        certificate_sha256
    }

    class ProtocolPublicationIdentity {
        <<serialized>>
        protocol_version
        generation_identity
        tls_ca_certificate_der
        trust_manifest_sha256
        coordinator_tls_certificate_sha256
    }

    class ClientEnrollmentRequest {
        <<serialized>>
        protocol_version
        generation_identity
        cli_id
        certificate_signing_request_der
    }

    class ClientEnrollmentResponse {
        <<serialized>>
        protocol_version
        credential
    }

    class SessionRegistrationRequest {
        <<serialized>>
        protocol_version
        session_binding
    }

    class SessionRegistrationResponse {
        <<serialized>>
        protocol_version
        session_capability
    }

    class ControlRequest {
        <<serialized>>
        <<union>>
        Enroll
        Register
        Execute
        ExecuteInteractive
    }

    class ControlResponse {
        <<serialized>>
        <<union>>
        Enrolled
        Registered
    }

    class SandboxCredentialBundle {
        <<serialized>>
        protocol_version
        generation_identity
        trust_manifest
        coordinator_protocol_credential
        sandbox_protocol_credential
        sandbox_private_key
    }

    class TlsRole {
        <<serialized>>
        <<enumeration>>
        SessionCoordinator
        ClientCLI
    }

    class ProtocolCredential {
        <<serialized>>
        protocol_version
        credential_id
        generation_identity
        role
        subject_id
        public_key
        issuer_signature
    }

    class ProtocolCredentialRole {
        <<serialized>>
        <<enumeration>>
        SessionCoordinator
        AgentSandbox
    }

    class SessionBinding {
        <<serialized>>
        generation_identity
        cli_id
        session_id
        session_anchor_public_key
    }

    class SessionRequestCapability {
        <<serialized>>
        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
    ClientEnrollmentRequest --> GenerationIdentity
    ClientEnrollmentResponse --> TlsCredential
    SessionRegistrationRequest --> SessionBinding
    SessionRegistrationResponse --> SessionRequestCapability
    ControlRequest --> ClientEnrollmentRequest : Enroll
    ControlRequest --> SessionRegistrationRequest : Register
    ControlRequest --> NonInteractiveSessionRequest : Execute
    ControlResponse --> ClientEnrollmentResponse : Enrolled
    ControlResponse --> SessionRegistrationResponse : Registered
    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 {
        <<serialized>>
        <<union>>
        payload_bytes
        signature
    }

    class MailboxRequest {
        <<serialized>>
        protocol_version
        session_binding
        request_id
        session_capability
        sandbox_credential_id
        command_argv
        justification
    }

    class MailboxCancellation {
        <<serialized>>
        protocol_version
        session_binding
        request_id
        session_capability
        sandbox_credential_id
    }

    class MailboxResultAck {
        <<serialized>>
        protocol_version
        session_binding
        request_id
        request_sha256
        session_capability
        sandbox_credential_id
        highest_contiguous_sequence
    }

    class DeveloperDecision {
        <<serialized>>
        protocol_version
        session_binding
        request_id
        request_sha256
        decision
        approved_command_argv
    }

    class DeveloperDecisionKind {
        <<serialized>>
        <<enumeration>>
        Approve
        Deny
    }

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

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

    class ResultStream {
        <<serialized>>
        <<enumeration>>
        Stdout
        Stderr
    }

    class EscalationOutcome {
        <<serialized>>
        <<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.

For embedded signed claims, the field order shown above is the Postcard field order. A protocol credential ID is SHA-256 over the minimally encoded claim fields excluding credential_id and issuer_signature. A Session Request Capability ID is SHA-256 over the minimally encoded claim fields excluding capability_id and coordinator_signature. A Trust Manifest digest is SHA-256 over the complete minimally encoded manifest, including its issuer signature. The manifest issuer signs its minimally encoded fields excluding issuer_signature.

Every signature input is the ASCII lazaret/v1/ prefix plus the lower-kebab record name, followed by the unsigned 64-bit big-endian payload length and the exact payload bytes. Mailbox signatures use the bytes carried in SignedProtocolRecord; verification occurs before decoding those payload bytes. Result stream digests are SHA-256 over the ordered concatenation of bytes for that stream.

Participant ownership

classDiagram
    class ParticipantOwnership {
        <<serialized>>
        <<union>>
    }

    class NamespaceIdentity {
        <<serialized>>
        device
        inode
    }

    class MountIdentity {
        <<serialized>>
        mount_id
        device
        inode
    }

    class PreparedLowerOwnership {
        <<serialized>>
        optional prepared_artifact_id
        optional source_identity
        optional lease_state
        optional path
        optional device
        optional inode
        optional released_state_anchor
    }

    class StateAnchorReleaseEvidence {
        <<serialized>>
        daemon
        user_namespace
        mount_namespace
        mount
    }

    class StateAnchorOwnership {
        <<serialized>>
        optional lower_artifact_id
        optional upper_path
        optional work_path
        optional merged_path
        daemon
        optional mount
    }

    class GitRoutingOwnership {
        <<serialized>>
        optional mountpoint
        optional host_root
        optional isolated_root
        daemon
        optional mount
    }

    class NetworkOwnership {
        <<serialized>>
        optional derived_policy_digest
        optional readiness_fingerprint
        optional readiness_evidence
        optional namespace
        runtime_supervisor
    }

    class HelperOwnership {
        <<serialized>>
        optional network_sidecar_id
        connectivity_helper
        runtime_supervisor
    }

    class SandboxOwnership {
        <<serialized>>
        optional container_id
        optional runtime_root
        optional runtime_root_identity
        optional bundle_digest
        host_coordinator
        runtime_supervisor
        attached_runsc
    }

    class PublicationOwnership {
        <<serialized>>
        optional staged_socket_path
        optional staged_identity_path
        optional socket_path
        optional identity_path
        optional socket_identity
        optional identity_file_identity
    }

    ParticipantOwnership --> PreparedLowerOwnership
    PreparedLowerOwnership --> StateAnchorReleaseEvidence : released_state_anchor
    StateAnchorReleaseEvidence --> PersistedHostProcessOwnership : daemon
    StateAnchorReleaseEvidence --> NamespaceIdentity : user_namespace
    StateAnchorReleaseEvidence --> NamespaceIdentity : mount_namespace
    StateAnchorReleaseEvidence --> MountIdentity : mount
    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 and exact mount/device/inode identity, bundle digest, current committed HC, RS, and runsc
Publication Committed staged and exposed fixed labels plus exact socket and identity-file mount/device/inode identities

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. Recorded evidence cannot be replaced or cleared. A transition to ReleaseIntent preserves all evidence. PreparedLower changes its lease phase to ReleaseIntent and may atomically add the exact released State Anchor daemon, user namespace, mount namespace, and mount evidence needed to resume physical release. That release evidence is immutable within 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 {
        <<serialized>>
    }

    class StoreMetadata {
        <<serialized>>
        schema_id
        schema_version
    }

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

    class ArtifactState {
        <<serialized>>
        <<enumeration>>
        Available
        Reclaiming
    }

    class ArtifactLease {
        <<serialized>>
        prepared_artifact_id
        domain_id
        generation_id
    }

    class PrepareOperation {
        <<serialized>>
        operation_id
        expected_artifact_id
        source_identity
        selected_manifest_digest
        policy_id
        id_map_digest
        stage_path
        stage_state
        device
        inode
    }

    class PrepareStageState {
        <<serialized>>
        <<enumeration>>
        Intent
        Owned
    }

    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
    PrepareOperation --> PrepareStageState

A prepare operation is durably recorded with stage_state=Intent and absent device/inode evidence before its stage is created. The exact same operation then moves to Owned with both values present. Only an owned operation can publish or authorize exact abandoned-stage recovery.

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 {
        <<serialized>>
        schema_id
        address_families
        default_actions
        prefix_rules
        resolvers
    }

    class AddressFamilyDisposition {
        <<serialized>>
        family
        disposition
    }

    class DefaultAction {
        <<serialized>>
        family
        action
    }

    class PrefixRule {
        <<serialized>>
        family
        network
        action
    }

    class Resolver {
        <<serialized>>
        family
        address
    }

    class AddressFamily {
        <<serialized>>
        <<enumeration>>
        IPv4
        IPv6
    }

    class PolicyAction {
        <<serialized>>
        <<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.

DerivedNetworkPolicy.schema_id is exactly lazaret-derived-network-policy-v1. Its canonical form is the minimal Postcard encoding of the complete document after address-family dispositions, default actions, prefix rules, and resolvers are sorted respectively by family; family; (family, address bytes, prefix length, action); and (family, address bytes). Duplicate prefixes and resolvers are invalid rather than silently removed. The policy-digest domain separator is the UTF-8 schema ID followed by exactly one zero byte.

Network readiness

classDiagram
    class NetworkReadinessEvidence {
        <<serialized>>
        domain_id
        generation_id
        network_sidecar_id
        readiness_fingerprint
    }

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

    class NetworkObservation {
        <<serialized>>
        policy_delivery
        helper_ready
        observed_routes
        routes_attested
    }

    class PolicyDelivery {
        <<serialized>>
        <<enumeration>>
        SealedMemfd
    }

    class NetworkReadinessIdentity {
        <<serialized>>
        schema_id
        domain_id
        generation_id
        configuration_fingerprint
        network_sidecar_id
        derived_policy_digest
        enforcement_namespace
    }

    class NamespaceIdentity {
        <<serialized>>
        device
        inode
    }

    class NetworkReachabilityProbe {
        <<serialized>>
        domain_id
        generation_id
        enforcement_namespace
    }

    class DeniedDestinationObservation {
        <<serialized>>
        destination
        unreachable
    }

    class PermittedDestinationObservation {
        <<serialized>>
        destination
        reachable
    }

    class DnsObservation {
        <<serialized>>
        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 prefixes the minimal Postcard encoding of the complete NetworkReadinessIdentity document. The schema ID is therefore present in both the domain-separation context and the encoded version-dispatched document.