Skip to content
Merged
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
2 changes: 1 addition & 1 deletion 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 @@ -31,7 +31,7 @@ edition = "2021"
# (`[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.29.0"
version = "0.30.0"

# 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
41 changes: 41 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,47 @@ boundary, so key material never crosses to a paired caller even with a valid tok
unit-tested exhaustively: an unpaired caller is denied on every mutation/custody method; a paired token
authorizes a mutation but NOT pairing administration; a revoked token is denied on the next request.

### 7.13. DIG auto-update beacon proxy (`control.updater.*`, #515)

The DIG auto-update beacon (`dig-updater`, a separate installable service — DIG-Network/dig-updater
SPEC §§1–13) checks daily for new releases of the DIG binaries and installs them behind a signed
trust chain. dig-node exposes a THIN proxy to it over the SAME `control.*` gate (§7.2) — it never
re-verifies the beacon's signed manifest and never decides what to install; it only reads the
beacon's world-readable status mirror and shells its own elevation-gated CLI.

| Method | Params | Result (essentials) |
|---|---|---|
| `control.updater.status` | — | `installed: false` when the beacon has no status.json yet (never an error); else `installed: true`, `status` = the beacon's `status.json` verbatim (dig-updater SPEC §13.2: `schema`, `version`, `channel`, `paused`, `paused_until`, `last_check`, `last_check_kind`, `last_outcome`, `last_reason`, `last_detail`, `components[]`, `next_wake`, `trust_state`) |
| `control.updater.setChannel` | `channel` (string, e.g. `"alpha"`) | The beacon CLI's `channel set <channel> --json` output verbatim (dig-updater SPEC §13.3) |
| `control.updater.pause` | `until?` (unix seconds) | The beacon CLI's `pause [--until <ts>] --json` output verbatim |
| `control.updater.resume` | — | The beacon CLI's `resume --json` output verbatim |
| `control.updater.checkNow` | — | The beacon CLI's `check --now --json` output verbatim — a full pass; the call blocks until it completes |

**Status is read directly off disk, never through the CLI** — `control.updater.status` is the
method a controller polls, and a file read is far cheaper than a process spawn on every poll. The
status directory is a WORLD-READABLE sibling of the beacon's own Admin/SYSTEM-only state directory
(dig-updater SPEC §13.2: `%ProgramData%\DIG\updater-status` on Windows, `/var/lib/dig-updater-status`
on Unix), so dig-node needs no elevation to read it. A present-but-corrupt `status.json` is reported
as `CONTROL_ERROR` (a genuine anomaly); an ABSENT one is `{ "installed": false }` (the beacon may
simply never have been installed on this machine) — never an error either way.

**Mutations shell the `dig-updater` CLI** — `setChannel`/`pause`/`resume`/`checkNow` invoke the
already elevation-gated operator CLI (dig-updater SPEC §13.3: `channel set`/`pause`/`resume` require
Administrator/root) rather than writing the beacon's Admin-only `config.json` directly. This service
runs privileged (Windows LocalSystem / a root daemon), so it satisfies that elevation check the same
way a human operator running an elevated terminal would. The CLI is resolved by an ABSOLUTE path —
an explicit override, else beside this running `dig-node` binary (the shared bin dir dig-installer
places `digstore`/`dig-node`/`dig-dns` into, and where the beacon installer, #514, is expected to
place `dig-updater`), else a per-OS conventional install root — NEVER a bare name resolved through
`PATH`. No binary resolves → `NOT_SUPPORTED` ("the DIG auto-update beacon is not installed on this
machine"); the CLI runs but declines (a bad channel, a missing elevation) → `CONTROL_ERROR` carrying
the CLI's own `detail` message; the CLI produces unparsable output (a crash) → `CONTROL_ERROR`.

**Opaque passthrough by design.** Both the status file and every CLI `--json` result are forwarded
as opaque JSON, never re-typed into a dig-node-owned shape — the beacon's schema-versioned wire
contract exists precisely so an independent reader can do this safely: a field the beacon adds later
passes through unchanged, with no shape to keep in sync on this side.

---

## 8. CLI contract
Expand Down
20 changes: 19 additions & 1 deletion crates/dig-node-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ default-run = "dig-node"
name = "dig-node"
path = "src/main.rs"

# A tiny stand-in for the REAL `dig-updater` beacon CLI (a separate repo/release, not
# vendored here), so the `updater.*` control-plane tests (#515) exercise a genuine child-
# process spawn — argv, stdout/exit-code parsing — without depending on that binary being
# installed on the test runner. Controlled entirely by env vars (see the fixture's own doc
# comment); never shipped (the release workflows build only `--bin dig-node`, §2.4a).
#
# Deliberately named WITHOUT "update"/"install"/"setup"/"patch": Windows' installer-detection
# heuristic can require elevation to launch (or even just spawn via CreateProcess) an
# unmanifested `.exe` whose name matches those substrings (`ERROR_ELEVATION_REQUIRED`, 740) —
# hit empirically while developing #515 with a `fake_dig_updater` name. The REAL `dig-updater`
# binary name is the beacon repo's own choice and out of scope here; this fixture just avoids
# the trap for OUR test infra.
[[bin]]
name = "fake_beacon_cli"
path = "tests/fixtures/fake_beacon_cli.rs"

# The service shell's own library (transport/control/CLI/meta). Named `dig_node_service`
# so it does not collide with the node library crate, which owns the `dig_node` name.
[lib]
Expand Down Expand Up @@ -46,7 +62,9 @@ dig-wallet = { path = "../dig-wallet" }
# and one server framework across the node and the service shell. `ws` enables
# `axum::extract::ws` for the `GET /ws/status` liveness endpoint (#239).
axum = { version = "0.7", features = ["ws"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "time"] }
# `process` backs the async `tokio::process::Command` the `updater` module (#515) spawns the
# `dig-updater` beacon CLI through — a non-blocking child-process wait alongside the axum runtime.
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "time", "process"] }
tower-http = { version = "0.6", features = ["cors"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
10 changes: 10 additions & 0 deletions crates/dig-node-service/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
//! not model: the **pin registry** (which stores the operator chose to host, so they
//! survive being listed even before/after caching) and the **upstream override**,
//! both persisted in this service's own keys inside the shared `config.json`.
//! `control.updater.*` (#515) proxies the DIG auto-update beacon the same way — see
//! [`crate::updater`] for what it reads directly vs. shells out to.

use std::path::{Path, PathBuf};
use std::sync::Arc;
Expand Down Expand Up @@ -573,6 +575,14 @@ pub async fn dispatch_control(ctx: &ControlCtx, id: Value, method: &str, params:
"control.hostedStores.status" => hosted_status(ctx, id, params).await,
"control.sync.status" => control_ok(id, sync_status(ctx).await),
"control.sync.trigger" => sync_trigger(ctx, id, params).await,
// The DIG auto-update beacon proxy (#515) — a THIN passthrough to `dig-updater`'s
// own status file + CLI (see `crate::updater`'s module doc for why nothing here
// re-implements the beacon's trust/install logic).
"control.updater.status" => crate::updater::status(id),
"control.updater.setChannel" => crate::updater::set_channel(id, params).await,
"control.updater.pause" => crate::updater::pause(id, params).await,
"control.updater.resume" => crate::updater::resume(id).await,
"control.updater.checkNow" => crate::updater::check_now(id).await,
// Pairing administration (#280) — reached only with the MASTER token (the
// gate blocks a paired token from these, see `is_pairing_admin_method`).
"control.pairing.list" => crate::pairing::list(&ctx.pairings, &ctx.state_dir, id),
Expand Down
4 changes: 4 additions & 0 deletions crates/dig-node-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub mod service;
/// paired-token store live so the daemon (which may run as a service under a different OS
/// account) and the operator CLI resolve the SAME files. See [`state`].
pub mod state;
/// The beacon (`dig-updater`) RPC proxy (#515): `control.updater.*` reads the DIG auto-update
/// beacon's world-readable status and shells its elevation-gated CLI for channel/pause/resume/
/// check-now — never a second implementation of the beacon's own trust logic. See [`updater`].
pub mod updater;
pub mod wallet_authz;

/// Windows Service Control Protocol entrypoint — only meaningful on Windows, where
Expand Down
39 changes: 39 additions & 0 deletions crates/dig-node-service/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,39 @@ pub fn methods() -> &'static [MethodInfo] {
NOT_SUPPORTED if no §21 identity / not eligible.",
requires_auth: true,
},
// -- DIG auto-update beacon proxy (#515) — a THIN passthrough to `dig-updater` --
MethodInfo {
name: "control.updater.status",
served: "control",
summary: "The DIG auto-update beacon's status (channel, paused, last check, \
per-component decisions); { installed: false } if not installed.",
requires_auth: true,
},
MethodInfo {
name: "control.updater.setChannel",
served: "control",
summary: "Set the beacon's update channel. Params { channel }.",
requires_auth: true,
},
MethodInfo {
name: "control.updater.pause",
served: "control",
summary: "Suspend the beacon's auto-updates. Params { until? } (unix seconds).",
requires_auth: true,
},
MethodInfo {
name: "control.updater.resume",
served: "control",
summary: "Resume the beacon's auto-updates (clears any pause).",
requires_auth: true,
},
MethodInfo {
name: "control.updater.checkNow",
served: "control",
summary: "Trigger an on-demand full beacon update pass; blocks until it \
completes.",
requires_auth: true,
},
// -- pairing administration (#280) — MASTER-token only ----------------------------
MethodInfo {
name: "control.pairing.list",
Expand Down Expand Up @@ -876,6 +909,12 @@ mod tests {
"control.hostedStores.status",
"control.sync.status",
"control.sync.trigger",
// DIG auto-update beacon proxy (#515).
"control.updater.status",
"control.updater.setChannel",
"control.updater.pause",
"control.updater.resume",
"control.updater.checkNow",
] {
assert!(
names.contains(&required),
Expand Down
Loading
Loading