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. After every completed verification run, call
falsegreen_get_verification_artifacts() when a run_id is returned, whether
accepted or not, before producing any completion, PR, or verification
report. The final report must be exactly the returned report_markdown,
copied verbatim. Do not summarize, rewrite, prepend, append, or add
verification claims.
Never create a replacement task for a failed or unresolved task. Retrieve its
canonical artifact first when one exists. If a stale task is not accepted and
work must continue, call falsegreen_close_task(reason); this explicitly
abandons the stale task and does not imply acceptance. Only an abandoned task
is closed and eligible for a new task. A task currently being verified cannot
be closed; wait for that run to finish. Use falsegreen_list_tasks() to discover
stale tasks.
Verifier commands support Python, Rust/Cargo, and Ruby/Rails directly. For
Ruby, use ruby, bundle, bundler, rake, rails, rspec, rubocop, or repository
binstubs under bin/. Do not replace valid Ruby commands because the local shell
differs. git is intentionally unavailable to verifier commands. The client
captures minimal Git provenance for the controller, but immutable verifier
snapshots do not contain .git; Git output is not an acceptance oracle.
Workspace-local Bundler PATH sources are supported when they remain inside the
immutable source snapshot; do not rewrite Rails monorepo Gemfiles to remove
them.
Call falsegreen_get_contract_schema() for the complete live command policy.
## 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. After every completed verification run, call
falsegreen_get_verification_artifacts() when a run_id is returned, whether
accepted or not, before producing any completion, PR, or verification
report. The final report must be exactly the returned report_markdown,
copied verbatim. Do not summarize, rewrite, prepend, append, or add
verification claims.
If the task is stale and not accepted, retrieve its canonical artifact when
one exists, then call falsegreen_close_task(reason) before starting another
task. Use falsegreen_list_tasks() to discover stale tasks.
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. The final report must be exactly the returned report_markdown,
copied verbatim."#;
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 && 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"
)
}
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 every completed verification run, retrieve the durable source-bound attestation and canonical report. Output its report_markdown field verbatim as the final report; do not summarize, rewrite, prepend, append, or add verification claims.",
"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 live contract schema and verifier command policy. Ruby/Rails commands and binstubs are supported; Git is intentionally not an acceptance oracle.",
"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_close_task",
"description": "Close a stale non-accepted task; this does not imply acceptance. Tasks currently being verified cannot be closed.",
"inputSchema": {
"type": "object",
"properties": {
"reason": {"type": "string"},
"task_id": {"type": ["string", "null"], "default": null},
},
"required": ["reason"],
},
}),
json!({
"name": "falsegreen_list_tasks",
"description": "List compact task lifecycle state and which stale tasks can be closed.",
"inputSchema": {"type": "object", "properties": {}},
}),
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::{INSTRUCTIONS, tool_definitions, tool_reads_workspace};
#[test]
fn accepted_workflow_requires_verification_artifacts() {
assert!(INSTRUCTIONS.contains("falsegreen_get_verification_artifacts()"));
assert!(INSTRUCTIONS.contains("before producing any completion, PR, or verification"));
assert!(INSTRUCTIONS.contains("falsegreen_close_task(reason)"));
assert!(INSTRUCTIONS.contains("falsegreen_list_tasks()"));
assert!(INSTRUCTIONS.contains("Ruby/Rails"));
assert!(INSTRUCTIONS.contains("Git output is not an acceptance oracle"));
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("report_markdown field verbatim"));
assert!(
description
.contains("do not summarize, rewrite, prepend, append, or add verification claims")
);
}
#[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",
] {
assert!(tool_reads_workspace(tool), "{tool} must sync source first");
}
assert!(!tool_reads_workspace("falsegreen_get_status"));
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"
));
assert!(!tool_reads_workspace("falsegreen_close_task"));
assert!(!tool_reads_workspace("falsegreen_list_tasks"));
}
}