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 because immutable verifier snapshots
do not contain trusted Git metadata; 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 {
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"
)
}
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"));
}
}