refactor: clarify resource control boundaries
This commit is contained in:
parent
eb0f69f287
commit
d443b82f1a
5 changed files with 275 additions and 64 deletions
|
|
@ -3,6 +3,7 @@ mod daemon;
|
|||
pub mod doctor;
|
||||
mod local_control;
|
||||
mod peer_client;
|
||||
mod resource_contracts;
|
||||
mod runtime;
|
||||
pub mod service;
|
||||
mod sync;
|
||||
|
|
@ -5588,6 +5589,7 @@ pub fn handle_request(
|
|||
node: &LocalNode,
|
||||
request: ControlRequest,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
debug_assert!(!resource_contracts::contracts().is_empty());
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
match request {
|
||||
ControlRequest::Status => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,55 @@ pub async fn handle_request_async(
|
|||
stream = trace.stream.as_deref().unwrap_or(""),
|
||||
"control request started"
|
||||
);
|
||||
let result = match request {
|
||||
let result = route_request_async(node, request).await;
|
||||
match &result {
|
||||
Ok(_) => tracing::info!(
|
||||
command = trace.command,
|
||||
peer_node = trace.peer_node.as_deref().unwrap_or(""),
|
||||
resource = trace.resource.as_deref().unwrap_or(""),
|
||||
capability = trace.capability.as_deref().unwrap_or(""),
|
||||
stream = trace.stream.as_deref().unwrap_or(""),
|
||||
"control request completed"
|
||||
),
|
||||
Err(error) => tracing::warn!(
|
||||
command = trace.command,
|
||||
peer_node = trace.peer_node.as_deref().unwrap_or(""),
|
||||
resource = trace.resource.as_deref().unwrap_or(""),
|
||||
capability = trace.capability.as_deref().unwrap_or(""),
|
||||
stream = trace.stream.as_deref().unwrap_or(""),
|
||||
error_code = node_error_code(error),
|
||||
%error,
|
||||
"control request failed"
|
||||
),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
enum AsyncRoute {
|
||||
Handled(Box<ControlResponse>),
|
||||
Continue(ControlRequest),
|
||||
}
|
||||
|
||||
async fn route_request_async(
|
||||
node: &LocalNode,
|
||||
request: ControlRequest,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
let request = match route_peer_family(node, request).await? {
|
||||
AsyncRoute::Handled(response) => return Ok(*response),
|
||||
AsyncRoute::Continue(request) => request,
|
||||
};
|
||||
let request = match route_resource_sync_family(node, request).await? {
|
||||
AsyncRoute::Handled(response) => return Ok(*response),
|
||||
AsyncRoute::Continue(request) => request,
|
||||
};
|
||||
route_local_with_native_mirrors(node, request).await
|
||||
}
|
||||
|
||||
async fn route_peer_family(
|
||||
node: &LocalNode,
|
||||
request: ControlRequest,
|
||||
) -> Result<AsyncRoute, NodeError> {
|
||||
let response = match request {
|
||||
ControlRequest::PeerCardExport { out } => export_peer_card(node, out, true).await,
|
||||
ControlRequest::PeerPing { node: peer_node } => peer_ping(node, &peer_node).await,
|
||||
ControlRequest::PeerAuthCheck {
|
||||
|
|
@ -36,6 +84,16 @@ pub async fn handle_request_async(
|
|||
ControlRequest::NodeEnrollSync { owner_node } => {
|
||||
node_enrollment_sync_from_owner(node, &owner_node).await
|
||||
}
|
||||
other => return Ok(AsyncRoute::Continue(other)),
|
||||
}?;
|
||||
Ok(AsyncRoute::Handled(Box::new(response)))
|
||||
}
|
||||
|
||||
async fn route_resource_sync_family(
|
||||
node: &LocalNode,
|
||||
request: ControlRequest,
|
||||
) -> Result<AsyncRoute, NodeError> {
|
||||
let response = match request {
|
||||
ControlRequest::CasFetch {
|
||||
node: peer_node,
|
||||
hash,
|
||||
|
|
@ -124,8 +182,16 @@ pub async fn handle_request_async(
|
|||
mtu,
|
||||
} => overlay_runtime_up(node, name, bearer_secret, mtu).await,
|
||||
ControlRequest::OverlayDown { name } => overlay_runtime_down(node, &name),
|
||||
other => {
|
||||
let response = handle_request(node, other)?;
|
||||
other => return Ok(AsyncRoute::Continue(other)),
|
||||
}?;
|
||||
Ok(AsyncRoute::Handled(Box::new(response)))
|
||||
}
|
||||
|
||||
async fn route_local_with_native_mirrors(
|
||||
node: &LocalNode,
|
||||
request: ControlRequest,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
let response = handle_request(node, request)?;
|
||||
match &response {
|
||||
ControlResponse::CasAdded { hash, .. } => {
|
||||
mirror_local_blob_to_iroh_blobs(node, hash).await?;
|
||||
|
|
@ -157,29 +223,6 @@ pub async fn handle_request_async(
|
|||
}
|
||||
Ok(response)
|
||||
}
|
||||
};
|
||||
match &result {
|
||||
Ok(_) => tracing::info!(
|
||||
command = trace.command,
|
||||
peer_node = trace.peer_node.as_deref().unwrap_or(""),
|
||||
resource = trace.resource.as_deref().unwrap_or(""),
|
||||
capability = trace.capability.as_deref().unwrap_or(""),
|
||||
stream = trace.stream.as_deref().unwrap_or(""),
|
||||
"control request completed"
|
||||
),
|
||||
Err(error) => tracing::warn!(
|
||||
command = trace.command,
|
||||
peer_node = trace.peer_node.as_deref().unwrap_or(""),
|
||||
resource = trace.resource.as_deref().unwrap_or(""),
|
||||
capability = trace.capability.as_deref().unwrap_or(""),
|
||||
stream = trace.stream.as_deref().unwrap_or(""),
|
||||
error_code = node_error_code(error),
|
||||
%error,
|
||||
"control request failed"
|
||||
),
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ControlTraceFields {
|
||||
|
|
|
|||
163
crates/geth-node/src/resource_contracts.rs
Normal file
163
crates/geth-node/src/resource_contracts.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
//! Reviewable resource-family contracts for daemon handlers.
|
||||
//!
|
||||
//! These contracts are intentionally small and static. They make every
|
||||
//! resource family name its resource ID pattern, remote capabilities, and
|
||||
//! mutation or host-access points in one place while the implementation
|
||||
//! continues to live in the focused handler functions in `lib.rs` and
|
||||
//! `local_control.rs`.
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ResourceModuleContract {
|
||||
pub(crate) family: &'static str,
|
||||
pub(crate) resource_ids: &'static [&'static str],
|
||||
pub(crate) capabilities: &'static [&'static str],
|
||||
pub(crate) mutation_points: &'static [&'static str],
|
||||
}
|
||||
|
||||
pub(crate) const RESOURCE_MODULE_CONTRACTS: &[ResourceModuleContract] = &[
|
||||
ResourceModuleContract {
|
||||
family: "cas-file-root",
|
||||
resource_ids: &["resource:cas:local", "resource:cas-tree:<name>"],
|
||||
capabilities: &["cas.fetch"],
|
||||
mutation_points: &[
|
||||
"local CAS blob writes",
|
||||
"local file-root registration and scans",
|
||||
"remote file-root imports stored as peer-qualified metadata",
|
||||
"file-root apply writes only missing paths and records conflicts",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "kv",
|
||||
resource_ids: &["resource:kv:<name>"],
|
||||
capabilities: &["kv.read", "kv.write_prefix:<prefix>", "kv.write_key:<key>"],
|
||||
mutation_points: &[
|
||||
"local SQLite-backed KV create/set",
|
||||
"authorized sync imports newer or equal timestamp entries idempotently",
|
||||
"Iroh Documents mirror when native docs are enabled",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "db",
|
||||
resource_ids: &["resource:db:<name>"],
|
||||
capabilities: &["db.sync"],
|
||||
mutation_points: &[
|
||||
"local DB resource registration",
|
||||
"read-only schema and crsql_changes inspection",
|
||||
"authorized crsql_changes apply into compatible local databases",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "document",
|
||||
resource_ids: &["resource:document:<name>"],
|
||||
capabilities: &["document.read"],
|
||||
mutation_points: &[
|
||||
"local JSON document create/set",
|
||||
"authorized sync imports last-writer-wins or Automerge-compatible state",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "pubsub",
|
||||
resource_ids: &["resource:pubsub:<topic>"],
|
||||
capabilities: &["pubsub.publish", "pubsub.subscribe"],
|
||||
mutation_points: &[
|
||||
"daemon-lifetime local topic ring buffer",
|
||||
"authorized remote publish appends to the serving peer snapshot",
|
||||
"Iroh gossip broadcast when native gossip is enabled",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "pipe",
|
||||
resource_ids: &[
|
||||
"resource:pipe:<name>",
|
||||
"resource:pipe-tcp:<target>",
|
||||
"resource:pipe-unix:<target>",
|
||||
],
|
||||
capabilities: &["pipe.connect", "pipe.listen", "pipe.forward"],
|
||||
mutation_points: &[
|
||||
"daemon-lifetime pipe listener registry",
|
||||
"daemon-lifetime message ring buffer",
|
||||
"authorized loopback TCP or absolute Unix socket connection opening",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "ssh",
|
||||
resource_ids: &[
|
||||
"resource:ssh:certs",
|
||||
"resource:ssh:revocations",
|
||||
"resource:ssh-proxy:local",
|
||||
],
|
||||
capabilities: &[
|
||||
"ssh_cert.sync",
|
||||
"ssh_revocation.sync",
|
||||
"ssh_proxy.connect",
|
||||
"ssh_proxy.admin_shell",
|
||||
],
|
||||
mutation_points: &[
|
||||
"local SSH certificate request/certificate metadata",
|
||||
"local SSH revocation metadata",
|
||||
"authorized SSH proxy connects only to 127.0.0.1:22",
|
||||
"restricted admin shell executes only built-in geth commands",
|
||||
],
|
||||
},
|
||||
ResourceModuleContract {
|
||||
family: "overlay",
|
||||
resource_ids: &["resource:overlay:<name>"],
|
||||
capabilities: &["overlay.route"],
|
||||
mutation_points: &[
|
||||
"local overlay network membership metadata",
|
||||
"daemon-lifetime packet queue and optional TUN/Wintun injection",
|
||||
"authorized packet route over the overlay ALPN",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn contracts() -> &'static [ResourceModuleContract] {
|
||||
RESOURCE_MODULE_CONTRACTS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resource_module_contracts_cover_review_families() {
|
||||
let families = RESOURCE_MODULE_CONTRACTS
|
||||
.iter()
|
||||
.map(|contract| contract.family)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
|
||||
for expected in [
|
||||
"cas-file-root",
|
||||
"kv",
|
||||
"db",
|
||||
"document",
|
||||
"pubsub",
|
||||
"pipe",
|
||||
"ssh",
|
||||
"overlay",
|
||||
] {
|
||||
assert!(
|
||||
families.contains(expected),
|
||||
"missing resource contract for {expected}"
|
||||
);
|
||||
}
|
||||
|
||||
for contract in RESOURCE_MODULE_CONTRACTS {
|
||||
assert!(
|
||||
!contract.resource_ids.is_empty(),
|
||||
"{} must document resource IDs",
|
||||
contract.family
|
||||
);
|
||||
assert!(
|
||||
!contract.capabilities.is_empty(),
|
||||
"{} must document capabilities",
|
||||
contract.family
|
||||
);
|
||||
assert!(
|
||||
!contract.mutation_points.is_empty(),
|
||||
"{} must document mutation points",
|
||||
contract.family
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,13 @@ Within `geth-node`, daemon lifecycle code is separated from feature handlers:
|
|||
`daemon.rs` owns `geth daemon run` startup, local socket binding, shutdown
|
||||
signal handling, Iroh endpoint startup, the Iroh accept loop, and background
|
||||
live-sync task spawning. `local_control.rs` owns async local `ControlRequest`
|
||||
routing and safe trace-field classification before delegating to feature
|
||||
handlers. Runtime registries for pubsub, pipes, and overlays live behind narrow
|
||||
mutex-protected structs in `runtime.rs`. Command-family handler modules and
|
||||
protected peer-control feature dispatch remain separate refactor targets.
|
||||
routing, safe trace-field classification, and named peer/resource/local handler
|
||||
families before delegating to feature implementations. `resource_contracts.rs`
|
||||
records the review boundary for each resource family: resource ID patterns,
|
||||
capabilities, and mutation or host-access points. Runtime registries for
|
||||
pubsub, pipes, and overlays live behind narrow mutex-protected structs in
|
||||
`runtime.rs`. Protected peer-control ALPN dispatch remains a separate refactor
|
||||
target.
|
||||
|
||||
The local metadata store is SQLite product state. `geth-store` tracks a numeric
|
||||
`schema_version` in the `meta` table and applies ordered migrations up to the
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ behavior.
|
|||
ownership and locking rules.
|
||||
- `[x]` Existing daemon startup and status tests pass unchanged.
|
||||
|
||||
- `[~]` Extract local control routing.
|
||||
- `[x]` Extract local control routing.
|
||||
Acceptance criteria:
|
||||
- `[x]` Local `ControlRequest` dispatch is a routing layer, not the home of
|
||||
every feature implementation.
|
||||
- `[ ]` Each command family has a small handler module or function group.
|
||||
- `[x]` Each command family has a small handler module or function group.
|
||||
- `[x]` Local-only behavior remains covered by existing integration tests.
|
||||
|
||||
- `[~]` Extract protected peer-control routing.
|
||||
|
|
@ -64,13 +64,13 @@ behavior.
|
|||
repeating peer-card boilerplate.
|
||||
- `[x]` Remote request tests still prove discovery alone grants no access.
|
||||
|
||||
- `[ ]` Extract resource module handlers.
|
||||
- `[x]` Extract resource module handlers.
|
||||
Acceptance criteria:
|
||||
- `[ ]` CAS/file-root, KV, DB, document, pubsub, pipe, SSH, and overlay
|
||||
- `[x]` CAS/file-root, KV, DB, document, pubsub, pipe, SSH, and overlay
|
||||
handlers are separated enough that each can be reviewed independently.
|
||||
- `[ ]` Each module documents its resource IDs, capabilities, and mutation
|
||||
- `[x]` Each module documents its resource IDs, capabilities, and mutation
|
||||
points.
|
||||
- `[ ]` No generic `geth-common` crate is introduced.
|
||||
- `[x]` No generic `geth-common` crate is introduced.
|
||||
|
||||
- `[x]` Extract live-sync engine.
|
||||
Acceptance criteria:
|
||||
|
|
|
|||
Loading…
Reference in a new issue