Skip to content

Commit de38762

Browse files
maxholmanclaude
andcommitted
refactor: rename hint_set → role across MCP, REST, and OpenAPI
Every UAT session flagged hint_set as confusing — "hints" is protocol jargon. The REPL and CLI already use "role" (role entry, role prefer exit, role auto). This aligns MCP and REST to match: - MCP: hint_set → role, hint_set_auto → role_auto - REST: PUT/DELETE /hints → PUT/DELETE /role - OpenAPI: HintSetRequest → RoleSetRequest, operationIds updated - level parameter now defaults to "fixed" (matches REPL: "role entry" means "role fixed entry") Also: removed confusing connect=false/listen=false from info output capabilities — the address fields already show this information. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b91a1c5 commit de38762

8 files changed

Lines changed: 66 additions & 77 deletions

File tree

AGENTS.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,13 @@ add, remove, or change any route, request body, or response shape in
6161
(source, destination, target) and concrete entities (peer, tun, device).
6262
- Explicit identifiers: Code and logs must use explicit, fixed IDs (e.g., peer1,
6363
dmz1, nodeA). Do not use network roles as variable names.
64-
- CLI consistency: eg REPL route add examples must explicitly include the --name
65-
<peer> flag on exit/relay commands to ensure routing examples remain
66-
self-documenting.
64+
- **Interface parity (STRICT):** All interfaces — REPL, CLI, MCP, REST API, and
65+
OpenAPI spec — must expose identical operations with identical names,
66+
parameters, and defaults. The REPL is the reference implementation. When
67+
adding or changing any command, update ALL interfaces in the same commit.
68+
Never create interface-specific names (e.g. `hint_set` in MCP when the REPL
69+
uses `role`). Never split into separate tools what the REPL handles as one
70+
command.
6771

6872
## TRIPLE PR Process for lead-agent only
6973

crates/api/src/handlers.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,21 @@ pub struct ListenResponse {
125125
pub fingerprint: String,
126126
}
127127

128-
/// Set hint request body.
128+
/// Role set request body.
129+
///
130+
/// `role` is always required. `level` defaults to `"fixed"` if omitted,
131+
/// matching the REPL behaviour where `role entry` means `role fixed entry`.
129132
#[derive(Debug, Deserialize)]
130-
pub struct HintSetRequestBody {
133+
pub struct RoleSetRequestBody {
134+
#[serde(default = "default_level")]
131135
pub level: String,
132136
pub role: String,
133137
}
134138

139+
fn default_level() -> String {
140+
"fixed".to_string()
141+
}
142+
135143
/// Logs query parameters.
136144
#[derive(Debug, Deserialize)]
137145
pub struct LogsQuery {
@@ -655,10 +663,14 @@ pub async fn shutdown(State(state): State<ApiState>) -> (StatusCode, Json<Succes
655663
}
656664
}
657665

658-
pub async fn hint_set(
666+
pub async fn role_set(
659667
State(state): State<ApiState>,
660-
Json(req): Json<HintSetRequestBody>,
668+
Json(req): Json<RoleSetRequestBody>,
661669
) -> (StatusCode, Json<SuccessResponse>) {
670+
if req.role == "auto" {
671+
return role_auto(State(state)).await;
672+
}
673+
662674
let level = match req.level.as_str() {
663675
"prefer" => HintLevel::Prefer,
664676
"exclude" => HintLevel::Exclude,
@@ -669,7 +681,7 @@ pub async fn hint_set(
669681
Json(SuccessResponse {
670682
success: false,
671683
message: Some(format!(
672-
"invalid hint level '{}' (expected: prefer, exclude, fixed)",
684+
"invalid level '{}' (expected: prefer, exclude, fixed)",
673685
req.level
674686
)),
675687
}),
@@ -686,7 +698,7 @@ pub async fn hint_set(
686698
Json(SuccessResponse {
687699
success: false,
688700
message: Some(format!(
689-
"invalid role '{}' (expected: entry, exit, relay)",
701+
"invalid role '{}' (expected: auto, entry, exit, relay)",
690702
req.role
691703
)),
692704
}),
@@ -738,7 +750,7 @@ pub async fn hint_set(
738750
}
739751
}
740752

741-
pub async fn hint_set_auto(State(state): State<ApiState>) -> (StatusCode, Json<SuccessResponse>) {
753+
pub async fn role_auto(State(state): State<ApiState>) -> (StatusCode, Json<SuccessResponse>) {
742754
let resp = state
743755
.ipc
744756
.lock()

crates/api/src/lib.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,7 @@ pub fn router(state: State) -> Router {
7777
.route("/listen", post(handlers::listen))
7878
.route("/disconnect", post(handlers::disconnect))
7979
.route("/shutdown", post(handlers::shutdown))
80-
.route(
81-
"/hints",
82-
put(handlers::hint_set).delete(handlers::hint_set_auto),
83-
)
80+
.route("/role", put(handlers::role_set))
8481
.layer(middleware::from_fn(move |req, next| {
8582
let auth = auth.clone();
8683
auth::middleware(auth, req, next)

crates/cli/src/output.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,7 @@ pub fn print_response(resp: &ManagementResponse) -> Result<(), CtlError> {
115115
if !s.listen_addr.is_empty() {
116116
println!("{:<18} {}", "listen addr:", s.listen_addr);
117117
}
118-
println!(
119-
"{:<18} tun={} listen={} connect={}",
120-
"capabilities:", s.tun_capable, s.listening, s.connecting
121-
);
118+
println!("{:<18} {}", "tun:", s.tun_capable);
122119
println!("{:<18} {}", "version:", s.version);
123120
println!("{:<18} {}", "uptime:", uptime);
124121
}

crates/daemon/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,7 @@ pub fn start_node(
140140
ModeConfig::Auto(cfg) => match &cfg.hint {
141141
Some(hint) if hint.level == wallhack_wire::data::HintLevel::Fixed as i32 => {
142142
wallhack_wire::data::NodeRole::try_from(hint.target)
143-
.ok()
144-
.map(NodeRole::from)
145-
.unwrap_or(NodeRole::Indeterminate)
143+
.map_or(NodeRole::Indeterminate, NodeRole::from)
146144
}
147145
_ => NodeRole::Indeterminate,
148146
},

crates/mcp/src/convert.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ pub fn format_response(resp: &ManagementResponse) -> Result<String, String> {
1818
if !s.listen_addr.is_empty() {
1919
let _ = writeln!(out, "listen addr: {}", s.listen_addr);
2020
}
21-
let _ = writeln!(
22-
out,
23-
"capabilities: tun={} listen={} connect={}",
24-
s.tun_capable, s.listening, s.connecting,
25-
);
21+
let _ = writeln!(out, "tun: {}", s.tun_capable);
2622
let _ = writeln!(out, "version: {}", s.version);
2723
let _ = writeln!(out, "uptime: {}", format_uptime(s.uptime_ms));
2824
Ok(out)

crates/mcp/src/tools.rs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,16 @@ pub struct LogsParams {
4343
}
4444

4545
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
46-
pub struct HintSetParams {
47-
/// How strongly to apply the hint: "prefer" (soft), "exclude" (avoid), or "fixed" (force)
48-
pub level: String,
49-
/// Target role: "entry", "exit", or "relay"
46+
pub struct RoleParams {
47+
/// "auto" to clear, or a role: "entry", "exit", "relay"
5048
pub role: String,
49+
/// How to apply (ignored when role is "auto"): "fixed" (default), "prefer", or "exclude"
50+
#[serde(default = "default_role_level")]
51+
pub level: String,
52+
}
53+
54+
fn default_role_level() -> String {
55+
"fixed".to_string()
5156
}
5257

5358
/// Wallhack MCP server — exposes daemon management as MCP tools.
@@ -182,19 +187,25 @@ impl WallhackServer {
182187
}
183188

184189
#[tool(
185-
description = "Set a role hint to influence auto-negotiation: \"prefer\" makes the role more likely, \"exclude\" prevents it, \"fixed\" forces it"
190+
description = "Set or clear this node's role. Examples: role entry (force entry), role prefer exit (soft preference), role exclude relay (avoid relay), role auto (clear all, return to negotiation)"
186191
)]
187-
async fn hint_set(
192+
async fn role(
188193
&self,
189-
Parameters(params): Parameters<HintSetParams>,
194+
Parameters(params): Parameters<RoleParams>,
190195
) -> Result<String, rmcp::ErrorData> {
196+
if params.role == "auto" {
197+
return ipc_call(management_request::Request::HintSetAuto(
198+
HintSetAutoRequest {},
199+
))
200+
.await;
201+
}
191202
let level = match params.level.as_str() {
192203
"prefer" => HintLevel::Prefer,
193204
"exclude" => HintLevel::Exclude,
194205
"fixed" => HintLevel::Fixed,
195206
other => {
196207
return Err(rmcp::ErrorData::invalid_params(
197-
format!("invalid hint level '{other}' (expected: prefer, exclude, fixed)"),
208+
format!("invalid level '{other}' (expected: prefer, exclude, fixed)"),
198209
None,
199210
));
200211
}
@@ -205,7 +216,7 @@ impl WallhackServer {
205216
"relay" => NodeRole::Relay,
206217
other => {
207218
return Err(rmcp::ErrorData::invalid_params(
208-
format!("invalid role '{other}' (expected: entry, exit, relay)"),
219+
format!("invalid role '{other}' (expected: auto, entry, exit, relay)"),
209220
None,
210221
));
211222
}
@@ -216,16 +227,6 @@ impl WallhackServer {
216227
}))
217228
.await
218229
}
219-
220-
#[tool(
221-
description = "Remove all role hints and return to automatic role negotiation based on peer capabilities. Use to undo a previous hint_set."
222-
)]
223-
async fn hint_set_auto(&self) -> Result<String, rmcp::ErrorData> {
224-
ipc_call(management_request::Request::HintSetAuto(
225-
HintSetAutoRequest {},
226-
))
227-
.await
228-
}
229230
}
230231

231232
impl rmcp::ServerHandler for WallhackServer {

website/src/data/openapi.json

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -282,19 +282,20 @@
282282
}
283283
}
284284
},
285-
"HintSetRequest": {
285+
"RoleSetRequest": {
286286
"type": "object",
287-
"required": ["level", "role"],
287+
"required": ["role"],
288288
"properties": {
289-
"level": {
289+
"role": {
290290
"type": "string",
291-
"enum": ["prefer", "exclude", "fixed"],
292-
"description": "Hint strength: prefer (soft), exclude (medium), fixed (hard)."
291+
"enum": ["auto", "entry", "exit", "relay"],
292+
"description": "Target role, or \"auto\" to clear all preferences."
293293
},
294-
"role": {
294+
"level": {
295295
"type": "string",
296-
"enum": ["entry", "exit", "relay"],
297-
"description": "Role to apply the hint to."
296+
"enum": ["prefer", "exclude", "fixed"],
297+
"default": "fixed",
298+
"description": "How to apply: fixed (default, force), prefer (soft), exclude (avoid)."
298299
}
299300
}
300301
}
@@ -623,47 +624,30 @@
623624
}
624625
}
625626
},
626-
"/hints": {
627+
"/role": {
627628
"put": {
628-
"summary": "Set role hint",
629-
"description": "Configures a negotiation hint to influence auto-role resolution.",
630-
"operationId": "hintSet",
629+
"summary": "Set role",
630+
"description": "Set or clear this node's role. Use role=auto to clear all preferences and return to negotiation.",
631+
"operationId": "roleSet",
631632
"security": [{ "basicAuth": [] }],
632633
"requestBody": {
633634
"required": true,
634635
"content": {
635636
"application/json": {
636-
"schema": { "$ref": "#/components/schemas/HintSetRequest" }
637+
"schema": { "$ref": "#/components/schemas/RoleSetRequest" }
637638
}
638639
}
639640
},
640641
"responses": {
641642
"200": {
642-
"description": "Hint applied.",
643-
"content": {
644-
"application/json": {
645-
"schema": { "$ref": "#/components/schemas/SuccessResponse" }
646-
}
647-
}
648-
},
649-
"400": { "description": "Invalid hint level or role." },
650-
"401": { "description": "Unauthorized." }
651-
}
652-
},
653-
"delete": {
654-
"summary": "Auto-negotiate",
655-
"description": "Returns to pure capability-based negotiation by removing all role hints.",
656-
"operationId": "hintSetAuto",
657-
"security": [{ "basicAuth": [] }],
658-
"responses": {
659-
"200": {
660-
"description": "Hints cleared.",
643+
"description": "Role applied.",
661644
"content": {
662645
"application/json": {
663646
"schema": { "$ref": "#/components/schemas/SuccessResponse" }
664647
}
665648
}
666649
},
650+
"400": { "description": "Invalid level or role." },
667651
"401": { "description": "Unauthorized." }
668652
}
669653
}

0 commit comments

Comments
 (0)