Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
6604640
chore(rpc): anchor the -32000 name-reconciliation lane at 0.264.0
MichaelTaylor3d Sep 3, 2026
5ba5464
test(rpc): pin -32000 to ONE machine name across both producers
MichaelTaylor3d Sep 3, 2026
52d747a
fix(rpc): reconcile -32000 onto SERVER_ERROR across both producers
MichaelTaylor3d Sep 3, 2026
2cb75b3
style(rpc): rustfmt the two -32000 envelope repairs
MichaelTaylor3d Sep 3, 2026
cc43bf8
chore: renumber to 0.252.10, under the MSI ProductVersion ceiling (#521)
MichaelTaylor3d Sep 3, 2026
5c22a46
Merge remote-tracking branch 'origin/main' into loop/496-239-rpc-cont…
MichaelTaylor3d Sep 3, 2026
fbee793
Merge remote-tracking branch 'origin/main' into loop/496-239-rpc-cont…
MichaelTaylor3d Sep 3, 2026
88919e5
chore(release): merge main into loop/496-239-rpc-contract (keep 0.252…
MichaelTaylor3d Sep 3, 2026
386999a
chore(release): merge main into loop/496-239-rpc-contract (keep 0.252…
MichaelTaylor3d Sep 3, 2026
ac7eb30
chore(release): merge main into loop/496-239-rpc-contract (keep 0.252…
MichaelTaylor3d Sep 3, 2026
a193b12
fix(rpc): widen -32000's catalogue description to cover both minters
MichaelTaylor3d Sep 3, 2026
0b2e9f3
chore(release): merge main into loop/496-239-rpc-contract (keep 0.252…
MichaelTaylor3d Sep 3, 2026
ee81636
chore(release): merge main into loop/496-239-rpc-contract (keep 0.252…
MichaelTaylor3d Sep 3, 2026
0c8a130
chore(release): merge main into loop/496-239-rpc-contract (keep 0.253.2)
MichaelTaylor3d Sep 3, 2026
a4acd73
chore(release): merge main into loop/496-239-rpc-contract (keep 0.253.2)
MichaelTaylor3d Sep 3, 2026
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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.253.0"
version = "0.253.2"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ distinguishing node-shell errors from upstream/boundary ones), beside the numeri
| -32600 | `INVALID_REQUEST` | shell | Not a single JSON-RPC object (batch arrays unsupported). |
| -32601 | `METHOD_NOT_FOUND` | boundary | Not resolved locally or by the upstream. |
| -32602 | `INVALID_PARAMS` | upstream | Invalid or missing method parameters. |
| -32000 | `DISPATCH_FAILED` | shell | The node failed to dispatch the request. |
| -32000 | `SERVER_ERROR` | shell | The request failed on the way to, or inside, the read path. Minted by both the shell and the node library; `data.origin` says which. |
| -32010 | `UPSTREAM_ERROR` | shell | The blind-passthrough relay to the upstream failed. |
| -32030 | `UNAUTHORIZED` | shell | A `control.*` method was called without a valid local control token. |
| -32031 | `NOT_SUPPORTED` | shell | A control op the node build can't perform (e.g. §21 sync with no identity). |
Expand Down
17 changes: 9 additions & 8 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ shell's HTTP control surface — pairing/control.status/hostedStores/updater —
ties its node-surface method names + the peer-reachable set to the crate. The shell keeps its own
`ErrorCode` enum, but every code it shares with the crate now SOURCES its number and machine string
from `dig_rpc_protocol::ErrorCode` rather than restating them, and a guard asserts that equality
across the whole shared set. One shell-specific string remains: `DISPATCH_FAILED` at `-32000`, where
the crate says `SERVER_ERROR`. That name is minted and published by the shell alone, so reconciling
it is a separate, wire-visible decision. Not yet adopted: the crate's `RpcError` envelope type and
across the whole shared set, with no exclusions. Not yet adopted: the crate's `RpcError` envelope type and
the `dig-rpc` server framework. The numeric error codes remain guaranteed
identical by the conformance vectors.

Expand Down Expand Up @@ -971,9 +969,12 @@ truth shared with `rpc.dig.net`. This node MUST NOT diverge from it.
- **The shell (`dig-node-service`) sources every code it shares with the contract crate from
`dig_rpc_protocol::ErrorCode` rather than restating it**, so a shell-catalogued number and its name
cannot disagree with the crate either. `-32004` is `RESOURCE_UNAVAILABLE` on both sides and on every
frame the node emits. The single remaining shell-specific string is `DISPATCH_FAILED` at `-32000`,
which the shell alone mints and publishes; the crate's generic name for that number is
`SERVER_ERROR`, and reconciling the two is tracked separately.
frame the node emits, and `-32000` is `SERVER_ERROR` on both sides — the shell and the read path
both mint that number, and a client branching on `data.code` cannot see which layer answered, so
they publish ONE name. Which layer answered is carried by `data.origin`.

`dig-node-control-interface` declares the same number as `DISPATCH_FAILED` for its control
surface, which dig-node does not mint.
- **The ONE exception, stated rather than left silent:** a code this node emits that
`dig-rpc-protocol` does not declare carries NO `data` object at all. Today that is `-32001`
alone (the push-authority refusal, §21.9), which `SYSTEM.md` records as reserved-by-occupancy.
Expand All @@ -990,7 +991,7 @@ For each request, in order:
1. `rpc.discover` → answered by the shell with the OpenRPC document (§6.3) as `result`.
2. `control.*` → the control plane (§7): authorization gate, then `dispatch_control`.
3. Everything else → normalized (§5.3), then dispatched to `dig_node_core::handle_rpc` on a
spawned task. A panicked/failed dispatch task yields `DISPATCH_FAILED` (`-32000`); the server
spawned task. A panicked/failed dispatch task yields `SERVER_ERROR` (`-32000`); the server
MUST survive it.
4. If the read path returns `-32601` (method not found), the shell relays the **original,
un-normalized** request to the upstream (§5.4).
Expand Down Expand Up @@ -3274,7 +3275,7 @@ method runs, and it MUST NOT be conflated with the wallet's own `-32043` egress
| -32600 | `INVALID_REQUEST` | shell | Not a single JSON-RPC object (batch arrays unsupported); also the 421 Host-rejection body. |
| -32601 | `METHOD_NOT_FOUND` | boundary | Not resolved locally or by the upstream (internally: the passthrough cue). |
| -32602 | `INVALID_PARAMS` | node | Invalid/missing method parameters (also minted by the control plane for bad control params). |
| -32000 | `DISPATCH_FAILED` | shell | The shell failed to dispatch the request to the read path. |
| -32000 | `SERVER_ERROR` | shell | The request failed on the way to, or inside, the read path: a failed dispatch task, an unclassified wallet-proxy failure, or a generic read-path failure. Minted by BOTH the shell and the node library; `data.origin` says which. |
| -32004 | `RESOURCE_UNAVAILABLE` | node | Genuine content miss at the requested root; distinct from transport failure. Minted by the node library for a LOCAL miss — `dig.fetchRange` ("resource not held") and `dig.getManifest` ("capsule not held locally") — and relayed with `origin: upstream` when a passthrough upstream returns it. Never a fabricated result. |
| -32005 | `ROOT_NOT_ANCHORED` | node | The node's mandatory read-path anchored-root pin (§14.4) fails closed: the requested root does not match the chain-anchored tip, the store has no confirmed on-chain generation, the chain is unreachable, or a rootless request cannot be resolved under enforcement. Minted by the node library on `dig.getContent`. |
| -32008 | `CONTENT_REDIRECT` | node | The node does not (or, under §17's throttle, will not right now) serve the requested content itself, but the DHT located peer(s) that hold it — `error.data.redirect` names them (`content`, `providers[].peer_id`/`addresses`, `redirect_depth`, `max_redirects`) so the caller re-requests there. The candidate set is CAPPED at `MAX_REDIRECT_PROVIDERS` (= dig-dht's `MAX_ADDRESSES_PER_RECORD`): a redirect NAMES holders (the requestor dials them over its own §5.2 reachability ladder — this node does NOT dial/probe them), so a few candidates suffice and probing-on-miss would itself be an amplification vector. Minted on a content miss (`dig.getContent`/`dig.fetchRange`/the peer range-stream) and on outgoing-bandwidth saturation (§17), bounded by the same redirect-hop cap either way. |
Expand Down
2 changes: 1 addition & 1 deletion USER_JOURNEY.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ every subcommand (machine output to stdout, prose to stderr).
`2 USAGE`, `3 PERMISSION_DENIED`, `4 SERVICE_FAILED`, `5 BIND_FAILED`, `6 IO_ERROR`, `12 NODE_UNREACHABLE`.
- **Stable JSON-RPC error codes** (UPPER_SNAKE in `error.data.code`): `PARSE_ERROR` (-32700),
`INVALID_REQUEST` (-32600), `METHOD_NOT_FOUND` (-32601), `INVALID_PARAMS` (-32602),
`DISPATCH_FAILED` (-32000, shell), `UPSTREAM_ERROR` (-32010, shell), and the control-plane codes
`SERVER_ERROR` (-32000, shell and node), `UPSTREAM_ERROR` (-32010, shell), and the control-plane codes
`UNAUTHORIZED` (-32020), `NOT_SUPPORTED` (-32021), `CONTROL_ERROR` (-32022). The `data.origin`
field distinguishes node-shell errors from upstream/boundary ones.
- **`rpc.discover`**: returns the OpenRPC document over the wire, so an agent can introspect the
Expand Down
2 changes: 1 addition & 1 deletion crates/dig-node-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ name = "dig-node-core"
# dig-node#276/#296). Changing a public return type is BREAKING for an out-of-workspace implementor;
# this crate is consumed in-workspace only and is pre-1.0, so it is a MINOR bump under SemVer's 0.x
# rule -- recorded here rather than letting the number imply the locator surface held still.
version = "0.67.0"
version = "0.68.0"
edition = "2021"
license = "GPL-2.0-only"
description = "The canonical DIG node ENGINE library (crate `dig_node_core`): the JSON-RPC dispatch (`handle_rpc`, the same contract as rpc.dig.net), local-first content serve/fetch/redirect from LOCAL .dig store modules (via digstore_host::serve_blind), chain-anchored-root resolution, chain-watch + subscriptions + generation gap-fill, the LRU cache, and the full P2P stack. Shared UNCHANGED by both host shells: the `dig-node` OS-service binary (dig-node-service) and the DIG Browser's in-process cdylib (dig-runtime). Native Rust so the compiled-module serve path works."
Expand Down
15 changes: 11 additions & 4 deletions crates/dig-node-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4574,7 +4574,11 @@ pub async fn handle_rpc_json(
/// Every frame is minted through [`seams::dig_rpc::errors::error_frame`], so a declared code
/// carries `data.code` + `data.origin` from `dig-rpc-protocol` by construction rather than by
/// each call site remembering to add them (dig-node#340).
fn rpc_err(id: &Value, code: i64, message: &str) -> Value {
///
/// PUBLIC so a consumer that also mints frames at the same numbers — the dig-node shell — can be
/// tested against the frame this crate actually emits, rather than against a restatement of it.
/// Two producers of one wire shape are one edit away from disagreeing (dig-node#496).
pub fn rpc_err(id: &Value, code: i64, message: &str) -> Value {
crate::seams::dig_rpc::errors::error_frame(id, code, message)
}

Expand Down Expand Up @@ -5239,10 +5243,13 @@ mod tests {
/// ([`RESOURCE_UNAVAILABLE`] and [`RESOURCE_NOT_AVAILABLE`]) are correctly read as one condition
/// under two names rather than as a collision.
///
/// Deliberately NOT exhaustive yet: `content_serve::SERVE_UNREADABLE` (`-32000`) specialises the
/// canonical `SERVER_ERROR`, and the chat band (`-32050`..`-32052`) is undeclared upstream
/// entirely. Both are pre-existing and out of this change; adding them is a follow-up that has to
/// Deliberately NOT exhaustive yet: the chat band (`-32050`..`-32052`) is undeclared upstream
/// entirely. That is pre-existing and out of this change; adding it is a follow-up that has to
/// resolve the condition, not the table.
///
/// `content_serve::SERVE_UNREADABLE` used to be named here as a second `-32000` gap. It was not
/// one: its code field's only sink answered `502` from the message and never read the number, so
/// it was a dead const rather than a producer, and it has been deleted (dig-node#496).
const LOCAL_WIRE_CODES: &[(i64, &str)] = &[
(
crate::download::CONTENT_MISS_RATE_LIMITED,
Expand Down
10 changes: 8 additions & 2 deletions crates/dig-node-core/src/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1282,8 +1282,14 @@ fn admission_refused(
peer = %conn_key.get(..16).unwrap_or(conn_key),
"peer serve: inbound work refused at admission"
);
json!({"jsonrpc":"2.0","id":id,
"error":{"code":-32000,"message":"request refused","data":{"reason":refusal.reason()}}})
// Minted through the canonical envelope so the refusal carries `data.code` +
// `data.origin` like every other declared code, with `reason` ADDED to that object rather
// than replacing it — the same shape `relay_pending_object` uses to attach its progress
// field. A frame that carries only `reason` forces a client back to prose for the one
// question `data.code` exists to answer (dig-node#496).
let mut error = crate::seams::dig_rpc::errors::error_object(-32000, "request refused");
error["data"]["reason"] = json!(refusal.reason());
json!({"jsonrpc":"2.0","id":id,"error":error})
}

/// Whether `method` may be answered over the **mTLS peer surface** (other DIG nodes).
Expand Down
14 changes: 5 additions & 9 deletions crates/dig-node-core/src/seams/content/content_serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ use serde_json::{json, Value};

use crate::{decide_pin, pin_enforced, CapsuleStore, Node, PinDecision, ROOT_NOT_ANCHORED};

/// JSON-RPC-style code for a serve that fetched bytes but could not verify/decrypt/reach them —
/// distinct from a clean content miss (`NotFound`) and from the anchored-root pin (`RootError`).
const SERVE_UNREADABLE: i64 = -32000;
/// The upstream/peer "resource not available at this root" code — a genuine content miss (SPEC §10).
const RESOURCE_UNAVAILABLE: i64 = -32004;

Expand Down Expand Up @@ -138,11 +135,11 @@ pub enum PlaintextOutcome {
InvalidParams { message: String },
/// Bytes were fetched but verification or decryption failed (tamper / wrong key / decode error),
/// or the fetch itself errored at the transport level. Fail-closed — no plaintext is returned.
Unreadable {
code: i64,
message: String,
root_hex: String,
},
///
/// Carries NO numeric code: this outcome's only sink is the HTTP serve path, which answers
/// `502 BAD_GATEWAY` from the message alone. A `-32000` field here never reached any wire and
/// was therefore a dead producer rather than a taxonomy gap (dig-node#496).
Unreadable { message: String, root_hex: String },
}

/// The canonical ROOTLESS resource URN whose SHA-256 is the retrieval key and whose bytes seed the AES
Expand Down Expand Up @@ -992,7 +989,6 @@ impl Node {
None
}
Err(ProxyMiss::Error(message)) => Some(PlaintextOutcome::Unreadable {
code: SERVE_UNREADABLE,
message,
root_hex: root_hex.to_string(),
}),
Expand Down
8 changes: 6 additions & 2 deletions crates/dig-node-core/src/seams/dig_rpc/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,12 @@ impl RpcDispatch for Node {
}
v
}
Err(e) => json!({"jsonrpc":"2.0","id":id,
"error":{"code":-32000,"message":format!("upstream: {e}")}}),
// Through the canonical frame builder, so this failure carries `data.code` +
// `data.origin` like every other declared code rather than a bare {code,message}
// a client cannot branch on (dig-node#496).
Err(e) => {
crate::seams::dig_rpc::errors::error_frame(&id, -32000, &format!("upstream: {e}"))
}
}
}
}
Expand Down
27 changes: 18 additions & 9 deletions crates/dig-node-service/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,16 @@ pub enum ErrorCode {
MethodNotFound,
/// `-32602` — invalid params (e.g. missing store_id / urn). From dig-node.
InvalidParams,
/// `-32000` — the dig-node shell failed to dispatch the request to the node.
/// Dig-node-shell error.
/// `-32000` — a request failed on the way to, or inside, the read path: the shell
/// could not dispatch it (a panicked/failed spawn task, an unclassified wallet-proxy
/// failure), or the read path itself failed generically.
///
/// ONE condition covering BOTH mints, which is why it carries one name: the shell mints
/// it at 2 sites and the embedded read path at 11, on the same port and at the same
/// number. Which layer answered is carried by `data.origin`, never by the name — so the
/// name is taken from the shared catalogue (`SERVER_ERROR`) rather than restated. The
/// Rust variant keeps its condition-describing spelling; that is internal identity, not
/// the wire (dig-node#496).
DispatchFailed,
/// `-32004` — the requested resource is not available at the requested root (a
/// genuine content miss for that capsule, distinct from a transport failure).
Expand Down Expand Up @@ -791,10 +799,7 @@ impl ErrorCode {
ErrorCode::InvalidRequest => shared(dig_rpc_protocol::ErrorCode::InvalidRequest),
ErrorCode::MethodNotFound => shared(dig_rpc_protocol::ErrorCode::MethodNotFound),
ErrorCode::InvalidParams => shared(dig_rpc_protocol::ErrorCode::InvalidParams),
// NOT sourced: the shell mints -32000 under its own `DISPATCH_FAILED`, which the
// crate calls `SERVER_ERROR`. That name IS on the wire, so reconciling it is a
// shipped-name change of a different kind — tracked separately, never silently.
ErrorCode::DispatchFailed => -32000,
ErrorCode::DispatchFailed => shared(dig_rpc_protocol::ErrorCode::ServerError),
ErrorCode::ResourceUnavailable => {
shared(dig_rpc_protocol::ErrorCode::ResourceUnavailable)
}
Expand Down Expand Up @@ -835,8 +840,7 @@ impl ErrorCode {
ErrorCode::InvalidRequest => dig_rpc_protocol::ErrorCode::InvalidRequest.machine_code(),
ErrorCode::MethodNotFound => dig_rpc_protocol::ErrorCode::MethodNotFound.machine_code(),
ErrorCode::InvalidParams => dig_rpc_protocol::ErrorCode::InvalidParams.machine_code(),
// See `code()` for why -32000 is deliberately not sourced.
ErrorCode::DispatchFailed => "DISPATCH_FAILED",
ErrorCode::DispatchFailed => dig_rpc_protocol::ErrorCode::ServerError.machine_code(),
ErrorCode::ResourceUnavailable => {
dig_rpc_protocol::ErrorCode::ResourceUnavailable.machine_code()
}
Expand Down Expand Up @@ -910,7 +914,12 @@ impl ErrorCode {
}
ErrorCode::MethodNotFound => "Method is not resolved locally or by the upstream.",
ErrorCode::InvalidParams => "Invalid or missing method parameters.",
ErrorCode::DispatchFailed => "The node failed to dispatch the request.",
ErrorCode::DispatchFailed => {
concat!(
"A generic server error — the shell failed to dispatch the request, or ",
"the embedded read path failed internally, whichever layer answered.",
)
}
ErrorCode::ResourceUnavailable => {
concat!(
"The requested resource is not available at the requested root — ",
Expand Down
Loading
Loading