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)
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_writerwritesstr(self)as a single wire string.__hash__/__eq__delegate tostr(self).Override
from_str/to_strin subclasses that need custom wire format (e.g.WireSignaturesplits 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)
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
opandresponse_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
logsfield is aWireLogs(stderr stream). Because it is aWireModelthe generic serde engine handles it automatically —to_writerwrites the full stderr stream before the body fields,from_readerreads 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.
Auth¶
Authentication related types.
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.