From d067f4632f13c170595de796a9e1bfcf7790375c Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 6 Nov 2026 23:05:46 +0100 Subject: [PATCH] refactor(tools): rename rhema-tools-contract to rhema-claw-tools-contract The crate binds the claw to its client tools, not tools at large, so it carries the claw's name: package, lib and dep names, the mock's binary, and the conformance suite's use paths. Specs and README follow. The tools/contract directory keeps its name, and no reference to the old name remains. --- crates/rhema-claw/Cargo.toml | 2 +- crates/rhema-claw/src/http.rs | 18 ++++--- crates/rhema-claw/tests/http.rs | 74 +++++++++++++++-------------- tools/Cargo.lock | 26 +++++----- tools/common/Cargo.toml | 2 +- tools/common/src/lib.rs | 2 +- tools/contract/Cargo.toml | 2 +- tools/contract/tests/conformance.rs | 29 +++++++---- tools/notify/Cargo.toml | 2 +- tools/notify/src/main.rs | 4 +- tools/notify/tests/e2e.rs | 6 +-- 11 files changed, 91 insertions(+), 76 deletions(-) diff --git a/crates/rhema-claw/Cargo.toml b/crates/rhema-claw/Cargo.toml index 5550c1c..3695d74 100644 --- a/crates/rhema-claw/Cargo.toml +++ b/crates/rhema-claw/Cargo.toml @@ -49,7 +49,7 @@ rhema-rsd = { path = "../rhema-rsd" } # The tool contract: the agent-token allowlist and version the auth # middleware enforces (rhema-tools spec). The arrow runs claw -> # contract only — never the reverse. -rhema-tools-contract = { path = "../../tools/contract" } +rhema-claw-tools-contract = { path = "../../tools/contract" } # The resident tier's durable memory: its tools join the resolver chain rhema-memory = { path = "../rhema-memory" } # The shared ModelCard the backend registry's listings return diff --git a/crates/rhema-claw/src/http.rs b/crates/rhema-claw/src/http.rs index 70c9e6c..3d79365 100644 --- a/crates/rhema-claw/src/http.rs +++ b/crates/rhema-claw/src/http.rs @@ -42,13 +42,13 @@ use axum::routing::put; use axum::routing::{delete, get, post}; use axum::{Router, middleware}; use rhema_autonomous_agent::Driver; +use rhema_claw_tools_contract::Auth; +use rhema_claw_tools_contract::CONTRACT_HEADER; +use rhema_claw_tools_contract::CONTRACT_VERSION; use rhema_core::ImageMediaType; use rhema_rsd::{ DispatchError, Dispatcher, IngestResult, KeyRecord, KeyState, ResourceKey, SqliteStore, Store, }; -use rhema_tools_contract::Auth; -use rhema_tools_contract::CONTRACT_HEADER; -use rhema_tools_contract::CONTRACT_VERSION; use serde::{Deserialize, Deserializer}; use serde_json::{Value, json}; use tokio::sync::broadcast::error::RecvError; @@ -401,8 +401,10 @@ async fn require_auth(State(state): State, request: Request, next: Ne ) .into_response(); } - if rhema_tools_contract::agent_allowed(request.method().as_str(), request.uri().path()) - { + if rhema_claw_tools_contract::agent_allowed( + request.method().as_str(), + request.uri().path(), + ) { next.run(request).await } else { ( @@ -427,8 +429,10 @@ async fn require_auth(State(state): State, request: Request, next: Ne ) .into_response(); } - if rhema_tools_contract::ingest_allowed(request.method().as_str(), request.uri().path()) - { + if rhema_claw_tools_contract::ingest_allowed( + request.method().as_str(), + request.uri().path(), + ) { next.run(request).await } else { ( diff --git a/crates/rhema-claw/tests/http.rs b/crates/rhema-claw/tests/http.rs index 2af0737..e0b618e 100644 --- a/crates/rhema-claw/tests/http.rs +++ b/crates/rhema-claw/tests/http.rs @@ -1483,42 +1483,44 @@ async fn the_surface_speaks_the_tool_contract() { .expect("the daemon assembles"); let base = serve(&daemon).await; let client = reqwest::Client::new(); - let result = rhema_tools_contract::conformance(|request: rhema_tools_contract::CallRequest| { - let client = client.clone(); - let base = base.clone(); - async move { - let method = reqwest::Method::from_bytes(request.method.as_bytes()) - .map_err(|e| e.to_string())?; - let builder = client.request(method, format!("{base}{}", request.path)); - let builder = match request.auth { - rhema_tools_contract::Auth::None => builder, - rhema_tools_contract::Auth::Human => { - builder.header(reqwest::header::AUTHORIZATION, "Bearer human-token") - } - rhema_tools_contract::Auth::Agent => { - builder.header(reqwest::header::AUTHORIZATION, "Bearer agent-token") - } - rhema_tools_contract::Auth::Invalid => { - builder.header(reqwest::header::AUTHORIZATION, "Bearer a-stranger") - } - rhema_tools_contract::Auth::Ingest => { - builder.header(reqwest::header::AUTHORIZATION, "Bearer ingest-token") - } - }; - let builder = match request.contract { - Some(version) => builder.header("x-rhema-contract", version.to_string()), - None => builder, - }; - let builder = match request.body { - Some(body) => builder.json(&body), - None => builder, - }; - let response = builder.send().await.map_err(|e| e.to_string())?; - let status = response.status().as_u16(); - let body = response.json().await.unwrap_or(serde_json::json!({})); - Ok(rhema_tools_contract::CallResponse { status, body }) - } - }) + let result = rhema_claw_tools_contract::conformance( + |request: rhema_claw_tools_contract::CallRequest| { + let client = client.clone(); + let base = base.clone(); + async move { + let method = reqwest::Method::from_bytes(request.method.as_bytes()) + .map_err(|e| e.to_string())?; + let builder = client.request(method, format!("{base}{}", request.path)); + let builder = match request.auth { + rhema_claw_tools_contract::Auth::None => builder, + rhema_claw_tools_contract::Auth::Human => { + builder.header(reqwest::header::AUTHORIZATION, "Bearer human-token") + } + rhema_claw_tools_contract::Auth::Agent => { + builder.header(reqwest::header::AUTHORIZATION, "Bearer agent-token") + } + rhema_claw_tools_contract::Auth::Invalid => { + builder.header(reqwest::header::AUTHORIZATION, "Bearer a-stranger") + } + rhema_claw_tools_contract::Auth::Ingest => { + builder.header(reqwest::header::AUTHORIZATION, "Bearer ingest-token") + } + }; + let builder = match request.contract { + Some(version) => builder.header("x-rhema-contract", version.to_string()), + None => builder, + }; + let builder = match request.body { + Some(body) => builder.json(&body), + None => builder, + }; + let response = builder.send().await.map_err(|e| e.to_string())?; + let status = response.status().as_u16(); + let body = response.json().await.unwrap_or(serde_json::json!({})); + Ok(rhema_claw_tools_contract::CallResponse { status, body }) + } + }, + ) .await; assert_eq!(result, Ok(()), "the daemon speaks the tool contract"); } diff --git a/tools/Cargo.lock b/tools/Cargo.lock index a16ec03..82897cb 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -466,8 +466,8 @@ version = "0.1.0" dependencies = [ "axum", "clap", + "rhema-claw-tools-contract", "rhema-tools-common", - "rhema-tools-contract", "serde", "serde_json", "tokio", @@ -525,18 +525,7 @@ dependencies = [ ] [[package]] -name = "rhema-tools-common" -version = "0.1.0" -dependencies = [ - "rhema-tools-contract", - "serde", - "serde_json", - "thiserror", - "ureq", -] - -[[package]] -name = "rhema-tools-contract" +name = "rhema-claw-tools-contract" version = "0.1.0" dependencies = [ "serde", @@ -544,6 +533,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "rhema-tools-common" +version = "0.1.0" +dependencies = [ + "rhema-claw-tools-contract", + "serde", + "serde_json", + "thiserror", + "ureq", +] + [[package]] name = "ryu" version = "1.0.23" diff --git a/tools/common/Cargo.toml b/tools/common/Cargo.toml index 19888de..0b78737 100644 --- a/tools/common/Cargo.toml +++ b/tools/common/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true authors.workspace = true [dependencies] -rhema-tools-contract = { path = "../contract" } +rhema-claw-tools-contract = { path = "../contract" } serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/tools/common/src/lib.rs b/tools/common/src/lib.rs index 6093c3c..1e26d26 100644 --- a/tools/common/src/lib.rs +++ b/tools/common/src/lib.rs @@ -7,7 +7,7 @@ use std::io::Write; -use rhema_tools_contract::{ApiError, CONTRACT_HEADER, CONTRACT_VERSION}; +use rhema_claw_tools_contract::{ApiError, CONTRACT_HEADER, CONTRACT_VERSION}; use serde::Serialize; use serde_json::Value; diff --git a/tools/contract/Cargo.toml b/tools/contract/Cargo.toml index 9c59b43..7029abf 100644 --- a/tools/contract/Cargo.toml +++ b/tools/contract/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rhema-tools-contract" +name = "rhema-claw-tools-contract" description = "The wire contract between the rhema claw daemon and its thin client tools: routes, shapes, the agent-token allowlist, and the conformance suite" version.workspace = true edition.workspace = true diff --git a/tools/contract/tests/conformance.rs b/tools/contract/tests/conformance.rs index 0c4d3c1..b9bf50c 100644 --- a/tools/contract/tests/conformance.rs +++ b/tools/contract/tests/conformance.rs @@ -2,7 +2,7 @@ //! The daemon's real router runs the same suite in its test tree — //! same results is the definition of implementing the contract. -use rhema_tools_contract::{CallRequest, MockClaw, conformance}; +use rhema_claw_tools_contract::{CallRequest, MockClaw, conformance}; #[tokio::test] async fn the_suite_passes_against_the_mock_router() { @@ -28,31 +28,40 @@ async fn an_open_mock_fails_the_suite() { #[tokio::test] async fn the_agent_allowlist_reads_the_documented_routes() { - assert!(rhema_tools_contract::agent_allowed("GET", "/v1/watches")); - assert!(rhema_tools_contract::agent_allowed( + assert!(rhema_claw_tools_contract::agent_allowed( + "GET", + "/v1/watches" + )); + assert!(rhema_claw_tools_contract::agent_allowed( "DELETE", "/v1/watches/github/acme/widgets" )); - assert!(rhema_tools_contract::agent_allowed( + assert!(rhema_claw_tools_contract::agent_allowed( "POST", "/v1/notifications" )); - assert!(rhema_tools_contract::agent_allowed( + assert!(rhema_claw_tools_contract::agent_allowed( "DELETE", "/v1/tasks/12" )); - assert!(!rhema_tools_contract::agent_allowed("POST", "/v1/ingest")); - assert!(!rhema_tools_contract::agent_allowed( + assert!(!rhema_claw_tools_contract::agent_allowed( + "POST", + "/v1/ingest" + )); + assert!(!rhema_claw_tools_contract::agent_allowed( "POST", "/v1/consents/c-1" )); - assert!(!rhema_tools_contract::agent_allowed("GET", "/v1/consents")); - assert!(!rhema_tools_contract::agent_allowed( + assert!(!rhema_claw_tools_contract::agent_allowed( + "GET", + "/v1/consents" + )); + assert!(!rhema_claw_tools_contract::agent_allowed( "DELETE", "/v1/phones/p1" )); assert!( - !rhema_tools_contract::agent_allowed("GET", "/v1/watches/github/acme/widgets"), + !rhema_claw_tools_contract::agent_allowed("GET", "/v1/watches/github/acme/widgets"), "only DELETE reaches subpaths of /v1/watches" ); } diff --git a/tools/notify/Cargo.toml b/tools/notify/Cargo.toml index b7efc09..2fe5837 100644 --- a/tools/notify/Cargo.toml +++ b/tools/notify/Cargo.toml @@ -14,7 +14,7 @@ path = "src/main.rs" [dependencies] rhema-tools-common = { path = "../common" } -rhema-tools-contract = { path = "../contract" } +rhema-claw-tools-contract = { path = "../contract" } clap.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/tools/notify/src/main.rs b/tools/notify/src/main.rs index 65634b1..09d5a2f 100644 --- a/tools/notify/src/main.rs +++ b/tools/notify/src/main.rs @@ -5,7 +5,7 @@ use clap::{Parser, Subcommand}; use rhema_tools_common::{Client, Config, fail, print}; -use rhema_tools_contract::NotificationPush; +use rhema_claw_tools_contract::NotificationPush; /// Push a notification to the user's devices. #[derive(Parser)] @@ -49,7 +49,7 @@ fn main() { body, device, }; - match client.call("POST", rhema_tools_contract::NOTIFICATIONS, Some(&push)) { + match client.call("POST", rhema_claw_tools_contract::NOTIFICATIONS, Some(&push)) { Ok(record) => print(&record), Err(error) => fail(error), } diff --git a/tools/notify/tests/e2e.rs b/tools/notify/tests/e2e.rs index 9c66c5a..95e14e3 100644 --- a/tools/notify/tests/e2e.rs +++ b/tools/notify/tests/e2e.rs @@ -10,7 +10,7 @@ use axum::extract::State; use axum::http::{HeaderMap, StatusCode}; use axum::routing::get; use rhema_tools_common::{Client, Config, ToolError}; -use rhema_tools_contract::NotificationPush; +use rhema_claw_tools_contract::NotificationPush; use serde_json::{Value, json}; /// What the fake daemon recorded, for the test to assert on. @@ -82,7 +82,7 @@ async fn a_push_rides_the_contract_and_prints_the_row() { let row = client .call( "POST", - rhema_tools_contract::NOTIFICATIONS, + rhema_claw_tools_contract::NOTIFICATIONS, Some(&NotificationPush { title: "CI is red".into(), body: "main failed".into(), @@ -111,7 +111,7 @@ async fn a_refused_call_maps_to_denied() { let error = client .call( "POST", - rhema_tools_contract::NOTIFICATIONS, + rhema_claw_tools_contract::NOTIFICATIONS, Some(&NotificationPush { title: "x".into(), body: "y".into(),