valis / Reference / API reference

The edge - API reference

Exported surface for the edge subsystem. Part of the API reference.

Package valis/src/edge/control-client

Conditions

control-client-error

Signalled when the control session fails a client-side expectation — a /ports walk that does not resolve, or a dial that never connects. Distinct from the nine-p-error a server Rerror surfaces through decode-message and from the descriptor-admission-refused the push primitive signals fail-closed.

Functions

connect-and-push

(connect-and-push control-socket-path fd ports &key (uname "valis") (aname ""))

Dial CONTROL-SOCKET-PATH, run the 9P control session, and perform the /ports descriptor push of FD carrying the PORTS declaration — the entry point the resident boot calls. Returns two values: the Rwrite-count the receiver reported and the live control ENDPOINT, kept open so the daemon holds the control session for teardown. FD is caller-owned and returned still open by the push, so the daemon keeps its copy to wrap as the steered source. On any failure the freshly-dialed endpoint is closed before the condition propagates, so a failed push leaks no control connection.

declare-ports-and-push-fd

(declare-ports-and-push-fd endpoint fd ports &key (uname "valis") (aname ""))

Drive the single-threaded 9P control session on ENDPOINT and push FD: version -> attach -> walk /ports -> open for write -> the /ports descriptor push of FD carrying the PORTS declaration. Returns the Rwrite-count the receiver reported for the /ports write.

Only the final /ports push obeys the sender-ordering contract (Twrite on the wire -> send FD WITHOUT awaiting the Rwrite -> read the bounded Rwrite); every earlier op is an ordinary synchronous request/reply. The descriptor transfer is race-free at the wire — the receiver reads with seven's exact-count control reader, so the carrier and its ancillary wait queued until it pulls them — and the ordering exists ONLY to avoid deadlocking a receiver that pulls the FD, on its serve thread inside the /ports write, before it answers. FD is validated and pushed by send-admitted-fd (LISTEN-state + AFINET, gated fail-closed); it is caller-owned on every outcome and returned still open on success — the caller keeps its copy to wrap as the steered edge source.

dial-control-endpoint

(dial-control-endpoint control-socket-path &key (retries 5) (retry-interval 0.05))

Dial the host agent's AFLOCAL control socket at CONTROL-SOCKET-PATH and return a seven socket-endpoint over the connected stream. Retries a bounded few times if the connect races the launcher bringing the socket up — the launcher binds and listens before it execs valis, so a connect normally lands in the listen backlog; the retry only covers a narrow startup race. Signals control-client-error if every attempt fails.

The endpoint carries both the 9P control frames and, sideband over SCMRIGHTS on the same connection, the pushed descriptor — there is no second channel.

Variables

*rwrite-read-deadline-seconds*

The bounded ceiling declare-ports-and-push-fd waits for the /ports Rwrite after the descriptor is sent — defence-in-depth against a receiver that never answers (a hung serve thread or a broken peer), NOT the primary transfer guard. The descriptor transfer is race-free at the wire: the receiver reads with seven's exact-count, control-armed reader, which cannot over-read the carrier, so the carrier and its SCMRIGHTS ancillary wait queued until the receiver pulls them. A healthy push therefore completes in well under a second, and this ceiling only ever fires when the peer stops responding. iolib puts the carrier fd in non-blocking mode and refills through a poll with no timeout, so a socket read-timeout option does not reach that read; %read-rwrite-bounded shuts the read half down after this deadline instead, turning a silent peer into a fast wire-error rather than a permanent hang at boot.

Package valis/src/edge/controller

Functions

attach-budget-source

(attach-budget-source executor source mux)

Wire the freshly-built listening SOURCE (which already carries EXECUTOR) and its MUX back into EXECUTOR, so the accept path can deregister SOURCE at cap and the exit path re-register it a slot later. Must be called after the source is built — the source does not exist when the executor is constructed — and before the source is registered. Returns SOURCE.

budget-executor-live-count

(budget-executor-live-count executor)

The number of connections currently inside their per-connection lifetime under EXECUTOR's budget. The accept path increments it and every exit path decrements it under one unwind-protect, so a drain can poll this to wait for in-flight connections to quiesce without matching worker thread names against a port recovered elsewhere.

edge-bound-ports

(edge-bound-ports)

Return the list of currently bound edge ports, ascending. A read-only view of the controller registry for the status axis and tests; never the live table.

edge-entry-port

(edge-entry-port instance)

Undocumented: this exported symbol needs a docstring.

edge-entry-protocol

(edge-entry-protocol instance)

Undocumented: this exported symbol needs a docstring.

edge-entry-source

(edge-entry-source instance)

Undocumented: this exported symbol needs a docstring.

edge-port-stats

(edge-port-stats port)

Return the PORT-STATS for the bound edge PORT, or NIL when PORT is not bound. The read entry point the status axis renders one port's budget counters from; never hands out the controller's live table.

edge-port-stats-list

(edge-port-stats-list)

Return the PORT-STATS of every bound edge port, ascending by port. A read-only snapshot for the status axis and tests.

edge-running-p

(edge-running-p)

Return T when the edge controller has at least one port bound.

make-edge-budget-executor

(make-edge-budget-executor &key (budget *default-edge-budget*) session-ceiling)

Build a budget-executor over a fresh set of port counters carrying BUDGET (default default-edge-budget, 64) and SESSION-CEILING. The returned executor caps concurrent per-connection lifetimes at BUDGET exactly as every bound edge port does: at cap it deregisters its listening source so further connections wait in the kernel backlog, and it re-registers the source as slots free. The caller builds the listening source carrying this executor, then calls attach-budget-source to wire the source and mux back in so the cap can act. This is the reuse seam the :53 serve wiring gates its steered TCP source through, so that stranger-facing path gets the same proven concurrency bound and drain counter as the edge ports.

A caller wiring its own source names its own bound on live sessions with SESSION-CEILING. It defaults to NIL, which leaves the port with no session bound of its own: only the request budget speaks for it, and whether a session may be adopted at all is then decided by the adoption entry point's own clause. That is the shipped state of every port wired through this seam.

make-steered-edge-datagram-source

(make-steered-edge-datagram-source port executor &key fd)

Build an edge datagram source from an inherited, already-bound UDP FD rather than by binding PORT — the datagram mirror of make-steered-edge-stream-source. The privileged front binds the UDP port and hands valis the descriptor over SCMRIGHTS; this wraps it into a source with no second bind, honouring the contract that valis never binds the designated ports.

PORT is not used to construct the source: the inherited fd already carries its bound port, and dest-port recovery happens through local-port on the descriptor. PORT stays the registry key the caller threads through. The actual fd->source wrap lives in the epoll backend so this file names no iolib symbol. DNS is deliberately not an edge-adapters entry, so start-edge never constructs this — the DNS serve wiring invokes it directly.

make-steered-edge-stream-source

(make-steered-edge-stream-source port executor &key fd)

Build an edge stream source from an inherited, already-listening FD rather than by binding PORT. This is the steered alternative for edge-source-constructor: rebind that variable to a closure over the fd a co-located privileged agent hands valis (arriving over SCMRIGHTS), and start-edge then accepts on the inherited descriptor with no port binding — the contract that valis never binds the designated ports.

PORT is not used to construct the source: the inherited fd already carries its LISTEN state, and destination-port recovery happens per accepted client, not from PORT. PORT stays the registry key the rest of start-edge threads through, so the recovered protocol still dispatches by the port the controller was asked to serve. The actual fd→source wrap lives in the epoll backend so this file names no iolib symbol.

port-stats-accepted-total

(port-stats-accepted-total stats)

Return the total accepts ever admitted on this port's STATS.

port-stats-budget

(port-stats-budget stats)

Return the fixed concurrent-connection budget for this port's STATS.

port-stats-deadline-cuts

(port-stats-deadline-cuts stats)

Return the count of read-deadline-cut exits on this port's STATS.

port-stats-live-count

(port-stats-live-count stats)

Return the current live-connection count on this port's STATS.

port-stats-port

(port-stats-port stats)

Return the integer port this STATS belongs to.

port-stats-session-ceiling

(port-stats-session-ceiling stats)

Return the fixed live-session ceiling for this port's STATS, or NIL when the port enforces no session bound of its own.

port-stats-session-count

(port-stats-session-count stats)

Return the number of loop-driven sessions live on this port's STATS.

retire-edge-adapter

(retire-edge-adapter name &key (mux (and *listener* (listener-mux *listener*))) (drain-timeout *edge-drain-timeout-seconds*))

Retire the live edge ports of the adapter named NAME: drain their in-flight connections, tear them down in start-edge's reverse order, and remove the adapter from the edge-adapters registry so a later start-edge does not bind it again. The ordered teardown per matching port is deregister-source -> drain (bounded) -> close-source -> unregister-protocol -> remhash the edge-entry. A port belongs to NAME when its edge-entry's ADAPTER-NAME matches NAME (compared by STRING / case-insensitively, since an adapter may name itself :http or "http"). The match is on the adapter name the entry recorded at start-edge, NOT on the protocol's PROTOCOL-NAME - a non-resident adapter whose protocol names itself differently from its registry key must still have its ports retired. MUX defaults to the running listener's multiplexer. This is the primitive the delete-module hook calls so deleting a protocol module retires its live ports before the module's packages unbind.

Returns the list of ports retired. Signals RETIREMENT-INCOMPLETE when any release did not happen - a port still bound after a successful-looking retire is the outcome this refuses to produce.

Every port is attempted before anything is reported, so one wedged port cannot strand the others. A port whose release failed is NOT counted as retired and KEEPS its edge-entry: the controller's record of what is bound stays true, the port still shows in EDGE-BOUND-PORTS, and a second retire picks it up again.

start-edge

(start-edge &key http-port gopher-port http-budget gopher-budget overrides (mux (listener-mux *listener*)))

Bind the registered adapter protocols' loopback ports over the running listener's multiplexer, fail-closed. The adapter set comes from the edge-adapters registry: start-edge iterates (edge-adapters) and binds every port each registered protocol declares — it names no concrete adapter, so a protocol installs simply by being loaded. MUX defaults to the running listener's multiplexer — the edge registers its sources on the already-running event loop rather than owning its own listener.

Per-adapter overrides: OVERRIDES is the real path, a generic alist mapping an adapter name to a (:port p :budget b) plist; a spec with no override binds its own default port and default-edge-budget. HTTP-PORT / GOPHER-PORT / HTTP-BUDGET / GOPHER-BUDGET are a TRANSITIONAL COMPAT SHIM that maps onto the two resident adapters by name ("http" / "gopher") so the existing call-sites keep working unchanged; remove this surface when HTTP/Gopher relocate out of valis and the only override path is OVERRIDES.

Sequence (no namespace → no port): first the pre-bind gate runs a REAL attach+walk of the anonymous public view through the seam; a grant that does not resolve aborts before any port binds. Then, per registered spec: construct the protocol via its (&key port budget) constructor (passing the resolved override), register the protocol with the port→protocol registry, and for EACH port the constructed protocol declares, build a budget-executor carrying that port's live counters, construct the source through edge-source-constructor carrying the executor, wire the source and mux back into the executor, and register the source on MUX.

Each protocol is tracked for unwind the moment it is registered, each source the moment it is built, and each bound port the moment its entry is created — so a failure ANYWHERE in the sequence (a source constructor that signals, a port already claimed, an N-th adapter beyond the resident two) unwinds exactly what was put in place, in reverse: deregister and close every source, unregister every protocol, and remhash precisely the ports that were tracked (never a fixed two-port literal). Returns the list of bound ports on success.

stop-edge

(stop-edge &key (mux (and *listener* (listener-mux *listener*))))

Close every bound edge port and unwire the edge, in reverse of start order (ports close before adapter/seam/fabric teardown). Idempotent and safe to call when the edge was never started — an empty registry is a no-op. Each step is wrapped so one failure cannot block the rest: per entry, deregister and close the source (the wire stops feeding the seam first), then unregister the protocol; then clear the controller registry; then reset the edge seam so the cached anonymous session closes AFTER the wire has stopped feeding it. MUX defaults to the running listener's multiplexer, or nil when no listener is up (the deregister steps tolerate a nil mux by skipping).

Variables

*default-edge-budget*

Default per-port concurrent-connection budget. Each bound edge port may have at most this many connections live at once; the budget-th accept deregisters the port's listening source so further connections wait in the kernel listen backlog rather than spending a valis thread (never accept-then-close). 64 sits far above any legitimate loopback load yet bounds the live SBCL threads a single flooded port can spawn. start-edge takes :http-budget / :gopher-budget to override per port; a flood on one protocol cannot starve another because each port counts independently.

*default-edge-session-ceiling*

How many live loop-driven sessions one edge port admits when it is given no figure of its own. A port takes this number at start-edge and holds its own copy, so two ports never spend one allowance and a flood on one protocol cannot close another.

PROVISIONAL. Nothing has profiled this system, and a measured figure replaces this one.

What sizes it is memory a port is willing to hold on behalf of peers that are doing nothing: the resident cost of one idle session, multiplied by this number. That cost is dominated by the session's own read buffer of 4096 octets, so this many of them hold about 1.6 MiB of buffer per port, and something under 3 MiB once the session object, its send queue and its clocks are counted alongside.

It respects a second bound. One process shares its descriptors between every bound port, its fabric, and whatever it dials outbound, and this figure stays well clear of that shared supply. A flood of sessions therefore meets this bound first: reaching it closes one port and leaves the rest of the process serving, where running out of descriptors takes everything down at once.

⛔ It is deliberately unrelated to the request budget beside it, and neither was derived from the other. That one is an abuse bound on work in flight: how much the port will have underway at once. This one is a density decision about what is resident while nothing at all is happening. Sizing either from the other would be a coincidence dressed up as a reason.

*edge-source-constructor*

A function of (PORT EXECUTOR) returning a stream event source bound to PORT. This is the single point where a wire port becomes an event source: a later phase rebinds it to a steered-fd source constructor — the host agent hands valis a steered fd and this becomes fd→source — without changing start-edge or any other call site. The default-multiplexer- factory discipline applied to the edge. No fd-handoff code lives here yet; this is the indirection alone.

Package valis/src/edge/delegation-check

Classes

delegated-nameserver

One nameserver a zone is delegated to. NAME is its domain name. ADDRESSES are the addresses a glue record carries for it, which is a list because a nameserver may hold both an IPv4 and an IPv6 address, and empty for a nameserver outside the zone, which needs no glue and must not be given any.

delegation-report

One delegation probe's whole result.

VERDICT is a member of DELEGATION-VERDICTS. EXPECTED is the intended nameserver set and FOUND the set the parent nameservers agreed on, both DELEGATED-NAMESERVER lists; FOUND is NIL when they did not agree, in which case the per-nameserver sets in FINDINGS carry the disagreement. PARENT-ZONE names the zone whose nameservers were asked. FINDINGS holds one PROBE-ATTEMPT per parent nameserver, so a report says which nameserver a reading belongs to and why any of them went unread. DESCENT holds the attempts the walk down made, kept because a walk that stopped early is diagnosed from them. BOUNDED-OUT is true when the deadline stopped the walk.

intended-delegation

What a domain's delegation should be: the ORIGIN and the NAMESERVERS it should be delegated to, each a DELEGATED-NAMESERVER.

This is the whole of the seam a registrar driver would arrive behind. A driver reading a registrar's API produces this value; the record template produces it from its own defaults; both then reach the same comparison, and neither knows about the other.

probe-attempt

One nameserver asked once, and what it said.

READING is a member of PARENT-READINGS. OUTCOME and RCODE are runciter's own reading of the same response, kept alongside rather than replaced, so a report can name the REFUSED behind an unreachable-looking verdict. NAMESERVERS is the delegated set read off the answer. REFERRAL-ZONE and REFERRAL-SERVERS carry the step the walk takes next. CAUSE holds the condition when the nameserver could not be asked.

Conditions

delegation-transport-failure

Signalled by the carrying half of a probe when a nameserver could not be asked: the dial was refused or failed, the answer did not arrive in time, the connection broke, the response was malformed, or it did not bind to the query. CAUSE holds the underlying condition, never a rendered string, so a caller can still discriminate.

Every one of those is a fact about reaching the nameserver, and the probe maps all of them to the UNREACHABLE verdict and never to PENDING. That mapping is the whole reason this is a single condition type: a reading that cannot be trusted must not be able to leak into a reading about the delegation's contents.

unaddressable-nameserver

Signalled when a nameserver value reaches the dialling step carrying nothing to dial. Deliberately NOT a transport failure: every referral names its nameservers and every root hint carries an address, so this can only arrive from a caller that constructed the value wrongly, and mapping it to UNREACHABLE would report a defect here as a fact about somebody's network.

Generic functions

delegation-transport-failure-cause

(delegation-transport-failure-cause condition)

Undocumented: this exported symbol needs a docstring.

delegation-transport-failure-nameserver

(delegation-transport-failure-nameserver condition)

Undocumented: this exported symbol needs a docstring.

unaddressable-nameserver-server

(unaddressable-nameserver-server condition)

Undocumented: this exported symbol needs a docstring.

Functions

delegated-nameserver-addresses

(delegated-nameserver-addresses instance)

Undocumented: this exported symbol needs a docstring.

delegated-nameserver-name

(delegated-nameserver-name instance)

Undocumented: this exported symbol needs a docstring.

delegated-nameserver-p

(delegated-nameserver-p object)

Undocumented: this exported symbol needs a docstring.

delegation-exit-code

(delegation-exit-code report)

The process exit code REPORT amounts to: 0 settled, 1 for a delegation that is not yet what it should be, and 2 for a probe that could not establish anything.

Non-zero on anything but settled, so the check works as a gate in a script rather than as something read by eye. The two failing codes are distinct because they call for different next steps: 1 says look at the registrar, 2 says look at the network.

delegation-report-bounded-out

(delegation-report-bounded-out instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-descent

(delegation-report-descent instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-expected

(delegation-report-expected instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-findings

(delegation-report-findings instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-found

(delegation-report-found instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-origin

(delegation-report-origin instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-p

(delegation-report-p object)

Undocumented: this exported symbol needs a docstring.

delegation-report-parent-zone

(delegation-report-parent-zone instance)

Undocumented: this exported symbol needs a docstring.

delegation-report-verdict

(delegation-report-verdict instance)

Undocumented: this exported symbol needs a docstring.

delegation-settled-p

(delegation-settled-p report)

True when REPORT's verdict is :SETTLED.

intended-delegation-for-origin

(intended-delegation-for-origin origin instance-address &key (primary-ns-label *default-primary-ns-label*) (secondary-ns (resolve-secondary-ns)))

The intended delegation for ORIGIN under the record template's nameserver policy: the in-zone primary, whose glue is this node's INSTANCE-ADDRESS, and the shared out-of-zone secondary, which carries none.

The primary's label is read from the template module rather than restated, so a ruling that moves it moves what this probe expects at the same moment. SECONDARY-NS comes from the operator's configuration, and is NIL on a node that runs no secondary: the delegation then names the primary alone, which is what the record template mints for that node.

intended-delegation-from-names

(intended-delegation-from-names origin names &key glue-address)

The intended delegation for ORIGIN naming NAMES, with GLUE-ADDRESS attached to every name that falls inside the zone.

The glue attaches by position in the tree rather than by the caller saying which name needs it, because that is the rule a registrar applies and getting it wrong is the failure the whole check exists to catch. For an operator naming the set by hand, and for a registrar driver reading it back from an API, which is the other half of the same seam.

intended-delegation-nameservers

(intended-delegation-nameservers instance)

Undocumented: this exported symbol needs a docstring.

intended-delegation-origin

(intended-delegation-origin instance)

Undocumented: this exported symbol needs a docstring.

intended-delegation-p

(intended-delegation-p object)

Undocumented: this exported symbol needs a docstring.

make-delegated-nameserver

(make-delegated-nameserver &key ((:name name) nil) ((:addresses addresses) nil))

Undocumented: this exported symbol needs a docstring.

make-dial-exchange

(make-dial-exchange dial-capability &key (port *delegation-probe-port*) (wire (make-delegation-probe-wire)) (attempt-seconds *dial-deadline*))

The exchange closure a probe carries its bytes through: a function of (SERVER ORIGIN QTYPE DEADLINE) returning (values QUERY RESPONSE AUTHORITY ADDITIONAL), and signalling DELEGATION-TRANSPORT-FAILURE when the nameserver could not be asked.

DIAL-CAPABILITY is required and positional, mirroring the seam it reaches: there is no arity of this that puts a query on a wire without the authority to do so having been handed in.

The exchange runs over TCP rather than UDP. A referral for a busy parent zone, with a nameserver set and its glue, is exactly the answer that does not fit in a bare datagram, and a truncated referral would read as a SHORTER nameserver set: a wrong reading rather than a failed one. TCP costs a handshake per attempt, which a diagnostic run a handful of times can afford.

Each attempt is bounded twice: ATTEMPT-SECONDS bounds every read on the connection, defaulting to the same per-address budget the dial itself uses, and DEADLINE caps that so an attempt can never outlive the whole probe.

make-intended-delegation

(make-intended-delegation &key ((:origin origin) nil) ((:nameservers nameservers) nil))

Undocumented: this exported symbol needs a docstring.

make-probe-attempt

(make-probe-attempt &key ((:server server) nil) ((:reading reading) nil) ((:outcome outcome) nil) ((:rcode rcode) nil) ((:nameservers nameservers) nil) ((:referral-zone referral-zone) nil) ((:referral-servers referral-servers) nil) ((:cause cause) nil))

Undocumented: this exported symbol needs a docstring.

probe-attempt-cause

(probe-attempt-cause instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-nameservers

(probe-attempt-nameservers instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-outcome

(probe-attempt-outcome instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-p

(probe-attempt-p object)

Undocumented: this exported symbol needs a docstring.

probe-attempt-rcode

(probe-attempt-rcode instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-reading

(probe-attempt-reading instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-referral-servers

(probe-attempt-referral-servers instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-referral-zone

(probe-attempt-referral-zone instance)

Undocumented: this exported symbol needs a docstring.

probe-attempt-server

(probe-attempt-server instance)

Undocumented: this exported symbol needs a docstring.

probe-delegation-through-exchange

(probe-delegation-through-exchange intended exchange &key (deadline (+ (get-universal-time) *delegation-probe-deadline*)) (roots *root-hint-servers*))

Probe the parent delegation for INTENDED through EXCHANGE and return a DELEGATION-REPORT.

EXCHANGE carries the bytes: a function of (SERVER ORIGIN QTYPE DEADLINE) returning (values QUERY RESPONSE AUTHORITY ADDITIONAL) and signalling DELEGATION-TRANSPORT-FAILURE when a nameserver could not be asked. Injecting it is what lets the comparison, the referral reading and the whole verdict vocabulary be exercised against constructed responses with no network at all.

DEADLINE is an absolute universal time and the probe is bounded by it: a deadline that has already passed produces a terminal verdict without issuing one query, and one that passes mid-walk stops the walk and returns the verdict the readings already support.

Every parent nameserver is asked, not just the first that answers, because nameservers disagreeing with each other is itself a distinct and expected reading during propagation.

probe-parent-delegation

(probe-parent-delegation intended dial-capability &key (port *delegation-probe-port*) (deadline (+ (get-universal-time) *delegation-probe-deadline*)) (roots *root-hint-servers*))

Probe the parent delegation for INTENDED, carrying the queries out through DIAL-CAPABILITY, and return a DELEGATION-REPORT.

DIAL-CAPABILITY is required and positional. It is the whole of valis's part here: the queries are runciter's and the reading of each answer is runciter's, and what this node contributes is the authority to leave it, the clock, and the walk.

registrar-instructions

(registrar-instructions intended)

The registrar block for INTENDED, as text.

This is what the operator copies from, and a wrong paste is the failure the whole verification exists to catch, so it is unambiguous about the one thing a registrar form gets wrong: which nameserver names need a glue (host) record and which must not be given one. It is deliberately a numbered list rather than a table, because a table's columns are what get lost on the way to a web form.

report-delegation

(report-delegation report &optional (stream *standard-output*))

Write REPORT to STREAM: the verdict, what was expected, what was found, and one line per parent nameserver saying what it said and, when it said nothing, why. Returns the verdict.

The per-nameserver lines are not decoration. A verdict alone cannot distinguish a repoint that is landing from one that was pasted wrong, and the reason a nameserver went unread is the whole difference between waiting and investigating.

Variables

*delegation-probe-deadline*

Seconds a whole delegation probe is given before it returns the verdict it has. The probe is bounded by an ABSOLUTE deadline derived from this, and each attempt under it is bounded again by the per-address dial budget, so one slow nameserver costs one attempt rather than the walk.

*delegation-probe-port*

The port a delegation probe is sent to. A parameter so a test can aim a walk at a loopback receiver.

*delegation-verdicts*

The closed set of verdicts a delegation probe yields.

:SETTLED Every parent nameserver that answered names exactly the intended nameserver set. The repoint is in place and has propagated.

:PENDING The parent nameservers agree with each other and name something other than the intended set, the empty set included. Either the repoint has not been made yet, or it was made wrongly. The report carries both what was expected and what was found, because those two cases need opposite responses and only the operator can tell them apart.

:INCONSISTENT The parent nameservers disagree with each other. During propagation this is normal and expected, and it is NOT the same as :PENDING: a repoint that is landing looks like this, and a repoint that was pasted wrong does not.

:UNREACHABLE No parent nameserver could be asked at all, or the probe ran out of time before one answered. This says nothing whatever about the delegation. It is its own verdict precisely so a network fault can never be read as a delegation that has not propagated; the report carries the cause of each failure.

*parent-readings*

The closed set of per-nameserver readings a probe attempt yields.

:DELEGATES The nameserver named a nameserver set for the origin, whether in a referral or an authoritative answer.

:NO-DELEGATION It answered and names no delegation for the origin: the name does not exist there, or exists with no NS set. A genuine answer, and evidence the delegation is not in place.

:REFERRED It referred the walk further down rather than answering for the origin's parent, so it is not a parent nameserver. This is what every step of the descent produces.

:REFUSED REFUSED: a live nameserver declining the zone. Kept distinct because the operator action differs completely. It means this server does not serve that zone, which is the single most useful discrimination a bring-up gets, and it is why this probe decodes its own responses instead of reusing a path that folds REFUSED into a generic failure.

:SERVER-FAILURE It answered that it could not answer. Retryable, and no evidence about the delegation.

:UNUSABLE It answered something that is not evidence about this query: a malformed message, or an answer to a different question.

:UNREACHABLE It could not be asked: the dial failed, or it did not answer in time. The cause is preserved.

*root-hint-servers*

The root nameservers the walk starts from, as (NAME ADDRESS) pairs.

Held as addresses rather than names on purpose: a name would have to be resolved before the first query, and needing a working resolver in order to check a delegation is exactly the dependency this probe exists without. Held as a parameter so a test can point a whole walk at a receiver it controls.

These are the published root hints and they change rarely. When one moves the walk still works: it stops at the first root that answers, and every root answers the same referral.

Package valis/src/edge/delegation-probe-codec

Classes

probe-codec

Binds runciter's probe-codec provider protocol to c3po's resolver-direction DNS codec. Stateless: it holds no connection, no outstanding set and no clock, because the probe's caller owns all three.

Conditions

probe-response-unbound

Signalled when a decoded probe response carries a message id other than the outstanding query's. The response decoded cleanly and is simply about something else, so it is refused rather than read: a verdict drawn from another query's answer would be worse than no verdict. A caller maps this to the verdict it uses for a nameserver it could not get an answer from, never to one about the delegation's contents.

Generic functions

probe-response-unbound-expected

(probe-response-unbound-expected condition)

Undocumented: this exported symbol needs a docstring.

probe-response-unbound-received

(probe-response-unbound-received condition)

Undocumented: this exported symbol needs a docstring.

Functions

make-delegation-probe-wire

(make-delegation-probe-wire)

Construct runciter's delegation PROBE-WIRE over this codec provider. runciter runs refuse-before-act at construction, so a version skew or a missing operation is a refusal here rather than a probe framed against a half-understood seam.

make-probe-codec

(make-probe-codec)

Construct the probe-codec provider.

Package valis/src/edge/descriptor-admission

Conditions

descriptor-admission-unavailable

Signalled by assert-descriptor-admission when the pinned admission chokepoint symbol is not fbound. The fail-closed elevation guard: an absent chokepoint refuses the admission outright rather than binding a descriptor with no capability check.

descriptor-admission-version-mismatch

Signalled by assert-descriptor-admission when the declared descriptor-admission contract version is outside supported-descriptor-admission-versions. The gate refuses BEFORE any bind side effect, so a cross-repo seam-version skew never wires a steered descriptor to a handler.

Functions

assert-descriptor-admission

(assert-descriptor-admission declared-version)

The version-and-availability precondition that guards the fulcrum-steered-descriptor -> adapter admission, run by a caller BEFORE any bind mutation and refusing fail-closed. Signals DESCRIPTOR-ADMISSION-UNAVAILABLE when the pinned admission chokepoint symbol (admit-module) is not fbound — a presence check on the chokepoint, NOT a call to it; the caller must not proceed to a bind when the chokepoint the steered-descriptor path depends on is absent. Signals DESCRIPTOR-ADMISSION-VERSION-MISMATCH when DECLARED-VERSION is not in supported-descriptor-admission-versions. Otherwise returns descriptor-admission-contract-version. This is a PURE precondition: it opens no socket, performs no bind, and does not itself admit a descriptor — it only refuses or clears the path. The owner-vouched-hash admission (admit-module) runs on the path where a descriptor is actually steered in; this precondition is the gate upstream of that path.

Variables

*supported-descriptor-admission-versions*

The descriptor-admission contract versions assert-descriptor-admission admits. A declared version outside this set is refused fail-closed — a cross-repo seam-version skew is never silently bridged into a bind.

Constants

+descriptor-admission-contract-version+

The contract version of the fulcrum-steered-descriptor -> adapter admission this valis exposes. Distinct from the DNS adapter seam version: this one couples the steered-descriptor admission to its boot caller, that one couples valis to the out-of-tree wire engine.

Package valis/src/edge/descriptor-peer

Conditions

descriptor-admission-refused

Signalled when a descriptor crossing is refused fail-closed — the descriptor is not a LISTEN-state socket of the expected family. In every rejection path the descriptor is closed before the condition is raised, so a refused crossing never leaks a descriptor. This condition covers the family and socket-state axes; a contract-version skew keeps the existing descriptor-admission-version-mismatch / descriptor-admission-unavailable conditions the admission gate signals.

Generic functions

descriptor-admission-refused-reason

(descriptor-admission-refused-reason condition)

Undocumented: this exported symbol needs a docstring.

Functions

admit-received-fd

(admit-received-fd endpoint &key (expected-domain 2))

Receive one descriptor over the SCMRIGHTS seam on ENDPOINT — a seven AFLOCAL control-socket-endpoint (constructed :control t) — gate it fail-closed against the versioned descriptor-admission contract and the kernel's own view of the descriptor, and return the validated descriptor. EXPECTED-DOMAIN is the address family the crossing must carry (default 2 = AFINET). The seam's receive-fd-over-endpoint reads only through the exact-count control reader, so a plain endpoint is refused up front; constructing ENDPOINT as a control endpoint is the caller's responsibility.

Refuses fail-closed on every axis, each path leaving nothing leaked: the admission gate refuses a contract-version skew or an absent chokepoint (its own conditions) BEFORE any descriptor is read; the seam itself refuses (and closes) a multi-descriptor or truncated message; and a descriptor that is not a LISTEN-state stream socket, or is of the wrong family, is closed here and signals DESCRIPTOR-ADMISSION-REFUSED with a structured log line. Returns the validated descriptor only when every gate passes; the caller owns closing it on teardown.

listening-socket-fd-p

(listening-socket-fd-p fd)

True when FD is a stream socket in the LISTEN state — SOTYPE is SOCKSTREAM and SOACCEPTCONN is set. This is the gate a crossed descriptor must pass before it is adopted: a connected or wrong-type descriptor is rejected. Returns NIL (never signals) for a descriptor that is not a socket, so the caller can fail closed uniformly.

send-admitted-fd

(send-admitted-fd endpoint fd &key (expected-domain 2))

Send valis's own LISTEN-state descriptor FD across ENDPOINT — a seven AFLOCAL socket-endpoint — via the SCMRIGHTS seam, gated fail-closed exactly as the receive path is. EXPECTED-DOMAIN is the address family the pushed descriptor must be (default 2 = AFINET).

The admission gate is asserted BEFORE the push (a contract-version skew or absent chokepoint refuses via its own conditions), and FD is validated to be a LISTEN-state stream socket of the declared family against the kernel's own view. A mismatch signals DESCRIPTOR-ADMISSION-REFUSED with a structured log line.

The caller owns FD on EVERY outcome and closes it itself: on a successful crossing FD is returned still open (the seam sends a caller-owned copy; the sender closes its own copy after the handoff), and on a refusal FD is left exactly as it was found — the send half never adopted anything, so it never closes the descriptor it was handed. This keeps ownership uniform, so a caller may wrap the call in (unwind-protect (send-admitted-fd ep fd) (close fd)) without a double close. The carrier ENDPOINT is never closed either.

socket-fd-domain

(socket-fd-domain fd)

Return FD's address family (SODOMAIN): 1 = AFUNIX, 2 = AFINET, 10 = AFINET6. Signals if FD is not a socket.

Package valis/src/edge/dns-controller

Functions

admit-and-bind-dns-module

(admit-and-bind-dns-module system root files vouch source &key (loader (function load-system)) (name +default-dns-adapter-name+))

Admit a DNS wire module through the owner-vouched-hash chokepoint and, ONLY on admission, bind runciter's serving handler over SOURCE onto the seam under NAME. Admission is delegated whole to admit-module (content hash, owner :admit vouch, revocation/fence) — no parallel trust check; loading the admitted module is its own self-registration onto the seam. Returns (values spec nil) on success, or (values nil reason-string) on a refused admission, in which case the loader never ran and no adapter was bound (refuse-before-act).

The registry is snapshotted before admission, so a bind that fails AFTER the module loaded rolls the whole registry back to its pre-admission state — the admitted module's own self-registration is undone too, leaving no orphan adapter behind from a failed admit-and-bind.

bind-dns-view

(bind-dns-view source &key (name +default-dns-adapter-name+) (verify (function %default-bind-verify)))

Bind runciter's DNS serving handler over SOURCE (a zone-data-source; in production a pg-zone-source) under NAME, returning the bound dns-adapter-spec. A SINGLE seam mutation: build the runciter handler over SOURCE (%build-runciter-dns- handler resolves runciter's serving builder late and fails closed when it is absent or the seam versions disagree) and register it under NAME. The registered handler is self-contained — it holds its own SOURCE and pulls its own refresh, reading no valis special — so there is no second view-offer mutation; this is a single-mutation bind.

The no-partial-bind discipline holds on that single mutation: PRIOR-SPEC is captured first, VERIFY runs LAST as a self-check, and on any signal in the sequence the unwind restores whatever adapter was registered under NAME before this bind (or unregisters when none was), so a failed bind never clobbers a pre-existing same-name adapter and leaves no partial state.

runciter's handler builds its per-origin index eagerly at construction by reading the source, so the build+register+verify runs inside an operator-state connection when the pool is armed and BARE otherwise — an in-memory / test bind with no DSN needs no connection. VERIFY defaults to %default-bind-verify and stays injectable so the unwind path is directly assertable with a forced fault.

register-serving-refresh-listener

(register-serving-refresh-listener &key (name +default-dns-adapter-name+))

Wire the resident serving path's post-commit refresh: register a zone-change listener that force-refreshes the LIVE serving handler bound under NAME whenever a zone commits. The handler is captured from the bound adapter spec (dns-adapter-spec-handler) — the SAME funcallable object the serve loop dispatches through — so a rebuild is visible to the loop with no swap. The listener fires under fire-zone-change on the committing thread (connected, post-commit, outside the transaction), so the rebuild's pg-zone-source reads run with a live connection, landing the just-committed member in the serving index synchronously.

Refuses fail-closed when no handler is bound under NAME or runciter's REFRESH-DNS-SERVICE is unavailable — a serve loop with no refresh path would strand a just-committed record invisible to the wire. While wired, the lazy-pull interval is pushed far out (the connectionless serve threads can never satisfy a pull), so the commit-hook push is the sole refresh path; the prior interval is restored on unregister. Returns the registered listener spec.

unbind-dns-view

(unbind-dns-view &optional (name +default-dns-adapter-name+))

Retire the controller-bound DNS adapter, the symmetric teardown of bind-dns-view (mirroring stop-edge / retire-edge-adapter). Unregisters NAME from the seam; the runciter handler held the source itself, so there is no separate view special to clear. Idempotent — a second teardown is a no-op.

unregister-serving-refresh-listener

(unregister-serving-refresh-listener)

Retire the serving-refresh listener wired by register-serving-refresh-listener and restore the lazy-pull interval, so neither the listener nor the pushed-out interval outlives the serve loop. Idempotent — a second call is a no-op.

Variables

+default-dns-adapter-name+

The seam name the controller binds the view-resolving handler under when a caller names none. valis names no concrete wire engine; this is only the registry key the bind and its teardown agree on.

Package valis/src/edge/dns-serve

Classes

dns-serve-handle

A handle to a running :53 serve loop, returned by start-dns-serve and consumed by stop-dns-serve. NAME is the admitted DNS adapter name this loop serves under (the key it is recorded under in dns-serve-handles, so a name-keyed retire can find it); PROTOCOL is the registered dns-serve-protocol; TCP-SOURCE and UDP-SOURCE are the two steered event sources; TCP-EXECUTOR is the budget-executor gating the TCP source's concurrency, held so teardown can drain its in-flight connections off its live counter; MUX is the multiplexer they are registered on; PORTS is the list of ports the protocol claims.

Conditions

c3po-serving-codec-unavailable

Signalled when c3po's serving codec is absent or its contract version skews from valis's. The fail-closed codec guard, asserted once at wire time so the hot path never reaches an unresolved codec.

runciter-serve-contract-version-mismatch

Signalled by %runciter-make-serve-exchange when valis's adapter contract version and runciter's serving contract version disagree. The fail-closed seam-version guard: a version skew across the repo boundary is refused, never silently bridged.

runciter-serve-exchange-unavailable

Signalled by %runciter-make-serve-exchange when runciter has not exported its serving-exchange constructor. The fail-closed seam guard: valis never answers DNS from its own composition, so an absent constructor refuses the exchange outright rather than fabricating a response.

Functions

dns-serve-handle-ports

(dns-serve-handle-ports instance)

Undocumented: this exported symbol needs a docstring.

retire-dns-serve

(retire-dns-serve &key (name +default-dns-adapter-name+) (drain-timeout *dns-serve-drain-timeout-seconds*))

Retire the live :53 serve loop admitted under NAME - the teardown-on-revoke mirror of retire-edge-adapter. Deregister both steered sources, drain in-flight workers (bounded), close both sources, unregister the serve protocol (dropping the captured serving handler from the hot path), forget the handle, and unbind the DNS view (release the admitted handler). Revoking the DNS module's owner-vouched capability runs this: the serve loop, finding no admitted handler, fails closed structurally with no per-query gate on the hot path, and valis composes nothing on that path - UDP silent, TCP closes, never a fabricated REFUSED or answer. Idempotent - a NAME with no live handle still unbinds the view.

Returns the list of retired ports, or NIL when no serve loop was wired under NAME. Signals RETIREMENT-INCOMPLETE when any release did not happen.

The view unbind is the LAST release in the revocation path and it is attempted even when the sources' teardown failed, so a wedged descriptor cannot keep the admitted handler alive. A serve loop whose teardown failed keeps its registry entry and its ports are not reported as retired: the handle still describes something live, and a second retire picks it up again.

start-dns-serve

(start-dns-serve &key udp-fd tcp-fd udp-port tcp-port (name +default-dns-adapter-name+) (mux (and *listener* (listener-mux *listener*))) (make-exchange (function %runciter-make-serve-exchange)) (make-tcp-source (function make-steered-edge-stream-source)) (make-udp-source (function make-steered-edge-datagram-source)) zone-source)

Wire the two steered :53 descriptors — a UDP datagram fd on UDP-PORT and a TCP stream fd on TCP-PORT — behind ONE admitted DNS handler and serve both over MUX. Returns a dns-serve-handle for teardown, or NIL when no DNS handler is admitted under NAME (fail closed: no handler => wire nothing).

The admitted handler is captured HERE, at wire time: the serving exchange is constructed ONCE over it (MAKE-EXCHANGE, defaulting to the runciter serving-exchange constructor resolved late) and closed over by the serve protocol, so the hot path holds the handler with no per-query admission consult (mirroring start-edge capturing its protocol). After a later teardown the protocol is unregistered and holds no handler.

The two transports take deliberately different concurrency paths against a stranger-facing port. TCP reuses the edge budget-executor: a connection-oriented port with a blocking read, it gets the proven per-port concurrency cap (at cap the listening source is deregistered and further connections wait in the kernel backlog) plus the live counter teardown drains on. UDP takes an inline executor: a datagram arrives already fully in hand, so its one-shot exchange runs on the mux thread with no per-datagram thread and no lifecycle. Neither transport can be driven into unbounded thread creation by a packet or connection flood.

The ports the serve protocol registers under — and the loop's teardown identity — are recovered from the two descriptors themselves, not from the UDP-PORT/TCP-PORT arguments (which the boot path may leave nil even while handing over live fds). The recovered port is exactly what a connection reports as its destination-port, so registering under it is what lets the registry route a query at all.

Fail-closed, no partial state: the c3po serving codec is version-checked before any source is built, each source is tracked the moment it is built, and the protocol is registered only once both sources exist, so a failure building the second source, recovering a port, or registering tears the first down — never one transport serving while the other failed. MAKE-EXCHANGE and the two source constructors are injectable so a test can drive the wiring against a stub exchange and force the second-source failure that proves the unwind.

The zone-data-source the TCP AXFR path streams off is read from the admitted adapter spec (the source the DNS view was bound over, in production a pg-zone-source) and captured on the serve protocol, so a query on an AXFR name reaches the same zone the ordinary exchange answers from. ZONE-SOURCE overrides that read so a test can inject a stub; NIL leaves an AXFR request failing closed rather than routing to the transfer path.

stop-dns-serve

(stop-dns-serve handle)

Tear down a :53 serve loop wired by start-dns-serve, symmetric to it and error-isolated: deregister both sources from the mux (stop feeding the seam first), close both sources, then unregister the serve protocol so the captured handler is dropped, and forget the handle. Each step tolerates an already-gone piece. Does NOT drain and does NOT unbind the DNS view — the boot path and the module-delete retire own the view's lifecycle. A NIL HANDLE is a no-op (nothing was wired).

Variables

*dns-serve-drain-timeout-seconds*

The bounded wall-clock ceiling retire-dns-serve waits for a port's in-flight :53 connection workers to quiesce after both steered sources are deregistered. A wedged worker cannot hang the teardown: once this many seconds elapse the retire proceeds regardless of any residual worker, so the drain is bounded, never an unbounded block. Mirrors the edge controller's edge-drain-timeout-seconds.

Package valis/src/edge/liveness

Classes

liveness-mixin

The clock state one session carries, mixed into the session class rather than held beside it so there is exactly one place to look for what a session costs and one place for the close path to withdraw.

Two timers are armed per session at the point liveness starts: the repeating idle timer and the one-shot registration timer. A third, the probe deadline, exists only while a probe is unanswered. Anything measuring what a session weighs should count from here.

liveness-policy

What one session's two clocks are set to.

⛔ The two are structurally different mechanisms, not two settings of one, and this is the thing a later reader will otherwise collapse. The REGISTRATION timeout runs from the moment the connection was established and is NEVER reset by activity. The IDLE timeout runs from the last activity and is paired with a probe the protocol supplies.

They answer different questions. The idle clock asks whether the peer is still there; the registration clock asks whether it has done what it connected to do. A peer that talks continuously without ever registering is exactly the case the registration clock exists to catch, and an idle clock can never catch it, because such a peer is never idle. Fold the registration timeout into the idle timeout and that peer holds a pre-registration slot for as long as it cares to keep typing.

Generic functions

liveness-cut

(liveness-cut session reason)

End SESSION for REASON. What ending means belongs to whoever owns the session and its descriptor; the clocks decide only when. REASON is :LIVENESS-TIMEOUT when a probe went unanswered and :REGISTRATION-TIMEOUT when a peer never registered.

liveness-idle-interval

(liveness-idle-interval object)

Undocumented: this exported symbol needs a docstring.

liveness-live-p

(liveness-live-p session)

Whether SESSION is still open. Every timer body asks before acting, because a session can end between a timer being scheduled and its body running and there is nothing left to cut.

liveness-mux

(liveness-mux session)

The multiplexer SESSION's clocks are armed on. Answering NIL arms nothing, which is what makes liveness something a session opts into rather than something every session pays for.

liveness-probe-deadline

(liveness-probe-deadline object)

Undocumented: this exported symbol needs a docstring.

liveness-protocol

(liveness-protocol session)

The protocol that supplies SESSION's probe. Answering NIL means there is nobody to ask, and is treated exactly as a protocol with no probe is.

liveness-registration-timeout

(liveness-registration-timeout object)

Undocumented: this exported symbol needs a docstring.

send-liveness-probe

(send-liveness-probe protocol session)

Ask the peer of SESSION to prove it is still there, in whatever PROTOCOL's own vocabulary that takes. Answer true when a probe was sent and NIL when this protocol has none.

⛔ A protocol that answers NIL has no probe, and a session with no probe gets NO IDLE CUT AT ALL. The idle clock withdraws itself rather than falling back to a plain deadline, and the reason has to be said out loud because the opposite is the tempting default: cutting a session that cannot be probed reinstates a wall-clock cut on exactly the connection that is idle by design, which is the mechanism this file exists to replace. Such a session ends when its peer goes away, when its send queue breaches, or when the edge tears its port down, and never on a clock.

The question is asked at the moment a probe is due rather than at adoption, because whether a protocol can probe may depend on how far the session has got: an exchange that has not yet completed registration may have no well-formed question to put.

The default answers NIL, so every protocol that says nothing keeps a session no clock will ever cut.

session-idle-timer

(session-idle-timer object)

Undocumented: this exported symbol needs a docstring.

session-last-activity

(session-last-activity object)

Undocumented: this exported symbol needs a docstring.

session-liveness-policy

(session-liveness-policy object)

Undocumented: this exported symbol needs a docstring.

session-probe-outstanding-p

(session-probe-outstanding-p object)

Undocumented: this exported symbol needs a docstring.

session-probe-timer

(session-probe-timer object)

Undocumented: this exported symbol needs a docstring.

session-registered-p

(session-registered-p object)

Undocumented: this exported symbol needs a docstring.

session-registration-timer

(session-registration-timer object)

Undocumented: this exported symbol needs a docstring.

Functions

make-liveness-policy

(make-liveness-policy &rest initargs &key idle-interval probe-deadline registration-timeout)

Build a policy. Every interval omitted is taken from its parameter, so the shipped figures are changed by binding those rather than by editing a default into this class.

note-liveness-activity

(note-liveness-activity session)

Something arrived on SESSION. Reset the idle clock and answer SESSION.

⛔ This never touches the registration clock. That clock measures from establishment, and a version of this that reset it too would let a peer stay unregistered for as long as it kept talking, which is the denial of service the two clocks are kept apart to prevent.

note-liveness-response

(note-liveness-response session)

The protocol recognised an answer to a probe. Withdraw the deadline, reset the idle clock, and answer SESSION.

The edge cannot recognise an answer itself, because what counts as one is the protocol's own vocabulary and the edge does not read the exchange it carries. A protocol for which any traffic at all proves the peer is there says so by calling this from its receive path; one for which only a particular reply counts calls it only then.

note-session-registered

(note-session-registered session)

The peer completed what the protocol calls registration. Withdraw the registration clock and answer SESSION.

It withdraws that clock and nothing else. A session that has registered is still subject to the idle clock, because having registered says nothing about whether the peer is still there.

start-liveness

(start-liveness session policy)

Arm SESSION's clocks from POLICY and answer SESSION. A NIL policy arms nothing at all.

Must be called on the loop thread, because scheduling mutates state the loop owns while it is dispatching. Both clocks start here, in one call, so a session's cost in timers is decided in one place and a close path has one call that undoes it.

stop-liveness

(stop-liveness session)

Withdraw every clock SESSION armed and answer SESSION. Idempotent, and safe to call on a session that armed none.

The close path calls this before it releases the descriptor, and the ordering is the whole reason it exists: a timer body that fired against a descriptor the close path had already released is the exact failure the request-shaped read-deadline watchdog carries a lock and a closing flag to prevent, and the same care is owed here.

Variables

*liveness-idle-interval*

Seconds of quiet on a session before its peer is asked to prove it is still there.

Provisional, and it says so because nothing has profiled this system yet. It is deliberately long: on a continuous protocol idleness is the normal state rather than a symptom, so the interval sets how quickly a peer that vanished without a final packet is noticed, and buys that promptness with one probe per session per interval. No figure here is carried over from another implementation. A number that suits a daemon serving one kind of traffic says nothing about this one, and a default that appears to be a convention often turns out to apply only where no configuration matched.

*liveness-probe-deadline*

Seconds a peer has to answer a probe before it is treated as gone.

Provisional. It has to clear a slow round trip and the peer's own scheduling by a wide margin, because the cost of being wrong is a live connection cut, while the cost of being generous is that a dead peer is noticed one deadline later than it might have been. That asymmetry is why this is the interval that gets the benefit of the doubt.

*liveness-registration-timeout*

Seconds from establishment within which a peer must complete whatever its protocol calls registration.

Provisional. It is deliberately far shorter than the idle interval, and the gap is the point rather than a coincidence: a session that has connected and not yet registered is holding a slot with nothing to show for it, and keeping the two figures well apart means an observed cut says which clock made it without anybody having to reconstruct the timing.

Package valis/src/edge/mail-serve

Classes

mail-serve-handle

A handle to a running :25 serve loop, returned by start-mail-serve and consumed by stop-mail-serve / retire-mail-serve. NAME is the admitted name; PROTOCOL is the serve protocol registered with the port->protocol registry; SOURCE is the event source registered on the mux; EXECUTOR is the budget-executor whose live counter the drain polls; MUX is the multiplexer SOURCE is registered on; PORTS is the list of ports recovered from the descriptor and registered under.

smtp-serve-protocol

The inbound :25 SMTP serve protocol both the inherited-fd and the loopback dev-fallback paths dispatch to. Its handle-connection drives one RFC 5321 session through c3po's engine, threading valis's store, fenced head, and the per-listener callout cache into the mail seam. Like tls-edge-protocol / dns-serve-protocol it is directly registered under the descriptor's recovered port rather than installed as an edge-adapters entry — the :25 leg is a stranger-facing serve loop, never a loopback edge port.

Generic functions

smtp-serve-protocol-callout-cache

(smtp-serve-protocol-callout-cache object)

Undocumented: this exported symbol needs a docstring.

smtp-serve-protocol-callout-probe

(smtp-serve-protocol-callout-probe object)

Undocumented: this exported symbol needs a docstring.

smtp-serve-protocol-head

(smtp-serve-protocol-head object)

Undocumented: this exported symbol needs a docstring.

smtp-serve-protocol-store

(smtp-serve-protocol-store object)

Undocumented: this exported symbol needs a docstring.

Functions

bind-mail-config

(bind-mail-config)

Bind the mail router / RCPT-disposition / MX-resolver specials from the operator-state config resolvers. Each resolver is env-sourced and fail-closed to NIL/empty when unset, so an unconfigured boot leaves each seam at its fail-closed posture. The accept-list keeps its module default when the resolver names none, so an unset accept-list never widens acceptance to a catch-all. Returns no values.

mail-serve-handle-ports

(mail-serve-handle-ports instance)

Undocumented: this exported symbol needs a docstring.

reset-mail-config

(reset-mail-config)

Reset the mail router / RCPT-disposition / MX-resolver specials to their fail- closed defaults — no secondary authority, no configured DoT upstream (so the drain defers every relay), and the module's named-localpart accept-list. The reset twin of bind-mail-config, run on every fabric teardown path so the mail seams never outlive an activation. Returns no values.

retire-mail-serve

(retire-mail-serve &key (name +default-mail-edge-name+) (drain-timeout *mail-serve-drain-timeout-seconds*))

Retire the live :25 serve loop admitted under NAME — deregister the source, drain in-flight workers (bounded by DRAIN-TIMEOUT), close the source, unregister the serve protocol, and forget the handle. Each step is error-isolated. Idempotent — a NAME with no live handle is a no-op. Returns the list of retired ports, or NIL when no serve loop was wired under NAME.

start-mail-serve

(start-mail-serve &key tcp-fd tcp-port store head callout-probe (name +default-mail-edge-name+) (mux (and *listener* (listener-mux *listener*))) (make-tcp-source (function make-steered-edge-stream-source)))

Wire the inbound :25 SMTP edge over an inherited TCP-FD (fulcrum steer) or, when no fd is inherited, a loopback self-bind on TCP-PORT (dev fallback), threading STORE and HEAD into c3po's engine, and serve it over MUX under NAME. Returns a mail-serve- handle for teardown, or NIL when STORE or HEAD is absent (store-gated open: no source is built and no protocol registered, so the :25 leg never opens with nowhere to land mail).

The edge is the structural sibling of start-tls-serve for ONE stream source. With an inherited TCP-FD the source is adopted through MAKE-TCP-SOURCE (defaulting to make-steered-edge-stream-source) with no second bind; with no fd the dev fallback self-binds TCP-PORT on loopback via make-stream-source (a TCP-PORT of 0 binds an ephemeral port). Either way the port the protocol registers under — and the loop's teardown identity — is recovered from the descriptor itself, not from TCP-PORT (which the boot path may leave nil even while handing over a live fd): the recovered port is exactly what a connection reports as its destination-port, so registering under it is what lets the registry route a session at all.

The single stream source reuses the edge budget-executor: a stranger-facing connection-oriented port gets the proven per-port concurrency cap (at cap the listening source is deregistered and further connections wait in the kernel backlog) plus the live-counter teardown drain. ONE make-callout-cache is constructed here and captured on the protocol, so every session run this listener drives shares one bounded-TTL callout cache.

Fail-closed, no partial state: the store gate runs before any source is built, the source is tracked the moment it is built, and the protocol is registered only once the source exists and its port is recovered — so a failure recovering the port or registering tears the source down rather than leaving a half-wired listener. The fd borrow/close discipline is inherited from make-steered-edge-stream-source and the teardown path: valis closes only the source it built (its own descriptor), never the inherited fd. MAKE-TCP-SOURCE is injectable so a test can force a second-stage failure that proves the unwind.

stop-mail-serve

(stop-mail-serve handle)

Tear down a :25 serve loop wired by start-mail-serve, symmetric to it and error- isolated: deregister the source from the mux (stop feeding the seam first), close the source (valis's own descriptor, never the inherited fd), then unregister the serve protocol, and forget the handle. Each step tolerates an already-gone piece. Does NOT drain. A NIL HANDLE is a no-op (nothing was wired).

Variables

*mail-serve-handles*

Name -> MAIL-SERVE-HANDLE for the live :25 serve loops, keyed by the admitted name (STRING-normalized). start-mail-serve records its handle here on success; stop-mail-serve and retire-mail-serve read it back to tear a loop down by name.

+default-mail-edge-name+

The default name the :25 SMTP edge wires under and is keyed by in mail-serve-handles, so a name-keyed retire can reach the live loop.

Package valis/src/edge/owner-client

Conditions

zone-not-served

Signalled when an owner verb is asked to act on a zone the RUNNING node does not serve. Distinct from a server refusal and from an unreachable resident, because the three call for three different corrections and a caller that cannot tell them apart gives the operator the wrong one.

Generic functions

zone-not-served-origin

(zone-not-served-origin condition)

Undocumented: this exported symbol needs a docstring.

Functions

add-record

(add-record origin owner ttl type rdata &key keyfile endpoint)

Add ONE durable rrset member (OWNER TTL TYPE RDATA) to zone ORIGIN as the owner over the sealed loopback fabric. TYPE is a record type ("A", "MX", :txt, …); RDATA is its presentation text (runciter types it server-side). A rejected add (a CNAME conflict, a name outside the zone, a second apex SOA, malformed rdata) surfaces as a signalled condition. Returns the door's reply text, whose first line says whether the durable edit also reached the serving side.

call-with-owner-session

(call-with-owner-session fn &key keyfile endpoint (aname ""))

Open an owner-authenticated sealed 9P session and funcall FN with (SESSION ROOT-HANDLE) — the promoted seven session and the attached owner-frame root. Resolves the endpoint and the owner keyfile, builds a mercer initiator over the owner key, drives the NoiseXX handshake + Tattach, and promotes to a continuously-sealed session. Tears the session and the socket down on any exit (clunk every handle, then close the carrier). valis performs no crypto.

create-zone

(create-zone origin instance-ip peer-address &key fields keyfile endpoint)

Create the domain ORIGIN, served from INSTANCE-IP with the secondary at PEER-ADDRESS enrolled as its transfer peer, through the RUNNING resident's owner control plane over the sealed loopback fabric. Walks to names/zone/create, opens read-write, writes the newline-framed create payload, reads the resident's reply, and clunks. FIELDS carries whichever template parameters and typed override records differ from the defaults. Returns the resident's reply text: line 1 is a result token from a closed set, then one key/value line per field, then a blank line and the BIND fragment the secondary side pastes verbatim. The create mints the record set and enrols the secondary as ONE act, so a refusal leaves neither; a refusal surfaces as the signalled condition the clunk raises, which the operator tool prints before exiting non-zero. There is no field that accepts a zone file: a create carries typed values only, so a typo is a failed construction rather than a plausible wrong record.

delete-record

(delete-record origin owner type rdata &key keyfile endpoint)

Delete ONE durable rrset member (OWNER TYPE RDATA) from zone ORIGIN as the owner over the sealed loopback fabric. The member is identified precisely by its rdata; a co-resident sibling at the same name is untouched. A delete that would strand the zone without its apex SOA or NS is refused (a signalled condition). Returns the door's reply text, whose first line says whether the durable edit also reached the serving side.

delete-zone-remote

(delete-zone-remote origin &key keyfile endpoint)

Delete the zone ORIGIN (whole-zone removal) as the owner over the sealed loopback fabric. Drives Tremove against the per-zone node (names/<origin>); a zone that does not exist fails the walk with an Rerror. Returns T on success.

enroll-transfer-peer

(enroll-transfer-peer origin peer key-name &key notify-ref keyfile endpoint)

Authorize PEER, presenting the TSIG key named KEY-NAME, to transfer the zone ORIGIN from the RUNNING node, and add NOTIFY-REF (defaulting to PEER) to that zone's also-notify recipients. Both facts are written as the owner over the sealed loopback fabric, so they land through the resident's fenced control path rather than around it.

One session, two commits. The axis control node is created fresh per walk and its clunk IS the commit, so each verb is its own walk, open, write and clunk on one owner session. A failure between the two leaves the allowlist row without its also-notify recipient: a peer that may pull but is not told when to, which is benign and is repaired by re-running. Both verbs are idempotent, so re-running an enrolment that already landed changes nothing.

A zone this node does not serve is refused BY NAME before either verb runs, and a token the server will not accept as a DNS name comes back as a signalled condition with nothing written. Returns T on success.

export-zone-master

(export-zone-master origin &key full keyfile endpoint)

Return the zone ORIGIN's master text as a string, read as the owner over the sealed loopback fabric. By default the DURABLE, re-importable master (names/durable/<origin>); with FULL true, the full serving set the per-zone master node renders (names/<origin>/master), which includes any transient turn-up rows. The per-zone node itself is a directory, so the master child is the only node that reads back as text. A zone that does not exist fails the walk with an Rerror.

fence-session-remote

(fence-session-remote session-id &key keyfile endpoint)

Fence the session SESSION-ID on the RUNNING node, as the owner over the sealed loopback fabric, and return the node's reply as a string. Walks to id/session-ctl, opens read-write, writes the one command line, and reads the answer back on that same handle before clunking, because this door answers on the handle and never signals into the 9P layer. The reply is returned verbatim and nothing here interprets it: a fence that took and a fence that named no live session are both real answers to a caller who asked, and turning either into a signalled condition would erase a contract the caller needs. A fence empties the named session's view, so it can name nothing from its root until it authenticates again; it does NOT take back a handle that session already holds below its root, so a caller checking the effect must walk from the session root afresh rather than reuse a handle it opened earlier.

import-zone-master

(import-zone-master origin master-text &key keyfile endpoint)

Import MASTER-TEXT — RFC-1035 zone master file text — as the zone ORIGIN, as the owner over the sealed loopback fabric. Drives create -> write -> clunk against the :names axis; the clunk IS the atomic commit, so a malformed master returns an Rerror that surfaces here as a signalled condition (a non-zero exit for the operator tool). Returns T on success.

list-records

(list-records origin &key keyfile endpoint)

Return the DURABLE records of zone ORIGIN as a list of plists, each (:digest :owner :ttl :class :type :rdata), read as the owner over the sealed loopback fabric from names/<origin>/records/list — the structured editing view with a stable per-member :digest identity the delete/replace verbs address a member by. :ttl is an integer; the rest are strings. A zone that does not exist fails the walk with an Rerror.

list-sessions-remote

(list-sessions-remote &key keyfile endpoint)

Return the live sessions of the RUNNING node as a list of plists, each (:id :principal :admitted-at :last-activity :fenced-at), read as the owner over the sealed loopback fabric from id/sessions. :admitted-at and :last-activity are integer seconds, :fenced-at is an integer for a session already fenced and NIL for one still live, and the rest are strings. The listing is rendered afresh on every read and the node advertises no length for it, because any length it advertised would be a number that was true a moment ago. This verb opens a session of its own to ask, so the node counts it among the sessions it reports. Returns the sessions in the order the node listed them.

management-endpoint-file

(management-endpoint-file)

The path of the loopback management endpoint file (a single host:port line).

obtain-certificate

(obtain-certificate domains contact &key profile directory-url keyfile endpoint)

Drive an ACME first-obtain for DOMAINS with account CONTACT through the RUNNING resident's owner control plane, over the sealed loopback fabric. Walks to names/cert/obtain, opens read-write, writes the newline-framed obtain payload, reads the resident's reply, and clunks. The obtain runs IN the resident that serves :53 so the dns-01 challenge TXT is refreshed into the serving index by the in-process commit hook — a challenge published from any other process is never served, so a standalone obtain returns invalid. Returns the resident's reply text: line 1 is the token obtained, then one domain line per issued identifier and a next-renewal line. A refused or failed order surfaces as the signalled condition the clunk raises (a non-zero exit for the operator tool). valis performs no crypto.

owner-transport-key

(owner-transport-key &key keyfile)

The owner's X25519 transport key, read from the resolved keyfile. An absent keyfile is a fail-closed error — this NEVER creates one, so a missing key can never silently mint a different (non-owner) identity.

publish

(publish slug content-type body &key keyfile endpoint)

Place the publication SLUG into /pub on the RUNNING node, as the owner over the sealed loopback fabric. CONTENT-TYPE is the media type the edge serves it as; BODY is the content as octets. Walks to pub, creates the slug when it is not there yet, then writes the content-type attribute and the body. A slug that already exists is replaced rather than refused, so an operator can revise a page as often as they like; the create is skipped and the two writes stand in for it. A slug outside the publication charset, or any other refusal, comes back as the Rerror the fabric raises, which surfaces here as a signalled condition (a non-zero exit for the operator tool). Returns T on success.

remove-management-endpoint

(remove-management-endpoint)

Unlink the management endpoint file if present — the resident's teardown so a stale endpoint never outlives the listener that bound it.

replace-record

(replace-record origin owner type old-rdata new-ttl new-rdata &key keyfile endpoint)

Replace ONE durable rrset member at (OWNER TYPE OLD-RDATA) with (OWNER TYPE NEW-TTL NEW-RDATA) in zone ORIGIN as the owner over the sealed loopback fabric — an atomic delete+add of one rrset member, the TYPE unchanged. The old member must exist; its absence is refused (a signalled condition). Returns the door's reply text, whose first line says whether the durable edit also reached the serving side.

resolve-keyfile

(resolve-keyfile keyfile)

Resolve the owner keyfile path: the KEYFILE argument (from –keyfile), then the VALISKEYFILE environment variable, then the data-root keyfile. A PATH only — the key bytes never cross argv.

resolve-management-endpoint

(resolve-management-endpoint &key endpoint)

Resolve the management endpoint to (values host port), in precedence order: the ENDPOINT argument (a "host:port" string, from –endpoint), then the VALISMGMTENDPOINT environment variable, then the runtime endpoint file the resident published. Fail-closed when none is available.

write-management-endpoint

(write-management-endpoint host port)

Publish the resident's bound loopback management endpoint HOST:PORT to the runtime file at 0600, creating the parent directory. Returns the path. The same-host operator client reads this to reach the owner frame without a fixed port.

Macros

with-owner-session

(with-owner-session (session-var root-var &key keyfile endpoint (aname "")) &body body)

Bind SESSION-VAR and ROOT-VAR to the owner session and owner-frame root for the dynamic extent of BODY, tearing both down on exit. Thin sugar over call-with-owner-session.

Package valis/src/edge/seam

Classes

edge-attach

A live 9P attach held by the seam on behalf of edge connections. SESSION is the seven client session; ROOT-HANDLE is the attach's root fid handle, the view everything else is walked from; TEARDOWN is the channel teardown thunk returned by the factory that built the transport underneath the session.

Functions

anonymous-view-dark-p

(anonymous-view-dark-p)

Return T when the anonymous view is dark and must not be served: its grant was revoked, or its standing could not be read at all. Either way self-heal will not resurrect it until the grant reads live again. A dark anonymous view serves the protocol's own absence response; the edge never synthesizes policy. ANONYMOUS-VIEW-DARK-REASON says which of the two it was, for an operator; the decision to refuse does not depend on that.

anonymous-view-dark-reason

(anonymous-view-dark-reason)

Return (values reason detail) naming why the anonymous view is dark: :REVOKED, or :UNDETERMINED with the condition's report text as DETAIL. Returns (values nil nil) while the view is serving. Callers deciding whether to serve read ANONYMOUS-VIEW-DARK-P; this is for telling an operator which fault to go and fix.

call-with-request-capability

(call-with-request-capability function capability-string &key principal)

Functional core of WITH-REQUEST-CAPABILITY: call FUNCTION with the root handle of a PRIVATE per-request view, and tear that view down when FUNCTION returns or unwinds.

For the anonymous principal the view is a fresh anonymous-grant attach with CAPABILITY-STRING mounted through its ctl door; teardown unmounts the leaf then closes the private session and channel.

For a keyed principal the view is the base namespace BUILD-BASE-VIEW resolves — the single owner-discrimination site: the owner DID gets its canonical sovereign frame, a non-owner keyed DID gets an empty view that names nothing (sovereignty-as-absence). CAPABILITY-STRING is not mounted on this path; the resolved frame is the view, served read-only. Either way the view rides the same per-request unwind-protected teardown, so the owner view is torn down with its request (stateless elevation) and no owner session outlives a keep-alive connection.

edge-attach-root-handle

(edge-attach-root-handle instance)

Undocumented: this exported symbol needs a docstring.

edge-attach-session

(edge-attach-session instance)

Undocumented: this exported symbol needs a docstring.

normalize-edge-principal

(normalize-edge-principal principal)

Normalize a connection's PRINCIPAL for the seam. Nil — a plain TCP edge connection that performed no identity handshake — resolves to the anonymous sentinel, so callers above the seam always see a principal object, never nil. The principal parameter is structurally carried from day one even though every edge connection resolves anonymous today.

reset-edge-seam

(reset-edge-seam)

Close every cached edge attach, clear the cache, and clear the dark flag. Idempotent — safe to call when nothing is cached. Called by tests between fixtures and by edge teardown so a stopped fabric never leaves the seam holding sessions into a dead namespace or a dark state carried across a restart.

view-for

(view-for connection principal capabilities)

Resolve CONNECTION's namespace view from its PRINCIPAL and request CAPABILITIES. This is the only way an edge module reaches the sovereign core: the seam holds the sessions, the caller gets a view.

Returns (values root-handle session): the attach's root fid handle to walk from, and the seven session it belongs to. Returns NIL when the anonymous view is dark (its grant was revoked) or a self-heal could not re-establish it — the adapter then serves the protocol's own absence response, never an edge-synthesized one.

A nil PRINCIPAL normalizes to the anonymous sentinel. Anonymous callers with no request capabilities share one cached attach made with ANONYMOUS-EDGE-GRANT as the aname — the grant, verified server-side, is the sole scope of what the view contains. Signals if the grant is not set (the fabric is not up): a view must never exist ahead of the authority that scopes it.

A keyed principal resolves its base view through build-base-view, the single owner-discrimination site: the owner DID receives the canonical sovereign frame; a non-owner keyed DID receives an empty view-root — present but naming nothing, sovereignty-as-absence, never the anonymous or owner view. This branch returns a third value, a teardown thunk the caller MUST funcall at request end: the keyed view is per-request and never cached (stateless elevation), so the owner view is torn down at request end exactly like the anonymous request-scoped path and no owner session persists across a keep-alive connection.

Non-nil CAPABILITIES (one capability string or a list of them) resolve to a PRIVATE per-request view — never the shared cached attach — with each capability mounted through the view's own ctl door. This branch returns a third value, a teardown thunk the caller MUST funcall at request end; no wire protocol carries in-band capabilities yet, so its consumers are direct API callers (WITH-REQUEST-CAPABILITY wraps the same machinery with unwind protection and is the preferred entry point).

Macros

with-request-capability

(with-request-capability (root-var capability-string &key principal) &body body)

Execute BODY with ROOT-VAR bound to the root handle of a PRIVATE per-request view into which CAPABILITY-STRING has been mounted through the view's own ctl door. The shared cached anonymous attach is never touched: the request's authority exists only in this view, for exactly the dynamic extent of BODY.

Teardown is unwind-protected — on normal return or non-local exit the mounted name is unmounted (self-narrowing), then the private session is clunked and its channel closed. The mount never outlives the request.

When PRINCIPAL is a keyed (non-anonymous) principal, ROOT-VAR is bound to the base view build-base-view resolves instead — the owner's canonical frame for the owner DID, an empty view otherwise — served read-only with the same per-request teardown; CAPABILITY-STRING is not mounted on that path.

Variables

*anonymous-edge-grant*

The one edge-wide bearer name designating the public view. Every anonymous edge connection attaches with this grant as its aname, so what anonymous callers can see is controlled by exactly one knob, consistent across protocols. Set by start-fabric; cleared by stop-fabric. Nil before the fabric is up. This module owns the binding — never declare a second binding elsewhere.

*edge-channel-factory*

A function of no arguments returning two values: a client endpoint suitable for seven's MAKE-SESSION, and a teardown thunk that closes the channel ends and reaps any server thread the factory started. Defaults to an in-process channel served in-image. This is the single channel-construction point at the edge — rebind to select a different transport (e.g. seven's TCP client path, or a socketpair channel into a separate core process) without changing any call site. Tests rebind it to count constructions and to prove the same adapter serves over a second transport with no code change.

*edge-grant-mint-fn*

A function of one argument (TTL in seconds) returning a fresh bearer name string for the public view. Installed by start-fabric; cleared by stop-fabric. The seam calls it to re-mint an expired grant during self-healing. A function-valued hook so this package never references the fabric package — the fabric is downstream and installs the function at start time, breaking what would otherwise be a package cycle.

Package valis/src/edge/send-queue

Classes

send-queue

One connection's outbound backlog, bounded by a hard byte cap.

⚠ The queue carries NO lock, deliberately. Its owner holds one, because the owner also owns the write-interest transition that has to happen with the same view of the fill as the accept did: a queue that guarded itself would make each operation atomic and the pair of them still racy. A second lock added here would buy nothing and would give a later reader two locks to reason about.

An owner therefore calls ENQUEUE and DRAIN under its own lock.

Conditions

message-exceeds-cap

The message is larger than the entire budget, so no amount of draining will ever make room for it. A producer that retries this one retries forever. It is refused on an empty queue too, which is what stops a single large write defeating the bound.

send-refused

Signalled when a queue will not accept a message. Carries the queue, the length of the message turned away and the cap it was measured against, so a report can state all three rather than saying only that a send failed.

A caller that will not retry handles this type and needs no more. A caller that will retry handles the two subtypes, because they differ in whether retrying can ever work.

send-would-exceed-cap

The message fits the cap but the queue does not have room for it right now. Draining changes this: the same message offered again after the peer has read can be accepted, so a producer that can wait should wait.

Generic functions

queued-bytes

(queued-bytes object)

Undocumented: this exported symbol needs a docstring.

send-queue-cap

(send-queue-cap object)

Undocumented: this exported symbol needs a docstring.

send-refused-cap

(send-refused-cap condition)

Undocumented: this exported symbol needs a docstring.

send-refused-message-length

(send-refused-message-length condition)

Undocumented: this exported symbol needs a docstring.

send-refused-queue

(send-refused-queue condition)

Undocumented: this exported symbol needs a docstring.

Functions

drain

(drain queue writer)

Write as much of QUEUE as WRITER will take. Answers two values: how many octets left, and whether the queue is now empty.

WRITER is called with a message and the bounds of its unwritten part, and answers how much of that it actually took. The count is treated as possibly short, which is the normal case against a peer that is slow to read, and the offset advances by exactly what left so no byte is written twice and none is skipped.

The drain stops as soon as WRITER answers anything other than a positive count. That covers both a descriptor with no room and a descriptor that has gone away: neither is this function's to distinguish, and the connection's read side reports an ending either way.

The emptiness answer is what the caller withdraws its write interest on. A caller that ignores it leaves the loop watching a descriptor that is permanently ready to accept output, and spins.

enqueue

(enqueue queue octets)

Accept OCTETS for later writing on QUEUE, or refuse them. Answers QUEUE.

The cap is tested against the length the queue WOULD have, before anything is buffered. Testing the length it already has and appending afterwards would bound it at the cap plus one whole message, and nothing on this path caps a message, so that placement bounds nothing at all.

⛔ QUEUE COPIES OCTETS here, at the moment it accepts them, and the caller keeps its vector and may reuse or mutate it as soon as this returns. The queue holds bytes across callback boundaries, because a short write leaves the tail of a message queued indefinitely, and the first obvious use of this whole seam is sending straight out of a session's read buffer, which is reused on the next read. Taking ownership of the caller's vector instead would put the wrong bytes on the wire silently, under load, and only on a short write. The counterpart rule is stated on the session's receive callback; the two are one contract and must agree.

The cost is one allocation per accepted message, and it is deliberately a visible cost rather than a hidden one: it is what the per-queued-message figure reports.

Refuses with MESSAGE-EXCEEDS-CAP when the message alone is larger than the cap, which draining can never fix, and with SEND-WOULD-EXCEED-CAP when it is the sum that is too large, which draining can. An empty message is accepted and holds nothing.

make-send-queue

(make-send-queue &key (cap *default-send-queue-bytes*))

An empty queue bounded at CAP octets, defaulting to the provisional figure this file declares.

queue-empty-p

(queue-empty-p queue)

Whether QUEUE has nothing left to write.

queue-pressure

(queue-pressure queue)

How full QUEUE is, as a fraction of its cap: 0 when empty and 1 when full.

This is the first line of backpressure. A producer generating more than one message consults it between chunks and stops at SUSPEND-FRACTION, which is how the cap stays a last resort rather than the only thing telling a producer to slow down.

Variables

*default-send-queue-bytes*

How many octets one connection may hold for a peer that is not reading.

Provisional, and it says so because nothing has profiled this system yet. It is expressed as a small multiple of the carrier frame ceiling this tree already declares, and it is derived from that bound rather than adopted from any other implementation's default: a number carried over from a daemon serving short text lines says nothing about a system whose messages are whatever a module puts on the wire.

What it trades is memory resident per slow reader against how long a burst survives before the connection is cut. Raising it lets a peer that stalls briefly catch up; it also lets every simultaneously stalled peer hold that much. A measured figure replaces this one.

*suspend-fraction*

The fill, as a fraction of the cap, at which a bulk producer should stop and wait for the queue to drain.

The cap is the last resort and this is the first. Reaching the cap costs the connection, so a producer that generates more than one message at a time consults QUEUE-PRESSURE between chunks and suspends here, and never reaches the kill at all. A producer that does not consult it is not wrong, only brittle: it discovers the bound by being cut.

Package valis/src/edge/session

Classes

connection-source

An ESTABLISHED connection presented to the multiplexer as an event source. Readiness on a listening source means a connection is waiting to be accepted; readiness here means bytes are waiting to be read, which is the whole of what this file adds to the loop's vocabulary.

edge-session

One established connection driven by the event loop. It owns its descriptor, its read buffer and its registration, and it hands its protocol nothing but octets and itself. The protocol never sees a descriptor, which is what keeps a module rebuildable from durable state and a peer reconnect.

Conditions

send-on-closed-session

Signalled when a send is offered to a session that has already ended.

It is a refusal rather than whatever the operating system says about a descriptor that is gone, because a caller cannot avoid the race: a session can end between a module deciding to write and the write happening, and the module may not have been told yet. Refusing it as a send-refused means one handler covers every way a send fails to land, and a caller that needs the difference can still ask which one it was.

session-adoption-refused

Signalled when a connection is offered for loop-driven adoption and one of the clauses refuses it. Carries the connection, the protocol claiming its port, and the CLAUSE that refused, so a caller falling back to the thread path can say which invariant it fell back on rather than reporting a bare failure.

The clause is one of :STREAM-DISCIPLINE, the protocol has not declared that it drives the raw descriptor; :CONNECTION-CLASS, the object offered is not the accepted stream connection; :SESSION-CEILING, no bound on live sessions is in force. Refusal is the ordinary outcome and every adapter shipped today produces one.

Generic functions

connection-source-session

(connection-source-session object)

Undocumented: this exported symbol needs a docstring.

protocol-stream-discipline

(protocol-stream-discipline protocol)

Answer how PROTOCOL expects the bytes of an accepted connection to reach it: :BUFFERED-STREAM, the default, or :RAW-DESCRIPTOR.

A protocol answering :RAW-DESCRIPTOR is declaring that it drives the accepted descriptor itself and hands nothing above it. Only such a protocol may have its connections driven from the event loop, and the declaration is the ONLY thing that opens that path.

An adapter that wraps or terminates the stream cannot honestly declare it. Readiness is asked of the descriptor while the wrapper holds transformed bytes above it, so the two disagree: the loop is told there is nothing to read while the wrapper is holding a whole request, or told there is something while the descriptor's bytes are meaningless on their own. On a port whose stream is terminated the failure is worse than a stall, because driving the descriptor puts the exchange on the wire in the clear.

The default is the safe answer, so every adapter that says nothing keeps exactly the thread shape it has today and no existing adapter changes behaviour by this file existing.

session-adoption-refused-clause

(session-adoption-refused-clause condition)

Undocumented: this exported symbol needs a docstring.

session-adoption-refused-connection

(session-adoption-refused-connection condition)

Undocumented: this exported symbol needs a docstring.

session-adoption-refused-protocol

(session-adoption-refused-protocol condition)

Undocumented: this exported symbol needs a docstring.

session-closed

(session-closed protocol session reason)

Tell PROTOCOL that SESSION has ended and why. Called exactly once per session, whatever ended it, and carrying the first reason given.

REASON is one of a closed set, enumerated here so it cannot drift across the files that signal it: :PEER-CLOSE, the peer went away in an orderly manner; :TRANSPORT-ERROR, the descriptor or the protocol failed and the session was cut; :SHUTDOWN, this side ended it deliberately; :SEND-QUEUE-OVERFLOW, the peer stopped reading for long enough that this side ran out of the budget it is willing to hold for it; :LIVENESS-TIMEOUT, the peer was asked to prove it was still there and did not answer inside the deadline; :REGISTRATION-TIMEOUT, the peer never completed what the protocol calls registration within the time allowed from establishment. Later work adds to this set by extending this enumeration and nowhere else.

The default does nothing.

session-connection

(session-connection object)

Undocumented: this exported symbol needs a docstring.

session-mux

(session-mux object)

Undocumented: this exported symbol needs a docstring.

session-opened

(session-opened protocol session)

Tell PROTOCOL that SESSION is registered and live. Runs on the loop thread, after the session's descriptor is being watched, so a protocol that greets its peer has somewhere to send. The default does nothing.

session-protocol

(session-protocol object)

Undocumented: this exported symbol needs a docstring.

session-queue

(session-queue object)

Undocumented: this exported symbol needs a docstring.

session-received

(session-received protocol session buffer start end)

Hand PROTOCOL the octets of SESSION that arrived, occupying BUFFER between START and END. Runs on the loop thread, so the body carries the loop's non-blocking obligation: it must return in the time of the work it does on those bytes, never wait on anything.

The buffer is the session's OWN and is reused by the next read, so a protocol that needs to keep these bytes past the call copies them. The counterpart is written here too, so the pair lives in one place and cannot drift apart: the send path copies what it is given, so handing this buffer straight to a send is safe and needs no copy of its own.

The default does nothing.

session-source

(session-source object)

Undocumented: this exported symbol needs a docstring.

session-state

(session-state object)

Undocumented: this exported symbol needs a docstring.

Functions

adopt-connection

(adopt-connection connection protocol mux &key (liveness (make-liveness-policy)) ceiling release)

Offer CONNECTION, claimed by PROTOCOL, for driving by MUX's event loop. Answers the session on success and signals SESSION-ADOPTION-REFUSED otherwise. Must be called on the loop thread, because registration mutates state the loop owns while it is dispatching.

Three clauses are evaluated before anything is changed, so a refusal leaves the connection exactly as it arrived and the caller can still run it the ordinary way.

First, PROTOCOL must have declared the raw-descriptor discipline. This is what refuses every adapter that wraps or terminates the stream, and it is what stands between this path and serving a terminated port in the clear.

Second, CONNECTION must be exactly the accepted stream connection. This one refuses nothing that reaches here through the shipped construction sites, where every port hands over that same class; it is kept because this function is exported and a future caller can offer it anything. What it actually keeps out is a connection with no descriptor to drive, a datagram being the shape that would arrive the moment a datagram source were wired to a budget executor.

Third, a ceiling on live sessions must be in force. The one in force is CEILING, the bound the admitting party names for itself, and only when it names none does this fall back to the shipped variable. That variable ships closed, so a caller that offers a connection under no bound of its own is refused exactly as it was before this argument existed.

RELEASE is how the count that admitted this session gets given back: a thunk of no arguments, called exactly once when the session ends, whatever ended it. An admitting party that hands one in does not have to watch for the close, and cannot miss an ending it was not watching for.

Only then is the connection put in non-blocking mode and built into a session. The source is registered BEFORE the protocol is told the session opened, so a protocol that greets its peer during open is sending on a descriptor the loop is already watching rather than on one it is about to start watching.

LIVENESS is the policy the session's two clocks are set from, and defaults to one built from the shipped parameters. Pass NIL for a session that arms no clocks and so can never be ended by one. The clocks start before the protocol is told the session opened, so a protocol that completes registration during open finds a registration clock there to withdraw.

close-session

(close-session session &key (reason :shutdown))

End SESSION for REASON and answer it. Idempotent: a second call neither signals nor tells the protocol a second time, and the reason recorded is the first one given.

The order is deliberate. Every outstanding clock is withdrawn first, under the same lock that moved the state, so no timer body can fire against a descriptor this path is about to release. That is the exact failure the request-shaped read-deadline watchdog carries a lock and a closing flag to prevent, and the same care is owed here.

Then the registration is withdrawn, so no readiness callback can arrive for a descriptor that is about to be released; then the connection is closed; then whoever admitted the session is given back what it spent; then the protocol is told. A protocol learning of the end while the descriptor is still being watched would be invited to write to something already gone.

The release sits after the close and not before it because what it gives back is permission to admit a replacement, and the descriptor this one holds is not gone until the close has run. Handing that permission back early lets a port take on a new peer while this one still holds everything it was counted for, which is the bound quietly exceeded rather than enforced. It sits inside the one-shot guard for the mirror-image reason: a second close must not credit the same slot twice, or the count drifts upward by one for every reconnect until the bound means nothing. It is error-isolated, because a party that fumbles its own accounting must not be able to strand a descriptor.

session-send

(session-send session octets)

Send OCTETS to SESSION's peer. Answers SESSION, or refuses.

⛔ SESSION COPIES OCTETS before this returns, so the caller keeps its vector and may reuse or mutate it the moment the call comes back. That is what makes sending straight out of the buffer a protocol was handed on receipt safe: the buffer is the session's own and is reused by the next read, and without the copy the bytes on the wire would be whatever arrived next. The rule is stated here in the caller's terms and on the queue in the queue's, and the two are one contract.

May be called from ANY thread, and that is the property the whole seam exists for: no handler need be running on this connection for the send to land, so a connection can be written to outside the extent of the request that created it. Write interest is loop-owned state, so the request for it is routed to the loop thread rather than made here, and it is only made when the queue goes from empty to non-empty, which keeps the cross-thread hop off the path of every subsequent send.

A send that would breach the cap ends the connection with :SEND-QUEUE-OVERFLOW, and the refusal is signalled to the caller as well. The close is the edge's decision because the queue and the cap are the edge's, and a module cannot enforce a bound it does not hold. What it leaves the module to do is notice the refusal: a send is not an assertion that bytes arrived, and a module that treats it as one will believe it has told a peer something it never told it.

Variables

*edge-session-ceiling*

The fallback bound on live loop-driven sessions for a caller that names none of its own, or NIL.

NIL is the shipped value and it still REFUSES adoption. This is not an absent limit meaning unlimited; it is a closed door. A caller that offers a connection without saying what bound it is admitting it under has no bound, and admitting an unbounded number of long-lived sessions is a denial of service with extra steps.

A party that admits sessions in earnest names its own ceiling at the offer instead, and the count that enforces that ceiling lives with it rather than here. A bound edge port is such a party: it holds its own number and its own running count of what it is holding, so two ports cannot spend each other's allowance and neither of them consults this.

*session-read-buffer-bytes*

How many octets each session's own read buffer holds.

Provisional, and it says so because nothing has profiled this system yet. It trades syscalls against residency: a larger buffer drains a burst in fewer reads, and costs those bytes on every idle session whether or not the peer ever sends. The figure is deliberately small rather than matched to the datagram path's 64 KiB, because the shape being served here is many mostly idle peers, and residency per idle peer is the cost that grows with them while syscalls per burst does not. A measured figure replaces this one.

Package valis/src/edge/tls-serve

Classes

edge-credential-set

The credentials one :443 listener holds: a BASE, and a table of credentials keyed by the served name each belongs to.

BASE is the credential presented when no better choice can be made, and it is the same object the listener was wired against, held by reference so a renewal that reloads that cell is seen here too. BY-NAME answers the question a handshake asks: which certificate belongs to the name this client sent. Both slots are read-only, so the set a reader holds cannot change under it; a boot that admits a new name builds a new set.

tls-edge-protocol

The public :443 HTTPS serve protocol both the inherited-fd and the loopback dev-fallback paths dispatch to. Its handle-connection terminates TLS through the mercer seam against the captured credential cell and hands the plaintext octet stream to the existing HTTP handler, forcing the principal anonymous so the fabric axes are structurally absent. Like dns-serve-protocol it is directly registered by recovered port, not an edge-adapters entry.

tls-serve-handle

A handle to a running :443 serve loop, returned by start-tls-serve and consumed by stop-tls-serve. NAME is the admitted name this loop serves under (its key in tls-serve-handles, so a name-keyed retire can find it); PROTOCOL is the registered tls-edge-protocol; SOURCE is the steered (or self-bound) TCP event source; EXECUTOR is the budget-executor gating the source's concurrency, held so teardown can drain its in-flight connections off its live counter; MUX is the multiplexer it is registered on; PORTS is the list of ports the protocol claims.

Generic functions

tls-edge-protocol-credential-cell

(tls-edge-protocol-credential-cell object)

Undocumented: this exported symbol needs a docstring.

tls-edge-protocol-credential-set

(tls-edge-protocol-credential-set object)

Undocumented: this exported symbol needs a docstring.

Functions

edge-credential-for-name

(edge-credential-for-name set name)

The credential SET holds for NAME, or NIL when it holds none and NIL when SET is NIL. A plain table lookup on a normalized key: this answers on the handshake path, once per ClientHello, so it reads no file, opens no store and parses nothing.

edge-credential-set-base

(edge-credential-set-base instance)

Undocumented: this exported symbol needs a docstring.

edge-credential-set-count

(edge-credential-set-count set)

How many names SET holds a certificate for; 0 when SET is NIL. This is the number an operator reads at boot to see whether the edge picked up every name it was meant to. A count that says one where two were expected is the failure this exists to make visible, and it belongs in the boot output rather than in an off-host discovery weeks later.

edge-credential-set-names

(edge-credential-set-names set)

The names SET holds a certificate for, sorted, or NIL when SET is NIL. For the boot line and for a test asserting which names were admitted, never for the handshake path.

edge-credential-set-p

(edge-credential-set-p object)

Undocumented: this exported symbol needs a docstring.

edge-custody-domains

(edge-custody-domains &key store-path)

Return (values DOMAINS STATUS): the domain names the ACME custody store holds a complete certificate pair for, and how that answer was reached. STATUS is :READ when the store was enumerated and :CUSTODY-UNREADABLE when it refused to be read. STORE-PATH roots the store (a launcher/test seam); NIL selects the default.

An unreadable store and a store holding nothing both produce an empty list and mean opposite things, so the answer says which it is. A caller that logged them alike would report a node serving no name where in fact it could not see the material it holds. Reached by symbol-call, so this file takes no load-time dependency on the ACME client package.

load-edge-credential-cell

(load-edge-credential-cell domain &key store-path)

Source the shared mercer server-credential cell for DOMAIN from the ACME custody store the renewal manager persists issued certs to, and return it — or NIL when DOMAIN is NIL or no usable cert+key is in custody yet. This is the cert-gate at the sourcing layer: a boot before issuance sources no cell, so the :443 edge stays dark and bring-up still completes. STORE-PATH overrides the default store location (a launcher/test seam).

The cell is constructed ONCE and returned by reference; the caller passes the SAME object to start-fabric as :edge-credential-cell, so the listener holds it by reference and a renewal's reload-live-tls-credential swaps THIS exact cell.

A present-but-unreadable cert (corrupt PEM, a key that will not parse) is logged and treated as dark rather than crashing the boot; the durable cert is retried on the next renewal.

load-edge-credential-set

(load-edge-credential-set domains &key store-path base-domain base-credential)

Build and return the edge's credential set over DOMAINS: one credential for each name the ACME custody store holds a usable certificate pair for. Returns NIL when not one credential could be built, which leaves the edge exactly as dark as a node holding no certificate is today.

STORE-PATH roots the custody store (a launcher/test seam). BASE-DOMAIN names the domain whose credential is the set's base, the certificate presented when no better choice can be made. BASE-CREDENTIAL, when supplied, is adopted as that base by reference rather than loaded a second time: the listener is wired against that exact cell and a renewal reloads it in place, so a second copy taken here would go stale at the first renewal and be presented for the base name from then on.

A name with no usable certificate in custody is skipped and the remaining names are still built, so losing the material for one name costs the node that name and never the edge. The store is opened once for the whole build.

reload-live-tls-credential

(reload-live-tls-credential new-chain new-key &key (name +default-tls-edge-name+) handle)

Hot-swap the live :443 listener's certificate in place: resolve the shared credential cell the running serve loop holds BY REFERENCE — from HANDLE when supplied, else the handle admitted under NAME in tls-serve-handles — and call mercer's reload-server-credential on THAT exact cell with the freshly-issued NEW-CHAIN and NEW-KEY (an already-parsed boomer chain and Ironclad key).

The swap lands on the same cell handle-connection reads per connection, never a parallel cell: reload-server-credential does one atomic reference store inside the cell, so the next handshake presents the new certificate while the listening socket is never touched and in-flight connections keep the snapshot they handshook against.

Returns the reloaded credential, or NIL when no live listener is admitted under NAME (and no HANDLE was given). A swap onto a listener that is not up is a no-op the renewal path treats as a miss to log, not an error to raise — the durable cert is still persisted for the next boot.

retire-tls-serve

(retire-tls-serve &key (name +default-tls-edge-name+) (drain-timeout *tls-serve-drain-timeout-seconds*))

Retire the live :443 serve loop admitted under NAME — deregister the steered source, drain in-flight workers (bounded by DRAIN-TIMEOUT), close the source, unregister the serve protocol (dropping the captured credential cell from the hot path), and forget the handle. Each step is error-isolated. Idempotent — a NAME with no live handle is a no-op. Returns the list of retired ports, or NIL when no serve loop was wired under NAME.

start-tls-serve

(start-tls-serve &key tcp-fd tcp-port credential-cell credential-set (name +default-tls-edge-name+) (mux (and *listener* (listener-mux *listener*))) (make-tcp-source (function make-steered-edge-stream-source)))

Wire the :443 TLS edge over an inherited TCP-FD (fulcrum steer) or, when no fd is inherited, a loopback self-bind on TCP-PORT (dev fallback), behind the shared CREDENTIAL-CELL, and serve it over MUX under NAME. Returns a tls-serve-handle for teardown, or NIL when either gate refuses the open: the credential cell holds no usable chain+key (cert-gated), or no descriptor is inherited and TCP-PORT names no port to fall back onto (port-gated). Neither gate signals and neither builds a source or registers a protocol, so the port simply never opens and the resident still comes up. A caller with a good certificate but no port loses :443, not the node it was serving alongside.

The edge is the structural sibling of start-dns-serve for ONE stream source. With an inherited TCP-FD the source is adopted through MAKE-TCP-SOURCE (defaulting to make-steered-edge-stream-source) with no second bind; with no fd the dev fallback self-binds the supplied TCP-PORT on loopback via make-stream-source. Either way the port the protocol registers under — and the loop's teardown identity — is recovered from the descriptor itself, not from TCP-PORT (which the boot path may leave nil even while handing over a live fd): the recovered port is exactly what a connection reports as its destination-port, so registering under it is what lets the registry route a request at all.

The single stream source reuses the edge budget-executor: a stranger-facing connection-oriented port gets the proven per-port concurrency cap (at cap the listening source is deregistered and further connections wait in the kernel backlog) plus the live counter teardown drains on.

Fail-closed, no partial state: both gates run before any source is built, the source is tracked the moment it is built, and the protocol is registered only once the source exists and its port is recovered — so a failure recovering the port or registering tears the source down rather than leaving a half-wired listener. MAKE-TCP-SOURCE is injectable so a test can force a second-stage failure that proves the unwind.

CREDENTIAL-SET is optional and carries the credentials the node holds for the other names it serves. It is held beside CREDENTIAL-CELL, never in place of it: the cert gate still judges the cell alone, so a caller that supplies no set gets exactly the listener it gets today.

stop-tls-serve

(stop-tls-serve handle)

Tear down a :443 serve loop wired by start-tls-serve, symmetric to it and error-isolated: deregister the source from the mux (stop feeding the seam first), close the source, then unregister the serve protocol so the captured credential cell is dropped, and forget the handle. Each step tolerates an already-gone piece. Does NOT drain. A NIL HANDLE is a no-op (nothing was wired).

tls-serve-handle-ports

(tls-serve-handle-ports instance)

Undocumented: this exported symbol needs a docstring.

Variables

*tls-serve-handles*

Live :443 serve loops keyed by the admitted name (normalized via STRING). start-tls-serve records its handle here on success; stop-tls-serve and retire-tls-serve remove it. This is what lets a name-keyed retire reach a wired serve loop, mirroring dns-serve-handles.

Package valis/src/edge/transfer-controller

Conditions

runciter-transfer-authorizer-unavailable

Signalled by bind-transfer-authorizer when runciter has not exported the injection binder. The fail-closed seam guard: valis never leaves the transfer emit path running without its authorization verdict wired in, so an absent runciter binder refuses the injection outright.

transfer-authorizer-contract-version-mismatch

Signalled by bind-transfer-authorizer when valis's transfer- allowlist contract version and runciter's transfer-source contract version disagree. The fail-closed seam-version guard: a version skew across the repo boundary is refused before the binder is funcalled, never silently bridged.

Generic functions

transfer-authorizer-contract-version-mismatch-runciter

(transfer-authorizer-contract-version-mismatch-runciter condition)

Undocumented: this exported symbol needs a docstring.

transfer-authorizer-contract-version-mismatch-valis

(transfer-authorizer-contract-version-mismatch-valis condition)

Undocumented: this exported symbol needs a docstring.

Functions

bind-transfer-authorizer

(bind-transfer-authorizer)

Inject valis's transfer-authorization verdict into runciter's authorize-before-emit gate, returning whatever runciter's binder returns. The upward mirror of bind-dns-view: resolve runciter's binder late (never imported) and refuse fail-closed with runciter-transfer-authorizer-unavailable when runciter has not exported it — runciter is a hard dependency for the transfer emit path and the wire never leaves that gate unauthorized. Assert valis's transfer-allowlist contract version equals runciter's transfer-source contract version BEFORE the funcall — refuse-before-act, signalling transfer-authorizer-contract-version-mismatch on skew — belt-and-suspenders with runciter's own binder-side version guard. On a clean handshake, funcall the binder POSITIONALLY as (binder AUTHORIZER CONTRACT-VERSION) with the self-contained authorizer closure (%transfer-authorizer) and valis's contract version.

Package valis/src/edge/zone-notify

Conditions

notify-source-address-unbound

Signalled when the outbound NOTIFY socket could not be bound to the required egress source address, when it bound a different address than the one asked for, or when the requested source is a wildcard. A wildcard is refused because it delegates the choice of source address to the routing table, which is the failure this whole seam exists to prevent. CAUSE carries the underlying condition when there was one, so the distinction between an unassignable address and a mismatched one survives.

The seam raises its own condition for all three and it is re-signalled as this one at the boundary. An egress source that cannot be held is a fact about this node, and the boot path, the log line and the tests all speak of it as a reason NOTIFY is unavailable here rather than as one dial that failed.

zone-notify-transport-unavailable

Signalled when runciter's real transfer wire cannot be resolved. The wire is what frames a NOTIFY, so its absence is a refusal to arm rather than a round that silently notifies nobody.

zone-notify-unavailable

Parent of the reasons this node cannot emit a NOTIFY at all. Distinct from the per-round refusals, which are returned as verdicts rather than signalled: a verdict says a round reached nobody, this says the notifier itself cannot be stood up here.

Generic functions

notify-source-address-unbound-bound

(notify-source-address-unbound-bound condition)

Undocumented: this exported symbol needs a docstring.

notify-source-address-unbound-cause

(notify-source-address-unbound-cause condition)

Undocumented: this exported symbol needs a docstring.

notify-source-address-unbound-requested

(notify-source-address-unbound-requested condition)

Undocumented: this exported symbol needs a docstring.

Functions

arm-zone-notify

(arm-zone-notify dial-capability &key (source-address (or *notify-source-address* (resolve-transfer-master-address))) (port *notify-destination-port*) (recipients-fn (function notify-recipient-set)))

Register the NOTIFY listener on the zone-change feed, so every committed serial advance drives a round through DIAL-CAPABILITY. Returns the registered listener spec.

DIAL-CAPABILITY is the authority every round dials on, handed in rather than reached for: the notifier holds what it was given and can reach nothing else, and what that authority permits is worked out at the node on each dial.

Refuses fail-closed BEFORE registering anything: runciter's transfer wire must be resolvable, and SOURCE-ADDRESS must actually bind on this node. The trial dial is the point. It turns the egress contract into a boot-time assertion, so a node that does not hold the primary's address says so at startup rather than emitting NOTIFYs that every secondary silently discards. A node that is not the primary is expected to fail here; the boot path declines ZONE-NOTIFY-UNAVAILABLE and carries on.

RECIPIENTS-FN, PORT and SOURCE-ADDRESS are captured by the listener, so the round a commit drives is aimed by what was configured at arm time. RECIPIENTS-FN defaults to the operator-state derivation and is the seam a test aims at a loopback receiver.

IDEMPOTENT: re-arming replaces the listener, because the underlying registry is name-keyed and replaces on re-register.

disarm-zone-notify

(disarm-zone-notify)

Retire the NOTIFY listener and drop the held retransmit state, so neither outlives the serve loop. Idempotent: a second call is a no-op. Returns T when a listener was registered and removed.

emit-zone-notify

(emit-zone-notify origin serial dial-capability &key (source-address (or *notify-source-address* (resolve-transfer-master-address))) (port *notify-destination-port*) (recipients-fn (function notify-recipient-set)) (now (get-universal-time)))

Drive one NOTIFY round for the committed revision of ORIGIN at SERIAL through DIAL-CAPABILITY. Returns (values VERDICT RESULTS) exactly as runciter reported them.

DIAL-CAPABILITY is required and positional, mirroring the seam it reaches: there is no arity of this function that puts a NOTIFY on a wire without the authority to do so having been handed in.

VERDICT is runciter's own, and every one of its refusals is passed through unchanged rather than collapsed: :SENT, :WAITING, :EXHAUSTED, :NO-SOURCE-ADDRESS, :NO-TRANSPORT, :NO-RECIPIENTS, :NO-USABLE-RECIPIENTS and :NO-REACHABLE-RECIPIENTS, plus :NO-TRANSFER-WIRE when runciter's transfer subsystem is not loaded here. RESULTS is runciter's per-recipient plist list, so a caller can see which recipient was framed for and which was not. A round that notified nobody is never reported the same way as one that did.

SOURCE-ADDRESS is proved before any recipient is considered, so a node that cannot hold that address refuses the round loudly rather than framing a NOTIFY the secondary would discard. Each recipient is then dialled as it comes up, and the source is bound and proved again on that recipient's own socket, which is where the guarantee actually has to hold. A recipient that cannot be dialled is framed for nobody and reported unreachable, exactly as one that could not be turned into an address was before. The zone the round is for is offered to the node while the round runs, so a grant naming this node's secondaries is decided against the secondaries THIS zone is configured to notify.

RECIPIENTS-FN is called with the canonical origin and defaults to the operator-state derivation, which reads through the caller's live operator-state connection. NOW is injected; runciter reads no clock of its own. The advanced retransmit state is held for the next round.

zone-notify-armed-p

(zone-notify-armed-p)

T when the NOTIFY listener is registered on the zone-change feed.

Variables

*notify-destination-port*

The port a NOTIFY is sent to. Held as a parameter so a test can aim a round at a loopback receiver and read the datagram that actually left.

*notify-source-address*

An explicit override for the address every outbound NOTIFY must leave from, or NIL to take the node's configured authoritative-DNS address at arm time. That is the same value rendered into a secondary's masters{} clause, because that clause is what authorises the notifier: a NOTIFY from a different source is dropped by the receiving nameserver without a reply. Bound explicitly on the socket and asserted after the bind, never left to the routing table.

A node with neither an override nor a configured address holds no egress source, and arming refuses rather than emitting NOTIFYs every secondary discards.

+zone-notify-listener-name+

The zone-change feed registry key the NOTIFY listener is held under.