Skip to content

Commit 467775b

Browse files
hyperpolymathclaude
andcommitted
feat: add Groove protocol integration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e1dfebb commit 467775b

4 files changed

Lines changed: 199 additions & 4 deletions

File tree

src/cli/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ pub enum Commands {
127127
#[clap(subcommand)]
128128
action: RsrAction,
129129
},
130+
131+
/// Start the groove discovery server
132+
///
133+
/// Runs a lightweight HTTP server exposing conflow's capabilities via
134+
/// the Gossamer groove protocol. Other groove-aware systems (PanLL,
135+
/// Gossamer, etc.) can discover conflow by probing
136+
/// GET /.well-known/groove on the configured port.
137+
Serve {
138+
/// Port to bind the groove server to
139+
#[clap(short, long, default_value = "7700")]
140+
port: u16,
141+
},
130142
}
131143

132144
/// RSR integration actions

src/groove.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
//! Gossamer Groove endpoint for conflow.
5+
//!
6+
//! Exposes conflow's config-orchestration capabilities via the groove
7+
//! discovery protocol. Any groove-aware system (Gossamer, PanLL, etc.)
8+
//! can discover conflow by probing GET /.well-known/groove on port 7700.
9+
//!
10+
//! conflow works standalone as a CLI tool. When groove consumers connect,
11+
//! they gain access to conflow's pipeline orchestration, config validation,
12+
//! and RSR compliance checking.
13+
//!
14+
//! The groove connector types are formally verified in Gossamer's Groove.idr:
15+
//! - IsSubset proves consumers can only connect if conflow satisfies their needs
16+
//! - GrooveHandle is linear: consumers MUST disconnect (no dangling grooves)
17+
//!
18+
//! ## Groove Protocol
19+
//!
20+
//! - `GET /.well-known/groove` — Capability manifest (JSON)
21+
//! - `GET /health` — Simple health check
22+
//!
23+
//! ## Capabilities Offered
24+
//!
25+
//! - `config-orchestration` — Pipeline execution for CUE, Nickel, and config workflows
26+
//!
27+
//! ## Capabilities Consumed (enhanced when available)
28+
//!
29+
//! - `octad-storage` (from VeriSimDB) — Persist pipeline results and compliance reports
30+
31+
use std::net::SocketAddr;
32+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
33+
use tokio::net::TcpListener;
34+
use tracing::{error, info, warn};
35+
36+
/// Maximum HTTP request size (16 KiB).
37+
const MAX_REQUEST_SIZE: usize = 16 * 1024;
38+
39+
/// Run the groove discovery HTTP server on the given port.
40+
///
41+
/// This is a minimal HTTP server that handles only the groove protocol
42+
/// endpoints. Invoked by `conflow serve --port 7700`.
43+
pub async fn run(port: u16) -> miette::Result<()> {
44+
let addr: SocketAddr = format!("127.0.0.1:{}", port)
45+
.parse()
46+
.map_err(|e| miette::miette!("Failed to parse bind address: {}", e))?;
47+
48+
let listener = TcpListener::bind(addr)
49+
.await
50+
.map_err(|e| miette::miette!("Failed to bind to {}: {}", addr, e))?;
51+
info!("Groove endpoint listening on {}", addr);
52+
info!("Probe: curl http://localhost:{}/.well-known/groove", port);
53+
54+
loop {
55+
match listener.accept().await {
56+
Ok((mut stream, _peer)) => {
57+
tokio::spawn(async move {
58+
if let Err(e) = handle_request(&mut stream).await {
59+
warn!("Groove request error: {}", e);
60+
}
61+
});
62+
}
63+
Err(e) => {
64+
error!("Groove accept error: {}", e);
65+
}
66+
}
67+
}
68+
}
69+
70+
/// Build the groove manifest JSON for conflow.
71+
fn manifest(port: u16) -> String {
72+
format!(
73+
r#"{{
74+
"groove_version": "1",
75+
"service_id": "conflow",
76+
"service_version": "{}",
77+
"capabilities": {{
78+
"config_orchestration": {{
79+
"type": "config-orchestration",
80+
"description": "Pipeline orchestration for CUE, Nickel, and configuration validation workflows",
81+
"protocol": "http",
82+
"endpoint": "/api/v1/config",
83+
"requires_auth": false,
84+
"panel_compatible": true
85+
}}
86+
}},
87+
"consumes": ["octad-storage"],
88+
"endpoints": {{
89+
"api": "http://localhost:{}/api/v1",
90+
"health": "http://localhost:{}/health"
91+
}},
92+
"health": "/health",
93+
"applicability": ["individual", "team"]
94+
}}"#,
95+
env!("CARGO_PKG_VERSION"),
96+
port,
97+
port
98+
)
99+
}
100+
101+
/// Handle a single groove HTTP request.
102+
async fn handle_request(
103+
stream: &mut tokio::net::TcpStream,
104+
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
105+
let mut buf = vec![0u8; MAX_REQUEST_SIZE];
106+
let n = stream.read(&mut buf).await?;
107+
let request = std::str::from_utf8(&buf[..n])?;
108+
109+
let first_line = request.lines().next().unwrap_or("");
110+
let parts: Vec<&str> = first_line.split_whitespace().collect();
111+
if parts.len() < 2 {
112+
send_response(stream, 400, "text/plain", "Bad Request").await?;
113+
return Ok(());
114+
}
115+
116+
let method = parts[0];
117+
let path = parts[1];
118+
119+
match (method, path) {
120+
// GET /.well-known/groove — Return the capability manifest.
121+
("GET", "/.well-known/groove") => {
122+
let json = manifest(7700);
123+
send_response(stream, 200, "application/json", &json).await?;
124+
}
125+
126+
// GET /health — Simple health check.
127+
("GET", "/health") => {
128+
send_response(
129+
stream,
130+
200,
131+
"application/json",
132+
r#"{"status":"ok","service":"conflow"}"#,
133+
)
134+
.await?;
135+
}
136+
137+
// Unknown route.
138+
_ => {
139+
send_response(stream, 404, "text/plain", "Not Found").await?;
140+
}
141+
}
142+
143+
Ok(())
144+
}
145+
146+
/// Send an HTTP response with the given content type and body.
147+
async fn send_response(
148+
stream: &mut tokio::net::TcpStream,
149+
status: u16,
150+
content_type: &str,
151+
body: &str,
152+
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
153+
let status_text = match status {
154+
200 => "OK",
155+
400 => "Bad Request",
156+
404 => "Not Found",
157+
_ => "Unknown",
158+
};
159+
let response = format!(
160+
"HTTP/1.0 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
161+
status,
162+
status_text,
163+
content_type,
164+
body.len(),
165+
body
166+
);
167+
stream.write_all(response.as_bytes()).await?;
168+
Ok(())
169+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub mod cache;
2020
pub mod cli;
2121
pub mod errors;
2222
pub mod executors;
23+
pub mod groove;
2324
pub mod pipeline;
2425
pub mod rsr;
2526
pub mod utils;

src/main.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,23 @@ async fn main() -> Result<()> {
5252
no_cache,
5353
dry_run,
5454
} => conflow::cli::run::run(pipeline, stage, no_cache, dry_run, cli.verbose).await,
55-
// ... [other commands: Watch, Validate, Cache, Graph, Rsr]
56-
_ => {
57-
// Logic for remaining commands implemented in their respective modules.
58-
Ok(())
55+
// Watch mode
56+
Commands::Watch { pipeline, debounce } => {
57+
conflow::cli::watch::run(pipeline, debounce, cli.verbose).await
5958
}
59+
// Config validation
60+
Commands::Validate { pipeline } => {
61+
conflow::cli::validate::run(pipeline, cli.verbose).await
62+
}
63+
// Cache management
64+
Commands::Cache { action } => conflow::cli::cache::run(action, cli.verbose).await,
65+
// Pipeline graph visualisation
66+
Commands::Graph { pipeline, format } => {
67+
conflow::cli::graph::run(pipeline, format, cli.verbose).await
68+
}
69+
// RSR compliance
70+
Commands::Rsr { action } => conflow::cli::rsr::run(action, cli.verbose).await,
71+
// Groove discovery server
72+
Commands::Serve { port } => conflow::groove::run(port).await,
6073
}
6174
}

0 commit comments

Comments
 (0)