Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ Deprecated, Fixed or Security.

## [Unreleased]

### Added

- **Provisional:** restricted pairing `status` accepts optional
`wait: {after_revision: <u64>}` for notification-driven waits bounded by the
attempt deadline or 30 seconds. Advanced revisions and terminal states return
immediately; consumers reissue after an unchanged response. Joining-daemon
`pairing_remote` forwards waits with a 45-second total response deadline.
The same absolute bound covers runtime reads and capacity waits; a stalled
read returns the last authenticated snapshot. Live reconciliation preserves
notifications. Ahead-of-current pending revisions receive the typed
`status-wait-revision-ahead` refusal with the current revision.

### Fixed

- **Provisional:** restricted pairing terminal replies use an independent
Expand Down
1 change: 1 addition & 0 deletions crates/fabric-config/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ error_codes! {
RuntimeCommitIndeterminate => "runtime_commit_indeterminate", Fault;
RuntimeReceiptCapacity => "runtime_receipt_capacity", Fault;
PairingExchangeInactive => "pairing_exchange_inactive", Refusal;
StatusWaitRevisionAhead => "status-wait-revision-ahead", Refusal;
RequestIdConflict => "request_id_conflict", Usage;
CancellationTimeout => "cancellation_timeout", Fault;
IdentityMismatch => "identity_mismatch", Usage;
Expand Down
30 changes: 27 additions & 3 deletions docs/runtime-peer-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ Only a reply bound to the requested attempt and this daemon's NodeID is returned

Each call makes one restricted connection using the daemon's current endpoint,
with at most 32 outbound operations and one 30-second deadline covering connect,
request, and completion. On exchange reconnect, a small status frame first
authenticates the bound attempt on the same stream before the larger send/receive
frame. Pre-auth present/status frames remain capped at 4 KiB; replies are bounded
request, and completion for nonwaiting calls. A `status` request's optional `wait`
is forwarded unchanged; waiting calls have a 45-second total deadline to leave
time for the gateway's bounded wait and response. On exchange reconnect, a small
status frame first authenticates the bound attempt on the same stream before the
larger send/receive frame. Pre-auth present/status frames remain capped at 4 KiB;
replies are bounded
to 1 MiB before allocation. The stream is half-closed after the final request;
the gateway releases its attempt lease before closing, so a subsequent call can
reconnect safely. Serialize calls for the same
Expand All @@ -110,6 +113,7 @@ maximum encoded exchange request length 266,240 bytes after authentication):
```json
{"operation":"present","attempt_id":"<attempt>","secret":"<creation secret>"}
{"operation":"status","attempt_id":"<attempt>"}
{"operation":"status","attempt_id":"<attempt>","wait":{"after_revision":2}}
```

Replies are length-prefixed pairing reply objects, without the local-control
Expand All @@ -122,6 +126,26 @@ are accepted only for the authenticated bound node, including retained terminal
status. Unknown, wrong-node, wrong-token and malformed requests close with a
generic reason and expose no attempt status.

Omitting `wait` preserves immediate status reads. With
`wait: {"after_revision": <u64>}`, status returns immediately if the current
`attempt_revision` is greater than `after_revision` or the attempt is terminal
(`confirmed`, `expired`, or `rejected`). A pending attempt refuses a revision
ahead of its current revision with code `status-wait-revision-ahead`; the reply's
`status.attempt_revision` identifies the current revision. This is not a transition.
Otherwise it waits for a revision change or terminal transition. One absolute
deadline, the earlier of the original attempt deadline and 30 seconds, covers
subscription, read-capacity acquisition, every state read, and notification waits.
At the bound it returns the last authenticated status snapshot normally, with
an unchanged revision if no transition was observed, even if a concurrent disk
operation prevents a fresh read. That snapshot may still be pending at expiry;
reissue with the returned revision to read projected expiry or keep waiting.
This notification re-arm is not polling. Bind, confirm, reject, cancel, expiry,
and recovery (`exchange_lost`) are observable; live reconciliation preserves
subscriptions. Opaque exchange activity does not wake status. A pending wait is
not an idle framing read. Waiting
does not extend the permit deadline or grant access; the same bound-node checks
and connection limits, including the 600-second connection lifetime, still apply.

There are at most 32 accepted pairing connections and one active connection per
attempt. Unknown identities need global credit before parsing or runtime work:
one request per second, burst 10, refunded after successful validation. Previously
Expand Down
4 changes: 4 additions & 0 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ pub struct DaemonState {
/// Network read permits live inside blocking workers, not socket futures.
/// A timed-out read waiting for the runtime mutex still consumes its permit.
pairing_read_slots: Arc<Semaphore>,
#[cfg(test)]
pairing_wait_read: tokio::sync::watch::Sender<Option<String>>,
dial_slots: Arc<Semaphore>,
mux_stream_slots: Arc<Semaphore>,
peer_connections: Arc<mux::PeerConnections>,
Expand Down Expand Up @@ -1355,6 +1357,8 @@ impl DaemonState {
handshake_slots: Arc::new(Semaphore::new(MAX_INCOMING_HANDLERS)),
pairing_slots: Arc::new(Semaphore::new(pairing::MAX_CONNECTIONS)),
pairing_read_slots: Arc::new(Semaphore::new(pairing::MAX_CONNECTIONS)),
#[cfg(test)]
pairing_wait_read: tokio::sync::watch::channel(None).0,
dial_slots: Arc::new(Semaphore::new(MAX_DIAL_HANDLERS)),
mux_stream_slots: Arc::new(Semaphore::new(MAX_INCOMING_HANDLERS)),
peer_connections,
Expand Down
Loading
Loading