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;
|
pub mod doctor;
|
||||||
mod local_control;
|
mod local_control;
|
||||||
mod peer_client;
|
mod peer_client;
|
||||||
|
mod resource_contracts;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
|
@ -5588,6 +5589,7 @@ pub fn handle_request(
|
||||||
node: &LocalNode,
|
node: &LocalNode,
|
||||||
request: ControlRequest,
|
request: ControlRequest,
|
||||||
) -> Result<ControlResponse, NodeError> {
|
) -> Result<ControlResponse, NodeError> {
|
||||||
|
debug_assert!(!resource_contracts::contracts().is_empty());
|
||||||
let store = Store::open(&node.paths.metadata_db())?;
|
let store = Store::open(&node.paths.metadata_db())?;
|
||||||
match request {
|
match request {
|
||||||
ControlRequest::Status => {
|
ControlRequest::Status => {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,55 @@ pub async fn handle_request_async(
|
||||||
stream = trace.stream.as_deref().unwrap_or(""),
|
stream = trace.stream.as_deref().unwrap_or(""),
|
||||||
"control request started"
|
"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::PeerCardExport { out } => export_peer_card(node, out, true).await,
|
||||||
ControlRequest::PeerPing { node: peer_node } => peer_ping(node, &peer_node).await,
|
ControlRequest::PeerPing { node: peer_node } => peer_ping(node, &peer_node).await,
|
||||||
ControlRequest::PeerAuthCheck {
|
ControlRequest::PeerAuthCheck {
|
||||||
|
|
@ -36,6 +84,16 @@ pub async fn handle_request_async(
|
||||||
ControlRequest::NodeEnrollSync { owner_node } => {
|
ControlRequest::NodeEnrollSync { owner_node } => {
|
||||||
node_enrollment_sync_from_owner(node, &owner_node).await
|
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 {
|
ControlRequest::CasFetch {
|
||||||
node: peer_node,
|
node: peer_node,
|
||||||
hash,
|
hash,
|
||||||
|
|
@ -124,61 +182,46 @@ pub async fn handle_request_async(
|
||||||
mtu,
|
mtu,
|
||||||
} => overlay_runtime_up(node, name, bearer_secret, mtu).await,
|
} => overlay_runtime_up(node, name, bearer_secret, mtu).await,
|
||||||
ControlRequest::OverlayDown { name } => overlay_runtime_down(node, &name),
|
ControlRequest::OverlayDown { name } => overlay_runtime_down(node, &name),
|
||||||
other => {
|
other => return Ok(AsyncRoute::Continue(other)),
|
||||||
let response = handle_request(node, other)?;
|
}?;
|
||||||
match &response {
|
Ok(AsyncRoute::Handled(Box::new(response)))
|
||||||
ControlResponse::CasAdded { hash, .. } => {
|
}
|
||||||
mirror_local_blob_to_iroh_blobs(node, hash).await?;
|
|
||||||
}
|
async fn route_local_with_native_mirrors(
|
||||||
ControlResponse::CasPrivateAdded { encrypted_hash, .. } => {
|
node: &LocalNode,
|
||||||
mirror_local_blob_to_iroh_blobs(node, encrypted_hash).await?;
|
request: ControlRequest,
|
||||||
}
|
) -> Result<ControlResponse, NodeError> {
|
||||||
ControlResponse::KvCreated { kv } => {
|
let response = handle_request(node, request)?;
|
||||||
mirror_kv_store_to_iroh_docs(node, kv.name.as_str()).await?;
|
match &response {
|
||||||
}
|
ControlResponse::CasAdded { hash, .. } => {
|
||||||
ControlResponse::KvSet { entry } => {
|
mirror_local_blob_to_iroh_blobs(node, hash).await?;
|
||||||
let name = entry
|
|
||||||
.store
|
|
||||||
.as_str()
|
|
||||||
.strip_prefix("kv:")
|
|
||||||
.unwrap_or(entry.store.as_str());
|
|
||||||
mirror_kv_store_to_iroh_docs(node, name).await?;
|
|
||||||
}
|
|
||||||
ControlResponse::PubsubPublished { message } => {
|
|
||||||
broadcast_pubsub_gossip(
|
|
||||||
node,
|
|
||||||
message.topic.as_str(),
|
|
||||||
message.message.as_str(),
|
|
||||||
Vec::new(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
};
|
ControlResponse::CasPrivateAdded { encrypted_hash, .. } => {
|
||||||
match &result {
|
mirror_local_blob_to_iroh_blobs(node, encrypted_hash).await?;
|
||||||
Ok(_) => tracing::info!(
|
}
|
||||||
command = trace.command,
|
ControlResponse::KvCreated { kv } => {
|
||||||
peer_node = trace.peer_node.as_deref().unwrap_or(""),
|
mirror_kv_store_to_iroh_docs(node, kv.name.as_str()).await?;
|
||||||
resource = trace.resource.as_deref().unwrap_or(""),
|
}
|
||||||
capability = trace.capability.as_deref().unwrap_or(""),
|
ControlResponse::KvSet { entry } => {
|
||||||
stream = trace.stream.as_deref().unwrap_or(""),
|
let name = entry
|
||||||
"control request completed"
|
.store
|
||||||
),
|
.as_str()
|
||||||
Err(error) => tracing::warn!(
|
.strip_prefix("kv:")
|
||||||
command = trace.command,
|
.unwrap_or(entry.store.as_str());
|
||||||
peer_node = trace.peer_node.as_deref().unwrap_or(""),
|
mirror_kv_store_to_iroh_docs(node, name).await?;
|
||||||
resource = trace.resource.as_deref().unwrap_or(""),
|
}
|
||||||
capability = trace.capability.as_deref().unwrap_or(""),
|
ControlResponse::PubsubPublished { message } => {
|
||||||
stream = trace.stream.as_deref().unwrap_or(""),
|
broadcast_pubsub_gossip(
|
||||||
error_code = node_error_code(error),
|
node,
|
||||||
%error,
|
message.topic.as_str(),
|
||||||
"control request failed"
|
message.message.as_str(),
|
||||||
),
|
Vec::new(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
result
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
|
|
||||||
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
|
`daemon.rs` owns `geth daemon run` startup, local socket binding, shutdown
|
||||||
signal handling, Iroh endpoint startup, the Iroh accept loop, and background
|
signal handling, Iroh endpoint startup, the Iroh accept loop, and background
|
||||||
live-sync task spawning. `local_control.rs` owns async local `ControlRequest`
|
live-sync task spawning. `local_control.rs` owns async local `ControlRequest`
|
||||||
routing and safe trace-field classification before delegating to feature
|
routing, safe trace-field classification, and named peer/resource/local handler
|
||||||
handlers. Runtime registries for pubsub, pipes, and overlays live behind narrow
|
families before delegating to feature implementations. `resource_contracts.rs`
|
||||||
mutex-protected structs in `runtime.rs`. Command-family handler modules and
|
records the review boundary for each resource family: resource ID patterns,
|
||||||
protected peer-control feature dispatch remain separate refactor targets.
|
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
|
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
|
`schema_version` in the `meta` table and applies ordered migrations up to the
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,11 @@ behavior.
|
||||||
ownership and locking rules.
|
ownership and locking rules.
|
||||||
- `[x]` Existing daemon startup and status tests pass unchanged.
|
- `[x]` Existing daemon startup and status tests pass unchanged.
|
||||||
|
|
||||||
- `[~]` Extract local control routing.
|
- `[x]` Extract local control routing.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[x]` Local `ControlRequest` dispatch is a routing layer, not the home of
|
- `[x]` Local `ControlRequest` dispatch is a routing layer, not the home of
|
||||||
every feature implementation.
|
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.
|
- `[x]` Local-only behavior remains covered by existing integration tests.
|
||||||
|
|
||||||
- `[~]` Extract protected peer-control routing.
|
- `[~]` Extract protected peer-control routing.
|
||||||
|
|
@ -64,13 +64,13 @@ behavior.
|
||||||
repeating peer-card boilerplate.
|
repeating peer-card boilerplate.
|
||||||
- `[x]` Remote request tests still prove discovery alone grants no access.
|
- `[x]` Remote request tests still prove discovery alone grants no access.
|
||||||
|
|
||||||
- `[ ]` Extract resource module handlers.
|
- `[x]` Extract resource module handlers.
|
||||||
Acceptance criteria:
|
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.
|
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.
|
points.
|
||||||
- `[ ]` No generic `geth-common` crate is introduced.
|
- `[x]` No generic `geth-common` crate is introduced.
|
||||||
|
|
||||||
- `[x]` Extract live-sync engine.
|
- `[x]` Extract live-sync engine.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue