use std::io::{BufRead, Write};
use std::path::PathBuf;
use serde_json::{Value, json};
use crate::shim::config;
use crate::shim::workspace;
const PROTOCOL_VERSION: &str = "2025-06-18";
const SERVER_NAME: &str = "falsegreen";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
const INSTRUCTIONS: &str = r#"This server owns task completion status.
Two workflows are supported:
## A. Dynamic workflow (no pre-existing task)
When no task is active, the agent can self-provision the full
completion-authority pipeline:
1. falsegreen_create_task(workspace, goal) — register a new task
2. falsegreen_save_contract_draft(task_id, contract) — draft acceptance criteria
3. falsegreen_validate_contract(task_id) — validate the draft
4. falsegreen_freeze_contract(task_id) — freeze the contract
5. falsegreen_get_assignment() — compact immutable requirements
6. Implement the software
7. falsegreen_check_completion() — independent completion decision
8. If the decision is accepted, call falsegreen_get_verification_artifacts()
before producing any completion, PR, or verification report. Use the
returned FalseGreen report and attestation rather than reconstructing them.
## B. Pre-frozen task workflow (host sets active task)
When a session already has one active frozen task:
1. falsegreen_get_assignment() — compact immutable requirements
2. Implement the software
3. falsegreen_check_completion() — independent completion decision
4. If the decision is accepted, call falsegreen_get_verification_artifacts()
before producing any completion, PR, or verification report. Use the
returned FalseGreen report and attestation rather than reconstructing them.
Do not invent or retype task IDs when the session already has an active task.
Optional task_id arguments exist only for multi-task/debug hosts.
Claim completion only when falsegreen_check_completion (or falsegreen_get_status)
returns status=accepted, and retrieve the verification artifacts before writing
the final report."#;
pub fn run() -> ! {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let mut active_workspace: Option<PathBuf> = None;
let mut line = String::new();
loop {
line.clear();
match stdin.lock().read_line(&mut line) {
Ok(0) => break, Ok(_) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let request: Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(_) => continue, };
let has_id = request.get("id").is_some();
let method = request.get("method").and_then(|m| m.as_str()).unwrap_or("");
let response = handle_request(&request, method, &mut active_workspace);
if has_id {
if let Some(resp) = response {
let serialized = serde_json::to_string(&resp).unwrap_or_default();
let _ = writeln!(stdout, "{}", serialized);
let _ = stdout.flush();
}
}
}
Err(_) => break,
}
}
std::process::exit(0);
}
fn handle_request(
request: &Value,
method: &str,
active_workspace: &mut Option<PathBuf>,
) -> Option<Value> {
let id = request.get("id").cloned();
let params = request.get("params").cloned().unwrap_or(Value::Null);
match method {
"initialize" => Some(json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {
"experimental": {},
"prompts": {"listChanged": false},
"resources": {"subscribe": false, "listChanged": false},
"tools": {"listChanged": false},
},
"serverInfo": {
"name": SERVER_NAME,
"version": SERVER_VERSION,
},
"instructions": INSTRUCTIONS,
}
})),
"notifications/initialized" => None,
"tools/list" => Some(json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"tools": tool_definitions(),
}
})),
"tools/call" => {
let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
let result = forward_tool_call(tool_name, &arguments, active_workspace);
Some(json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
}))
}
"prompts/list" => Some(json!({
"jsonrpc": "2.0",
"id": id,
"result": {"prompts": []}
})),
"resources/list" => Some(json!({
"jsonrpc": "2.0",
"id": id,
"result": {"resources": []}
})),
_ => Some(json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": format!("method not found: {}", method),
}
})),
}
}
fn forward_tool_call(
tool_name: &str,
arguments: &Value,
active_workspace: &mut Option<PathBuf>,
) -> Value {
let url = config::api_url();
let key = match config::load_token() {
Some(t) => t,
None => {
return error_result("not authenticated — run `falsegreen login` first");
}
};
if tool_reads_workspace(tool_name) {
let local_workspace = match local_workspace(tool_name, arguments, active_workspace) {
Ok(path) => path,
Err(message) => return error_result(&message),
};
if let Err(message) = workspace::sync(&local_workspace, &key) {
return error_result(&message);
}
*active_workspace = Some(local_workspace);
}
let rpc_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments,
}
});
let body = serde_json::to_string(&rpc_request).unwrap_or_else(|_| "{}".to_string());
let result = std::process::Command::new("curl")
.arg("-sS")
.arg("-f")
.arg("-X")
.arg("POST")
.arg(&url)
.arg("-H")
.arg("Content-Type: application/json")
.arg("-H")
.arg(format!("X-API-KEY: {}", key))
.arg("-d")
.arg(&body)
.arg("--max-time")
.arg("1800")
.output();
match result {
Ok(output) if output.status.success() => {
let response_body = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str::<Value>(&response_body) {
Ok(v) => {
if let Some(result) = v.get("result") {
result.clone()
} else if let Some(error) = v.get("error") {
let msg = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
error_result(msg)
} else {
v
}
}
Err(_) => error_result(&format!("invalid response from server: {}", response_body)),
}
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
error_result(&format!("request failed: {}", stderr))
}
Err(e) => error_result(&format!("failed to send request: {}", e)),
}
}
fn tool_reads_workspace(tool_name: &str) -> bool {
matches!(
tool_name,
"falsegreen_create_task"
| "falsegreen_validate_contract"
| "falsegreen_freeze_contract"
| "falsegreen_check_completion"
| "falsegreen_verify"
| "falsegreen_get_status"
)
}
fn local_workspace(
tool_name: &str,
arguments: &Value,
active_workspace: &Option<PathBuf>,
) -> Result<PathBuf, String> {
if tool_name == "falsegreen_create_task" {
let supplied = arguments
.get("workspace")
.and_then(Value::as_str)
.ok_or_else(|| "falsegreen_create_task requires a workspace path".to_string())?;
return PathBuf::from(supplied)
.canonicalize()
.map_err(|error| format!("cannot resolve workspace {supplied}: {error}"));
}
if let Some(workspace) = active_workspace {
return Ok(workspace.clone());
}
std::env::current_dir().map_err(|error| format!("cannot resolve current workspace: {error}"))
}
fn error_result(message: &str) -> Value {
json!({
"content": [{"type": "text", "text": message}],
"isError": true,
})
}
fn tool_definitions() -> Vec<Value> {
vec![
json!({
"name": "falsegreen_create_task",
"description": "Register a new task.",
"inputSchema": {
"type": "object",
"properties": {
"workspace": {"type": "string"},
"goal": {"type": "string"},
"title": {"type": ["string", "null"], "default": null},
},
"required": ["workspace", "goal"],
},
}),
json!({
"name": "falsegreen_save_contract_draft",
"description": "Draft acceptance criteria for a task.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": "string"},
"contract": {"type": "object"},
},
"required": ["task_id", "contract"],
},
}),
json!({
"name": "falsegreen_validate_contract",
"description": "Validate the current contract draft.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_freeze_contract",
"description": "Freeze the contract (locks criteria + verifier).",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_get_assignment",
"description": "Get compact immutable requirements for the active task.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_check_completion",
"description": "Run the independent verifier and get a completion decision.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_get_status",
"description": "Get current task status without running verification.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_get_verification_artifacts",
"description": "After acceptance, retrieve the durable source-bound attestation and PR-ready report before producing any completion, PR, or verification report.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
"run_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_get_attestation_verification_key",
"description": "Get public Ed25519 material for independent artifact verification.",
"inputSchema": {"type": "object", "properties": {}},
}),
json!({
"name": "falsegreen_get_repair_feedback",
"description": "Get detailed failure feedback from the last verification run.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_get_contract_schema",
"description": "Get the JSON schema for contract documents.",
"inputSchema": {"type": "object", "properties": {}},
}),
json!({
"name": "falsegreen_get_contract",
"description": "Get the frozen contract for a task.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
"frozen": {"type": "boolean", "default": true},
},
},
}),
json!({
"name": "falsegreen_begin_implementation",
"description": "Transition task to implementing state.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_verify",
"description": "Run verification explicitly.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_consume_repair_cycle",
"description": "Consume a repair cycle.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
json!({
"name": "falsegreen_mark_unresolved",
"description": "Mark the task as unresolved with a reason.",
"inputSchema": {
"type": "object",
"properties": {
"reason": {"type": "string"},
"task_id": {"type": ["string", "null"], "default": null},
},
"required": ["reason"],
},
}),
json!({
"name": "falsegreen_get_audit_summary",
"description": "Get audit summary for a task.",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": ["string", "null"], "default": null},
},
},
}),
]
}
#[cfg(test)]
mod tests {
use super::{tool_definitions, tool_reads_workspace, INSTRUCTIONS};
#[test]
fn accepted_workflow_requires_verification_artifacts() {
assert!(INSTRUCTIONS.contains("falsegreen_get_verification_artifacts()"));
assert!(INSTRUCTIONS.contains(
"before producing any completion, PR, or verification report"
));
let artifact_tool = tool_definitions()
.into_iter()
.find(|tool| tool["name"] == "falsegreen_get_verification_artifacts")
.expect("artifact tool must be advertised");
let description = artifact_tool["description"]
.as_str()
.expect("artifact tool must have a description");
assert!(description.contains(
"before producing any completion, PR, or verification report"
));
}
#[test]
fn source_sensitive_tools_trigger_workspace_sync() {
for tool in [
"falsegreen_create_task",
"falsegreen_validate_contract",
"falsegreen_freeze_contract",
"falsegreen_check_completion",
"falsegreen_verify",
"falsegreen_get_status",
] {
assert!(tool_reads_workspace(tool), "{tool} must sync source first");
}
assert!(!tool_reads_workspace("falsegreen_get_contract_schema"));
assert!(!tool_reads_workspace("falsegreen_get_repair_feedback"));
assert!(!tool_reads_workspace("falsegreen_get_verification_artifacts"));
assert!(!tool_reads_workspace(
"falsegreen_get_attestation_verification_key"
));
}
}