Core Infrastructure

Base classes, serde contexts, protocol enums, and type aliases that underpin the entire serde system.


Base Classes

WireModel

The abstract Pydantic base class that auto-generates to_writer() and from_reader() from type annotations.

Declarative binary serialization for Nix daemon protocol types.

Pydantic models inherit from WireModel to auto-generate to_writer()/from_reader() from type annotations.

Class variables (ClassVar[int]) are skipped — they're protocol constants like op codes that are written/read by the operation layer, not the message body.

nix_daemon_protocol.wire_message.WireField(default=PydanticUndefined, *, default_factory=None, min_version=None, max_version=None, serialize=None, deserialize=None, wire_depends_on=None, needs_features=None, unless_features=None, text_errors='strict', **kwargs)[source]

A Pydantic Field with Nix protocol version and feature requirements.

Usage:

times_built: int = WireField(default=0, min_version=proto(1, 29))
legacy_field: str = WireField(default="", max_version=proto(1, 27))

The version number stopped at 1.38, and a new capability is a feature. worker-protocol.hh:105 of Nix says so. Both sides send a set of names in the handshake, and the negotiated set is the intersection, so a version number alone no longer says what shape a field has.

Nix writes that choice as an if/else over the negotiated set, and these two arguments are its two halves. worker-protocol.cc:268 is the example: BuildResult.builtOutputs is a map of UnkeyedRealisation when realisation-with-path-not-hash is on, and a map of JSON strings when it is off:

built_outputs_new: ... = WireField(needs_features=[FEATURE_REALISATION_WITH_PATH])
built_outputs_old: ... = WireField(unless_features=[FEATURE_REALISATION_WITH_PATH])

needs_features keeps the field when the negotiated set holds every name. unless_features keeps it when the set holds none of them. A field with neither is there whatever the peers agreed, which is every field of the Nix 2.34 shape. Issue #14.

text_errors is the error handler for the bytes of a str field, and it is "strict" unless the field says otherwise. "surrogateescape" decodes every byte and re-encodes it back to the same byte, which is what a field takes when Nix treats it as opaque bytes that it never validates: log and error text. A builder prints whatever it prints, and the daemon forwards it without looking, so strict decoding fails the whole build on a gzip stream in the log. Issue nixidae/nixidae#662. Anything but "strict" or "surrogateescape" is refused here: a lossy handler would round-trip the bytes into something else, silently.

Parameters:
  • default (Any)

  • default_factory (Any)

  • min_version (int | None)

  • max_version (int | None)

  • serialize (bool | None)

  • deserialize (bool | None)

  • wire_depends_on (Callable | None)

  • needs_features (Iterable[str] | None)

  • unless_features (Iterable[str] | None)

  • text_errors (str)

  • kwargs (Any)

Return type:

Any

class nix_daemon_protocol.wire_message.WireModel[source]

Pydantic base class with auto-generated Nix daemon protocol serde.

Usage:
class FooResponse(WireModel):

valid: int # uint64 on the wire path: str # length-prefixed UTF-8

model_config = {'arbitrary_types_allowed': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

async to_writer(ctx)[source]

Write all non-ClassVar fields in declaration order.

Parameters:

ctx (WriteContext)

Return type:

None

async classmethod from_reader(ctx)[source]

Read all non-ClassVar fields in declaration order.

Parameters:

ctx (ReadContext)

to_json(**kwargs)[source]

Serialize to JSON string.

Uses Pydantic's model_dump_json.

Return type:

str

classmethod from_json(json_data, **kwargs)[source]

Deserialize from JSON string.

Uses Pydantic's model_validate_json.

Parameters:

json_data (str | bytes)

Return type:

Self

WireString

Abstract base for types that are a single length-prefixed UTF-8 string on the wire.

Abstract base: a single length-prefixed string on the wire.

class nix_daemon_protocol.wire_string.WireString[source]

Abstract base: a single length-prefixed string on the wire.

Subclasses define their own fields. JSON is native Pydantic (the destructed object). Wire format is str(self).

to_writer writes str(self) as a single wire string. __hash__ / __eq__ delegate to str(self).

Override from_str / to_str in subclasses that need custom wire format (e.g. WireSignature splits on :).

model_config = {'arbitrary_types_allowed': True, 'frozen': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

async to_writer(ctx)[source]

Write all non-ClassVar fields in declaration order.

Parameters:

ctx (WriteContext)

Return type:

None

async classmethod from_reader(ctx)[source]

Read all non-ClassVar fields in declaration order.

Parameters:

ctx (ReadContext)

to_str()[source]

Override in subclasses to customize wire output. Default: single-field value.

Return type:

str

classmethod from_str(data)[source]

Default: map string to the single field.

Parameters:

data (str)

Return type:

object

WireRequest / WireResponse

Operation base classes. WireRequest auto-registers each op in WIRE_REGISTRY.

WireRequest / WireResponse — base classes for Nix daemon operations.

These live in their own module to avoid circular imports: they depend on both wire_message (WireModel, WireField) and logs (WireLogs).

class nix_daemon_protocol.wire_ops.WireRequest[source]

Base class for Nix daemon protocol requests.

Subclasses must override op and response_type:

class SomeRequest(WireRequest):
    op: ClassVar[int] = 42
    response_type: ClassVar[type[SomeResponse]] = SomeResponse
    path: StorePath
async to_writer(ctx)[source]

Write op code then body, resolving the codec once.

Parameters:

ctx (WriteContext)

Return type:

None

async classmethod from_reader(ctx)[source]

Read body only — op was consumed by dispatch.

Parameters:

ctx (ReadContext)

model_config = {'arbitrary_types_allowed': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class nix_daemon_protocol.wire_ops.WireResponse(*, logs=<factory>)[source]

Base class for Nix daemon protocol responses.

The logs field is a WireLogs (stderr stream). Because it is a WireModel the generic serde engine handles it automatically — to_writer writes the full stderr stream before the body fields, from_reader reads the stream before the body fields.

Subclasses define wire-body fields as normal:

class SomeResponse(WireResponse):
    valid: bool
Parameters:

logs (Annotated[WireLogs, VersionMeta(min_version=None, max_version=None, serialize=None, deserialize=None, wire_depends_on=None, needs_features=None, unless_features=None, text_errors=strict)])

classmethod fast(**body)[source]

Build a response the daemon answers with, without validation.

Only for values that are already the declared types: the daemon decoded them from the wire, parsed them from a store, or holds them as constants. There is nothing to coerce and nothing to refuse, so a full pydantic validation only re-checks what an earlier boundary already proved. Anything built from untrusted input -- a remote .narinfo, a client string -- goes through the validated constructor instead.

The plan resolves at version 0 with no features, and only its defaults are kept: field defaults are static values, so no version resolves different ones, and the encode still gates fields under the negotiated version. This sets the body, fills those defaults, and gives the response fresh empty logs.

The logs are fresh per response and not shared: query_missing and set_options append to the logs of the response they build, and a shared log would carry one operation's warnings into another's answer. The log is built by hand and not by model_construct: an empty log needs no validation, and model_construct cost half of this constructor.

Parameters:

body (Any)

Return type:

Self

property is_not_found: bool

True when extension fallback should continue to another store.

model_config = {'arbitrary_types_allowed': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].


Serde Contexts

Context dataclasses passed through the serialization/deserialization pipeline.

Transport-neutral contexts used by daemon message codecs.

class nix_daemon_protocol.context.LogSink(*args, **kwargs)[source]

Optional real-time receiver for decoded daemon log records.

`LogMessage` and not `object`. A parameter is contravariant, so a sink declared to take object can only be satisfied by something that accepts anything. ClientConn.send takes a WireModel, which is narrower, so it did not satisfy this and every construction of a ReadContext from a client read as a type error.

LogMessage is what is actually passed: read_stream yields one and logs.py:198 is the only caller. A WireModel is wider than that, so ClientConn satisfies it.

class nix_daemon_protocol.context.ReadContext(reader, version, log_sink=None, buffer_logs=True, raise_on_error=True, error_factory=<class 'nix_daemon_protocol.exceptions.DaemonProtocolError'>, logger=None, features=frozenset({}))[source]

Bundles the arguments needed to deserialize a response from the wire.

Parameters:
  • reader (NixReader)

  • version (int)

  • log_sink (LogSink | None)

  • buffer_logs (bool)

  • raise_on_error (bool)

  • error_factory (Callable[[str], Exception])

  • logger (ProtocolLogger | None)

  • features (frozenset[str])

error_factory

alias of DaemonProtocolError

features: frozenset[str] = frozenset({})

The features that the two peers negotiated, and not the ones one named.

intersectFeatures at worker-protocol-connection.cc:148 of Nix builds it. A field with needs_features or unless_features reads this set. Issue #14.

class nix_daemon_protocol.context.WriteContext(writer, version, features=frozenset({}))[source]

Bundles the arguments needed to serialize a request/response to the wire.

Parameters:
  • writer (NixWriter)

  • version (int)

  • features (frozenset[str])

features: frozenset[str] = frozenset({})

The features that the two peers negotiated. See ReadContext.features.


Protocol Enums

Generic protocol-level enums and types.

class nix_daemon_protocol.protocol.FileIngestionMethod(*values)[source]

Method for ingesting files into the store.

class nix_daemon_protocol.protocol.GCAction(*values)[source]

Actions for collect-garbage operation.

class nix_daemon_protocol.protocol.OptTrusted(*values)[source]

Trust status for a path or user.

class nix_daemon_protocol.protocol.Verbosity(*values)[source]

Log verbosity levels.

class nix_daemon_protocol.protocol.ActivityType(*values)[source]

Tracked activity types in the daemon.

class nix_daemon_protocol.protocol.ResultType(*values)[source]

Result types for activities.

class nix_daemon_protocol.protocol.FieldType(*values)[source]

Types for activity/result fields.


Auth

Authentication related types.

class pynixd.serde.auth.Role(*values)[source]

Client authorization roles.


IDs

Strongly-typed identifiers for stores, builds, and requests.

nix_daemon_protocol.ids.StoreId

Opaque identifier for a Nix store backend (e.g. "local", "ssh-builder1").

nix_daemon_protocol.ids.BuildId

Opaque identifier for an in-flight build in the build queue.

nix_daemon_protocol.ids.RequestId

Opaque identifier for a client request being tracked by the proxy.

nix_daemon_protocol.ids.LOCAL_STORE_ID = 'local'

The identifier of the store that runs beside pynixd itself.

Every deployment has this store, and pynixd makes one when the configuration names none. Sixteen sites wrote StoreId("local") or the bare string "local", so a reader had to know that the two spellings mean one thing.


Type Aliases

Reusable type aliases for common complex types.

type nix_daemon_protocol.aliases.OutputName = str

Name of a derivation output (e.g. "out", "bin", "lib").

type nix_daemon_protocol.aliases.OutputMap = dict[StorePath, dict[OutputName, StorePath | None]]

Map of derivation paths to their output maps.

type nix_daemon_protocol.aliases.StorePathSet = set[StorePath]

Set of store paths — the protocol Set<StorePath>.