Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 156 additions & 5 deletions crates/codra-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use codra_core::provider::{create_provider, EchoMockProvider, IntelligenceProvider};
use codra_core::provider_config::ProviderConfigService;
use codra_protocol::{McpServerInfo, ProviderConfig, ProviderKind};
use codra_runtime::{StoredPairing, TrustLevel, WorkerHealth, WorkerId, WorkerStore};
use codra_runtime::{
PairingFingerprint, StoredPairing, TrustLevel, WorkerHealth, WorkerId, WorkerStore,
};
use codra_tools::design::load_design_system;
use codra_tools::registry::builtin_tool_definitions;
use std::env;
use std::io::{self, Write};
use std::io::{self, BufRead, Write};
use std::path::PathBuf;

fn main() {
Expand Down Expand Up @@ -119,13 +121,19 @@ fn worker_command(args: &[String]) -> Result<(), String> {
"add" => worker_add(&args[1..]),
"check" | "probe" => worker_check(&args[1..]),
"list" => worker_list(),
"remove" => worker_remove(&args[1..]),
"pair" => worker_pair(&args[1..]),
"remove" | "unpair" => worker_remove(&args[1..]),
"trust" => worker_trust(&args[1..]),
_ => {
println!("codra worker <command>");
println!(" add <url> --fingerprint <pin-or-fingerprint> Register a remote worker");
println!(" check|probe <worker_id> Probe worker health endpoint");
println!(" list List registered workers");
println!(" remove <worker_id> Remove a registered worker");
println!(" pair <url> Interactive pair and verify");
println!(
" remove|unpair <worker_id> Remove/unpair a registered worker"
);
println!(" trust <worker_id> <level> Update trust level");
Ok(())
}
}
Expand Down Expand Up @@ -250,6 +258,147 @@ fn worker_remove(args: &[String]) -> Result<(), String> {
Ok(())
}

fn worker_pair(args: &[String]) -> Result<(), String> {
let url = args
.first()
.ok_or_else(|| "Usage: codra worker pair <url> [--trust <level>]".to_string())?;

let trust_override = args
.iter()
.position(|a| a == "--trust")
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse::<TrustLevel>().ok());

let health_url = format!("{}/api/workers/health", url.trim_end_matches('/'));
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;

let resp = client
.get(&health_url)
.send()
.map_err(|e| format!("Failed to reach worker at {}: {}", health_url, e))?;

if !resp.status().is_success() {
return Err(format!(
"Worker at {} returned HTTP {}",
health_url,
resp.status()
));
}

let health: WorkerHealth = resp
.json()
.map_err(|e| format!("Malformed health response: {}", e))?;

// Derive provisional fingerprint from stable fields
let fp = PairingFingerprint::from_bytes(&health.provisional_fingerprint_bytes());
let pin = fp.pin();

// Display pairing preview
println!("Pairing preview");
println!(" worker id: {}", health.worker_id.0);
println!(" URL: {}", url.trim_end_matches('/'));
println!(" hostname: {}", health.hostname);
println!(" os / arch: {} / {}", health.os, health.arch);
println!(" daemon version: {}", health.version);
println!(
" protocol: {}",
health.remote_worker_protocol_version
);
println!(" capabilities:");
println!(
" task_execution: {}",
yesno(health.capabilities.task_execution)
);
println!(
" event_streaming: {}",
yesno(health.capabilities.event_streaming)
);
println!(
" remote_pairing: {}",
yesno(health.capabilities.remote_pairing)
);
println!(
" approval_fwd: {}",
yesno(health.capabilities.approval_forwarding)
);
println!(
" mdns_discovery: {}",
yesno(health.capabilities.mdns_discovery)
);
println!(" fingerprint: {}", fp);
println!(" PIN: {}", pin);

// Prompt for confirmation
print!("Pair this worker? Type the PIN to confirm: ");
io::stdout().flush().map_err(|e| e.to_string())?;

let mut input = String::new();
io::stdin()
.lock()
.read_line(&mut input)
.map_err(|e| e.to_string())?;
let input = input.trim().to_string();

if input != pin.as_str() {
return Err("PIN does not match — pairing rejected.".to_string());
}

println!("PIN verified ✓");

let url_trimmed = url.trim_end_matches('/');
let (host, port) = parse_worker_url(url_trimmed)?;
let trust_level = trust_override.unwrap_or(TrustLevel::Standard);

let worker = StoredPairing {
worker_id: WorkerId(health.worker_id.0.clone()),
worker_label: health.hostname.clone(),
pin_sha256: fp.as_hex().to_string(),
worker_host: host,
worker_port: port,
trust_level: trust_level.clone(),
paired_at: chrono::Utc::now().to_rfc3339(),
last_seen: chrono::Utc::now().to_rfc3339(),
};

let store = WorkerStore::new_global();
store
.add_worker(worker)
.map_err(|e| format!("Failed to register worker: {}", e))?;

println!("Worker paired successfully.");
println!(" ID: {}", health.worker_id.0);
println!(" URL: {}", url_trimmed);
println!(" Trust: {}", trust_level);
println!(" Store: {}", store.file_path().display());

Ok(())
}

fn worker_trust(args: &[String]) -> Result<(), String> {
let worker_id = args
.first()
.ok_or_else(|| "Usage: codra worker trust <worker_id> <level>".to_string())?;
let level_str = args
.get(1)
.ok_or_else(|| "Usage: codra worker trust <worker_id> <level>".to_string())?;

let level: TrustLevel = level_str.parse().map_err(|e: String| e)?;
let store = WorkerStore::new_global();
let updated = store
.update_trust_level(&WorkerId(worker_id.clone()), level.clone())
.map_err(|e| format!("Failed to update trust level: {}", e))?;

if updated {
println!("Worker '{}' trust level set to '{}'.", worker_id, level);
} else {
println!("Worker '{}' not found.", worker_id);
}
Ok(())
}

fn worker_check(args: &[String]) -> Result<(), String> {
let worker_id = args
.first()
Expand Down Expand Up @@ -374,7 +523,9 @@ fn help() -> Result<(), String> {
println!(" worker add Register a remote worker");
println!(" worker check Probe a registered worker's health endpoint");
println!(" worker list List registered workers");
println!(" worker remove Remove a registered worker");
println!(" worker pair Interactive pair and verify a remote worker");
println!(" worker trust Update a worker's trust level");
println!(" worker remove Remove/unpair a registered worker");
println!(" headless <intent> Run a dry-run headless planning surface");
println!(" mcp-server Print MCP-compatible server/tool metadata");
Ok(())
Expand Down
87 changes: 87 additions & 0 deletions crates/codra-runtime/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,32 @@ pub enum TrustLevel {
Full,
}

impl std::fmt::Display for TrustLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Untrusted => write!(f, "untrusted"),
Self::Limited => write!(f, "limited"),
Self::Standard => write!(f, "standard"),
Self::Elevated => write!(f, "elevated"),
Self::Full => write!(f, "full"),
}
}
}

impl std::str::FromStr for TrustLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"untrusted" => Ok(Self::Untrusted),
"limited" => Ok(Self::Limited),
"standard" => Ok(Self::Standard),
"elevated" => Ok(Self::Elevated),
"full" => Ok(Self::Full),
_ => Err(format!("Unknown trust level '{}'; expected one of: untrusted, limited, standard, elevated, full", s)),
}
}
}

/// The current state of a pairing request between controller and worker.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PairingStatus {
Expand Down Expand Up @@ -545,6 +571,20 @@ pub struct WorkerHealth {
pub capabilities: WorkerCapabilities,
}

impl WorkerHealth {
/// Provisional identity bytes for deriving a fingerprint when the
/// daemon does not yet expose a public key. Concatenates the
/// most stable, identifying fields so the fingerprint is
/// deterministic unless the worker identity or platform changes.
pub fn provisional_fingerprint_bytes(&self) -> Vec<u8> {
format!(
"{}|{}|{}|{}",
self.worker_id.0, self.remote_worker_protocol_version, self.os, self.arch,
)
.into_bytes()
}
}

impl Default for SafetyConfig {
fn default() -> Self {
Self {
Expand Down Expand Up @@ -706,4 +746,51 @@ mod tests {
.unwrap());
assert!(!json["capabilities"]["mdns_discovery"].as_bool().unwrap());
}

#[test]
fn trust_level_display_and_parse() {
for (level_str, variant) in [
("untrusted", TrustLevel::Untrusted),
("limited", TrustLevel::Limited),
("standard", TrustLevel::Standard),
("elevated", TrustLevel::Elevated),
("full", TrustLevel::Full),
] {
assert_eq!(variant.to_string(), level_str);
assert_eq!(level_str.parse::<TrustLevel>().unwrap(), variant);
}
assert!("unknown".parse::<TrustLevel>().is_err());
}

#[test]
fn provisional_fingerprint_bytes_is_deterministic() {
let health = WorkerHealth {
status: "ok".to_string(),
worker_id: WorkerId("wkr-001".to_string()),
version: "0.1.0".to_string(),
hostname: "build-server".to_string(),
os: "linux".to_string(),
arch: "aarch64".to_string(),
uptime_seconds: 86400,
supported_runtime_kinds: vec![],
available_runtimes: vec![],
workspace_mode: "local_only".to_string(),
remote_worker_protocol_version: "0.1".to_string(),
capabilities: WorkerCapabilities {
task_execution: true,
event_streaming: true,
approval_forwarding: false,
remote_pairing: false,
mdns_discovery: false,
},
};
let bytes1 = health.provisional_fingerprint_bytes();
let bytes2 = health.provisional_fingerprint_bytes();
assert_eq!(bytes1, bytes2);
let encoded = String::from_utf8_lossy(&bytes1);
assert!(encoded.contains("wkr-001"));
assert!(encoded.contains("linux"));
assert!(encoded.contains("aarch64"));
assert!(encoded.contains("0.1"));
}
}
39 changes: 38 additions & 1 deletion crates/codra-runtime/src/worker_store.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::types::{StoredPairing, WorkerId};
use crate::types::{StoredPairing, TrustLevel, WorkerId};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
Expand Down Expand Up @@ -101,6 +101,22 @@ impl WorkerStore {
}
}

/// Update the trust level for a registered worker.
pub fn update_trust_level(
&self,
worker_id: &WorkerId,
trust_level: TrustLevel,
) -> Result<bool, String> {
let mut wf = self.load();
if let Some(worker) = wf.workers.iter_mut().find(|w| w.worker_id == *worker_id) {
worker.trust_level = trust_level;
self.save(&wf)?;
Ok(true)
} else {
Ok(false)
}
}

/// Returns the file path used by this store.
pub fn file_path(&self) -> &PathBuf {
&self.file_path
Expand Down Expand Up @@ -241,4 +257,25 @@ mod tests {
assert_eq!(workers[1].worker_id.0, "wkr-002");
}
}

#[test]
fn update_trust_level_works() {
let (store, _dir) = test_store();
store.add_worker(test_worker("wkr-001")).unwrap();
let updated = store
.update_trust_level(&WorkerId("wkr-001".to_string()), TrustLevel::Elevated)
.unwrap();
assert!(updated);
let worker = store.get_worker(&WorkerId("wkr-001".to_string())).unwrap();
assert_eq!(worker.trust_level, TrustLevel::Elevated);
}

#[test]
fn update_trust_level_nonexistent_returns_false() {
let (store, _dir) = test_store();
let updated = store
.update_trust_level(&WorkerId("ghost".to_string()), TrustLevel::Elevated)
.unwrap();
assert!(!updated);
}
}
Loading