mod acp;
mod tui;
use std::{
fs,
io::{self, Write},
path::{Path, PathBuf},
process::{Command as ProcessCommand, ExitCode},
sync::{Mutex, mpsc::sync_channel},
};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, Parser, Subcommand, ValueEnum};
use proofborne_adapters::{build_provider, register_mcp_servers};
use proofborne_core::{
Criterion, EventEnvelope, EvidenceFreshness, EvidenceKind, RunOutcome, TaskContract,
};
use proofborne_runtime::{
AppConfig, AuthStore, CasStore, EventSink, PolicyConfig, PolicyPreset, ProviderAttempt,
ProviderChain, Redactor, RunControl, RunOptions, RuntimePaths, SessionEngine, SessionStore,
ToolRegistry, VerificationCommand, diff_proof_bundles, export_intoto_statement,
export_proof_bundle, reproduce_proof_bundle, verify_proof_bundle,
};
use serde::Serialize;
use serde_json::{Value, json};
use tracing_subscriber::EnvFilter;
use uuid::Uuid;
#[derive(Debug, Parser)]
#[command(
name = "proofborne",
version,
about = "The coding agent that proves its work",
long_about = None
)]
struct Cli {
#[arg(long, global = true, default_value = ".")]
workspace: PathBuf,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
Init,
Run(RunArgs),
Acp(AcpArgs),
Auth {
#[command(subcommand)]
command: AuthCommand,
},
Models {
#[arg(long)]
json: bool,
},
Session {
#[command(subcommand)]
command: SessionCommand,
},
Proof {
#[command(subcommand)]
command: ProofCommand,
},
Doctor {
#[arg(long)]
json: bool,
},
}
#[derive(Debug, Args, Clone)]
struct RunArgs {
#[arg(long)]
task: Option<String>,
#[arg(
long,
conflicts_with = "auto_contract",
required_unless_present = "auto_contract"
)]
contract: Option<PathBuf>,
#[arg(long, conflicts_with = "contract")]
auto_contract: bool,
#[arg(long)]
jsonl: bool,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_enum)]
policy: Option<PolicyArg>,
#[arg(long, requires = "policy")]
trust_workspace: bool,
#[arg(long)]
verification: Option<PathBuf>,
#[arg(long, default_value_t = 64)]
max_steps: usize,
}
#[derive(Debug, Args, Clone)]
struct AcpArgs {
#[arg(long)]
profile: Option<String>,
#[arg(long, value_enum)]
policy: Option<PolicyArg>,
#[arg(long)]
allow_client_mcp: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum PolicyArg {
Observe,
Review,
Workspace,
Ci,
}
impl From<PolicyArg> for PolicyPreset {
fn from(value: PolicyArg) -> Self {
match value {
PolicyArg::Observe => Self::Observe,
PolicyArg::Review => Self::Review,
PolicyArg::Workspace => Self::Workspace,
PolicyArg::Ci => Self::Ci,
}
}
}
#[derive(Debug, Subcommand)]
enum AuthCommand {
Add {
provider: String,
id: String,
},
List {
#[arg(long)]
provider: Option<String>,
#[arg(long)]
json: bool,
},
Remove { provider: String, id: String },
}
#[derive(Debug, Subcommand)]
enum SessionCommand {
List {
#[arg(long, default_value_t = 25)]
limit: usize,
#[arg(long)]
json: bool,
},
Show {
id: Uuid,
#[arg(long)]
jsonl: bool,
},
Resume { id: Uuid },
}
#[derive(Debug, Subcommand)]
enum ProofCommand {
Export {
session: Uuid,
#[arg(long)]
output: PathBuf,
#[arg(long)]
in_toto_output: Option<PathBuf>,
#[arg(long)]
include_artifacts: bool,
},
Verify {
bundle: PathBuf,
#[arg(long)]
json: bool,
},
Diff {
left: PathBuf,
right: PathBuf,
#[arg(long)]
json: bool,
},
Reproduce {
bundle: PathBuf,
#[arg(long)]
execute: bool,
#[arg(long)]
json: bool,
},
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")),
)
.with_writer(io::stderr)
.without_time()
.compact()
.init();
let cli = Cli::parse();
match execute(cli).await {
Ok(code) => code,
Err(error) => {
eprintln!("proofborne: {error:#}");
ExitCode::from(4)
}
}
}
async fn execute(cli: Cli) -> Result<ExitCode> {
let workspace = canonical_workspace(&cli.workspace)?;
match cli.command {
Some(Command::Init) => {
let path = AppConfig::init_project(&workspace)?;
println!("initialized {}", path.display());
Ok(ExitCode::SUCCESS)
}
Some(Command::Run(args)) => run_headless(&workspace, args).await,
Some(Command::Acp(args)) => {
acp::serve(
workspace,
args.profile,
args.policy.map(Into::into),
args.allow_client_mcp,
)
.await
}
Some(Command::Auth { command }) => auth_command(&workspace, command),
Some(Command::Models { json }) => models_command(&workspace, json),
Some(Command::Session { command }) => session_command(&workspace, command).await,
Some(Command::Proof { command }) => proof_command(&workspace, command).await,
Some(Command::Doctor { json }) => doctor_command(&workspace, json),
None => run_tui(&workspace).await,
}
}
async fn run_headless(workspace: &Path, args: RunArgs) -> Result<ExitCode> {
let verification_path = args
.verification
.as_deref()
.map(|path| resolve_workspace_argument(workspace, path));
let verification = verification_path
.as_deref()
.map(load_verifications)
.transpose()?
.unwrap_or_default();
let contract_path = args
.contract
.as_deref()
.map(|path| resolve_workspace_argument(workspace, path));
let contract = load_contract(
args.task.as_deref(),
contract_path.as_deref(),
args.auto_contract,
&verification,
)?;
let prepared = prepare_engine(
workspace,
args.profile.as_deref(),
args.policy.map(Into::into),
)
.await?;
let mut options = RunOptions::new(workspace.to_path_buf(), &prepared.model, contract);
options.policy = prepared.policy;
if args.trust_workspace {
if options.policy.preset != PolicyPreset::Workspace {
bail!("--trust-workspace requires --policy workspace");
}
options.policy.trusted_workspace = true;
}
options.max_steps = args.max_steps;
options.verification = verification;
options.redactor = prepared.redactor;
if args.jsonl {
let sink = JsonlSink::new();
let result = prepared
.engine
.run_chain(prepared.provider_chain, options, &sink)
.await?;
Ok(outcome_exit_code(result.outcome))
} else {
let sink = HumanSink;
let result = prepared
.engine
.run_chain(prepared.provider_chain, options, &sink)
.await?;
if !result.output_text.is_empty() {
println!("\n{}", result.output_text);
}
println!(
"\noutcome: {:?}\nsession: {}",
result.outcome, result.session_id
);
Ok(outcome_exit_code(result.outcome))
}
}
async fn run_tui(workspace: &Path) -> Result<ExitCode> {
let Some(contract) = tui::capture_task_contract()? else {
return Ok(ExitCode::from(130));
};
let prepared = prepare_engine(workspace, None, None).await?;
let mut options = RunOptions::new(workspace.to_path_buf(), &prepared.model, contract.clone());
options.policy = prepared.policy;
options.interactive = true;
let (approvals, elicitation, interactions) = tui::live_interaction_handlers();
options.approvals = approvals;
options.elicitation = elicitation;
options.redactor = prepared.redactor;
let control = RunControl::default();
options.control = control.clone();
let (sink, events) = tui::live_event_channel();
let (completion_sender, completion) = sync_channel(1);
let engine = prepared.engine;
let provider_chain = prepared.provider_chain;
tokio::spawn(async move {
let result = engine.run_chain(provider_chain, options, &sink).await;
let _ = completion_sender.send(result.map_err(anyhow::Error::from));
});
let (result, _captured) =
tui::show_live_result(&contract, events, interactions, completion, control)?;
Ok(outcome_exit_code(result.outcome))
}
fn load_contract(
task: Option<&str>,
contract_path: Option<&Path>,
auto_contract: bool,
verifications: &[VerificationCommand],
) -> Result<TaskContract> {
let mut contract = if auto_contract {
let task = task.ok_or_else(|| anyhow!("--task is required with --auto-contract"))?;
compile_auto_contract(task, verifications)?
} else {
let path = contract_path.ok_or_else(|| anyhow!("--contract is required"))?;
parse_path(path).with_context(|| format!("failed to load contract {}", path.display()))?
};
if let Some(task) = task
&& contract.goal != task
{
bail!("--task differs from the goal pinned in --contract");
}
if !contract.confirmed {
bail!("headless contracts must contain confirmed = true");
}
contract.validate()?;
for criterion in &mut contract.criteria {
if criterion.waiver.is_none() {
criterion.state = proofborne_core::CriterionState::Pending;
criterion.evidence_ids.clear();
}
}
Ok(contract)
}
fn compile_auto_contract(
task: &str,
verifications: &[VerificationCommand],
) -> Result<TaskContract> {
if verifications.is_empty() {
return Ok(TaskContract::automatic(task));
}
let mut seen = std::collections::BTreeSet::new();
let mut criteria = Vec::with_capacity(verifications.len());
for verification in verifications {
if verification.criterion_id.trim().is_empty() || verification.program.trim().is_empty() {
bail!("verification criterionId and program must not be empty");
}
if !seen.insert(verification.criterion_id.clone()) {
bail!(
"auto-contract verification criterion {} is duplicated",
verification.criterion_id
);
}
let mut criterion = Criterion::required(
verification.criterion_id.clone(),
format!(
"Runtime verification command `{}` succeeds against the final workspace",
verification.program
),
);
criterion.evidence_requirement.allowed_kinds =
[EvidenceKind::Process].into_iter().collect();
criterion.evidence_requirement.allowed_producers =
["verify.exec".to_owned()].into_iter().collect();
criterion.evidence_requirement.freshness = EvidenceFreshness::FinalWorkspaceState;
criteria.push(criterion);
}
let mut contract = TaskContract::new(task, criteria);
contract.confirmed = true;
contract.constraints.push(
"Only runtime-executed verification commands may satisfy generated criteria".to_owned(),
);
contract.validate()?;
Ok(contract)
}
fn load_verifications(path: &Path) -> Result<Vec<VerificationCommand>> {
parse_path(path).with_context(|| format!("failed to load verifications {}", path.display()))
}
fn parse_path<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
let text = fs::read_to_string(path)?;
if path.extension().and_then(|extension| extension.to_str()) == Some("toml") {
Ok(toml_from_str(&text)?)
} else {
Ok(serde_json::from_str(&text)?)
}
}
fn toml_from_str<T: serde::de::DeserializeOwned>(text: &str) -> Result<T> {
let value: toml::Value = toml::from_str(text)?;
let json = serde_json::to_value(value)?;
Ok(serde_json::from_value(json)?)
}
struct PreparedEngine {
engine: SessionEngine,
provider_chain: ProviderChain,
model: String,
policy: PolicyConfig,
redactor: Redactor,
}
async fn prepare_engine(
workspace: &Path,
profile_override: Option<&str>,
policy_override: Option<PolicyPreset>,
) -> Result<PreparedEngine> {
prepare_engine_with_tools(workspace, profile_override, policy_override, None, &[]).await
}
async fn prepare_engine_with_tools(
workspace: &Path,
profile_override: Option<&str>,
policy_override: Option<PolicyPreset>,
attached_tools: Option<&ToolRegistry>,
attached_secrets: &[String],
) -> Result<PreparedEngine> {
let paths = RuntimePaths::for_workspace(workspace)?;
paths.ensure_data_directories()?;
let mut config = AppConfig::load(workspace)?;
if let Some(policy) = policy_override {
config.policy.preset = policy;
}
let (profile_name, _) = config.selected_profile(profile_override)?;
let profile_name = profile_name.to_owned();
let auth = AuthStore::new(&paths.user_data);
let profile_order = config.provider_fallback_order(&profile_name)?;
let mut attempts = Vec::new();
let mut secrets = Vec::new();
for name in profile_order {
let profile = config
.providers
.get(&name)
.expect("validated provider fallback order resolves every profile");
let credentials = auth.resolve_all(profile)?;
if credentials.is_empty() {
attempts.push(ProviderAttempt::new(
name,
profile.model.clone(),
None,
build_provider(profile, None)?,
));
continue;
}
for credential in credentials {
secrets.push(credential.secret.clone());
attempts.push(ProviderAttempt::new(
name.clone(),
profile.model.clone(),
Some(credential.id),
build_provider(profile, Some(credential.secret))?,
));
}
}
let mut attempts = attempts.into_iter();
let primary = attempts
.next()
.ok_or_else(|| anyhow!("selected provider profile produced no attempts"))?;
let provider_chain = ProviderChain::new(primary, attempts);
secrets.extend(attached_secrets.iter().cloned());
let redactor = Redactor::with_secrets(secrets);
let store = SessionStore::open(paths.workspace_data.join("sessions.sqlite3"))?;
let cas = CasStore::new(paths.workspace_data.join("cas"))?;
let mut tools = ToolRegistry::with_builtins();
register_mcp_servers(&config.mcp, &config.policy.mcp_allow, &mut tools).await?;
if let Some(attached_tools) = attached_tools {
tools.extend_unique(attached_tools)?;
}
let engine = SessionEngine::new(store, cas, tools);
Ok(PreparedEngine {
engine,
provider_chain,
model: config
.providers
.get(&profile_name)
.expect("selected profile exists")
.model
.clone(),
policy: config.policy,
redactor,
})
}
fn auth_command(workspace: &Path, command: AuthCommand) -> Result<ExitCode> {
let paths = RuntimePaths::for_workspace(workspace)?;
paths.ensure_data_directories()?;
let store = AuthStore::new(&paths.user_data);
match command {
AuthCommand::Add { provider, id } => {
let secret = rpassword::prompt_password(format!("{provider}/{id} secret: "))?;
store.add(&provider, &id, &secret)?;
println!("stored {provider}/{id} in the OS keychain");
}
AuthCommand::List { provider, json } => {
let entries = store.list(provider.as_deref())?;
if json {
print_json(&entries)?;
} else if entries.is_empty() {
println!("no keychain credentials indexed");
} else {
for entry in entries {
println!("{}\t{}\t{}", entry.provider, entry.id, entry.created_at);
}
}
}
AuthCommand::Remove { provider, id } => {
store.remove(&provider, &id)?;
println!("removed {provider}/{id} from the OS keychain");
}
}
Ok(ExitCode::SUCCESS)
}
fn models_command(workspace: &Path, json_output: bool) -> Result<ExitCode> {
let config = AppConfig::load(workspace)?;
let models: Vec<_> = config
.providers
.iter()
.map(|(name, profile)| {
json!({
"name": name,
"provider": profile.provider,
"model": profile.model,
"baseUrl": profile.base_url,
"fallbacks": profile.fallbacks,
"selected": config.default_profile.as_deref() == Some(name),
"capabilities": declared_capabilities(&profile.provider),
})
})
.collect();
if json_output {
print_json(&models)?;
} else {
for model in models {
println!(
"{}\t{}\t{}{}",
model["name"].as_str().unwrap_or_default(),
model["provider"].as_str().unwrap_or_default(),
model["model"].as_str().unwrap_or_default(),
if model["selected"].as_bool() == Some(true) {
"\t(default)"
} else {
""
}
);
}
}
Ok(ExitCode::SUCCESS)
}
fn declared_capabilities(provider: &str) -> Value {
json!({
"streaming": matches!(provider, "openai" | "anthropic" | "gemini" | "openai_compatible"),
"tools": matches!(provider, "mock" | "openai" | "anthropic" | "gemini" | "openai_compatible"),
"structuredOutput": matches!(provider, "mock" | "openai" | "gemini"),
"multimodalInput": false,
})
}
async fn session_command(workspace: &Path, command: SessionCommand) -> Result<ExitCode> {
let (_, store, _) = open_stores(workspace)?;
match command {
SessionCommand::List { limit, json } => {
let sessions = store.list_sessions(limit)?;
if json {
print_json(&sessions)?;
} else {
for session in sessions {
println!(
"{}\t{}\t{}/{}\t{}",
session.id,
session.status,
session.provider,
session.model,
session.updated_at
);
}
}
}
SessionCommand::Show { id, jsonl } => {
let session = store.load_session(id)?;
if jsonl {
for event in session.events {
println!("{}", serde_json::to_string(&event)?);
}
} else {
print_json(&session)?;
}
}
SessionCommand::Resume { id } => {
let checkpoint = store.load_resumable_checkpoint(id)?;
let record = store.load_session(id)?;
let config = AppConfig::load(workspace)?;
let profile = config
.providers
.iter()
.find_map(|(name, profile)| {
(profile.provider == record.provider && profile.model == record.model)
.then_some(name.as_str())
})
.ok_or_else(|| anyhow!(
"resume requires the recorded provider/model {}/{}; no matching configured profile exists",
record.provider, record.model
))?;
let prepared = prepare_engine(workspace, Some(profile), None).await?;
let mut options = RunOptions::new(
workspace.to_path_buf(),
&prepared.model,
checkpoint.contract,
);
options.policy = prepared.policy;
options.redactor = prepared.redactor;
options.resume_session_id = Some(id);
let sink = HumanSink;
let result = prepared
.engine
.run_chain(prepared.provider_chain, options, &sink)
.await?;
println!(
"session {id} resumed from checkpoint at step {} ({} pending calls); outcome: {:?}",
checkpoint.next_step,
checkpoint.pending_tool_calls.len(),
result.outcome,
);
return Ok(outcome_exit_code(result.outcome));
}
}
Ok(ExitCode::SUCCESS)
}
async fn proof_command(workspace: &Path, command: ProofCommand) -> Result<ExitCode> {
match command {
ProofCommand::Export {
session,
output,
in_toto_output,
include_artifacts,
} => {
let (_, store, cas) = open_stores(workspace)?;
let session = store.load_session(session)?;
let manifest = export_proof_bundle(&session, &cas, &output, include_artifacts)?;
if let Some(attestation_output) = in_toto_output {
export_intoto_statement(&output, &attestation_output)?;
println!(
"exported unsigned in-toto Statement v1 {}",
attestation_output.display()
);
}
println!(
"exported unsigned {} ({:?}/{:?}/{:?}, root {})",
output.display(),
manifest.outcome,
manifest.claim_scope,
manifest.assurance_level,
manifest.event_root_hash
);
Ok(ExitCode::SUCCESS)
}
ProofCommand::Verify { bundle, json } => match verify_proof_bundle(&bundle) {
Ok(verified) => {
if json {
print_json(&verified)?;
} else {
println!(
"verified {:?}/{:?}/{:?} (unsigned): {} events, {} evidence nodes, root {}",
verified.outcome,
verified.claim_scope,
verified.assurance_level,
verified.event_count,
verified.evidence_count,
verified.manifest.event_root_hash
);
}
Ok(ExitCode::SUCCESS)
}
Err(error) => {
eprintln!("proof verification failed: {error}");
Ok(ExitCode::from(2))
}
},
ProofCommand::Diff { left, right, json } => match diff_proof_bundles(&left, &right) {
Ok(diff) => {
if json {
print_json(&diff)?;
} else if diff.equal {
println!("proofs are structurally identical");
} else {
println!(
"proofs differ at {} deterministic paths ({} -> {})",
diff.differences.len(),
diff.left_event_root_hash,
diff.right_event_root_hash
);
for difference in &diff.differences {
println!("{:?}\t{}", difference.kind, difference.path);
}
}
Ok(ExitCode::SUCCESS)
}
Err(error) => {
eprintln!("proof diff failed verification: {error}");
Ok(ExitCode::from(2))
}
},
ProofCommand::Reproduce {
bundle,
execute,
json,
} => {
let config = AppConfig::load(workspace)?;
match reproduce_proof_bundle(&bundle, workspace, config.policy, execute).await {
Ok(report) => {
if json {
print_json(&report)?;
} else if report.executed {
println!(
"reproduced {} literal-argv commands on host ({:?}); recorded results matched: {}",
report.command_results.len(),
report.assurance_level,
report.matches_recorded_results.unwrap_or(false)
);
println!("{}", report.notice);
} else {
println!(
"reproduction capsule contains {} commands; no process was executed",
report.capsule.commands.len()
);
println!("{}", report.notice);
}
if report.matches_recorded_results == Some(false) {
Ok(ExitCode::from(2))
} else {
Ok(ExitCode::SUCCESS)
}
}
Err(error) if error.is_blocked() => {
eprintln!("proof reproduction blocked: {error}");
Ok(ExitCode::from(3))
}
Err(error) => Err(error.into()),
}
}
}
}
fn open_stores(workspace: &Path) -> Result<(RuntimePaths, SessionStore, CasStore)> {
let paths = RuntimePaths::for_workspace(workspace)?;
paths.ensure_data_directories()?;
let store = SessionStore::open(paths.workspace_data.join("sessions.sqlite3"))?;
let cas = CasStore::new(paths.workspace_data.join("cas"))?;
Ok((paths, store, cas))
}
fn doctor_command(workspace: &Path, json_output: bool) -> Result<ExitCode> {
let paths = RuntimePaths::for_workspace(workspace)?;
paths.ensure_data_directories()?;
let config = AppConfig::load(workspace);
let git = command_version("git", &["--version"]);
let docker = command_version("docker", &["--version"]);
let secret_findings = [paths.project_config.as_path(), paths.user_config.as_path()]
.into_iter()
.map(scan_config_for_secrets)
.collect::<Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
let configured_provider = config.as_ref().ok().and_then(|config| {
config.selected_profile(None).ok().map(|(name, profile)| {
let credential = AuthStore::new(&paths.user_data).resolve(profile, 0);
json!({
"profile": name,
"adapter": profile.provider,
"model": profile.model,
"credentialConfigured": credential.as_ref().is_ok_and(Option::is_some)
|| profile.provider == "mock",
"credentialResolutionError": credential.err().map(|error| error.to_string()),
"fallbackCount": profile.fallbacks.len(),
})
})
});
let isolation_ready = config.as_ref().is_ok_and(|config| {
!matches!(
config.policy.isolation,
proofborne_runtime::IsolationMode::Docker | proofborne_runtime::IsolationMode::Required
) || docker["available"] == true
});
let report = json!({
"ok": config.is_ok()
&& git["available"] == true
&& secret_findings.is_empty()
&& isolation_ready,
"workspace": workspace,
"projectConfig": paths.project_config,
"userConfig": paths.user_config,
"workspaceData": paths.workspace_data,
"configuration": match config {
Ok(config) => json!({
"valid": true,
"defaultProfile": config.default_profile,
"policy": config.policy.preset.to_string(),
"isolation": config.policy.isolation,
"remoteTelemetryOptIn": config.telemetry.remote_opt_in,
"mcpServerCount": config.mcp.len(),
"mcpAllowCount": config.policy.mcp_allow.len(),
}),
Err(error) => json!({"valid": false, "error": error.to_string()}),
},
"git": git,
"docker": docker,
"provider": configured_provider,
"isolationReady": isolation_ready,
"hostExecution": "policy-enforced, not sandboxed",
"secretsInToml": !secret_findings.is_empty(),
"secretFindings": secret_findings,
});
if json_output {
print_json(&report)?;
} else {
println!(
"Proofborne doctor\n{}",
serde_json::to_string_pretty(&report)?
);
}
Ok(if report["ok"] == true {
ExitCode::SUCCESS
} else {
ExitCode::from(4)
})
}
fn scan_config_for_secrets(path: &Path) -> Result<Vec<Value>> {
if !path.exists() {
return Ok(Vec::new());
}
let text = fs::read_to_string(path)
.with_context(|| format!("failed to inspect configuration {}", path.display()))?;
let value: toml::Value = toml::from_str(&text)
.with_context(|| format!("failed to parse configuration {}", path.display()))?;
let mut findings = Vec::new();
collect_secret_keys(&value, "", &mut findings);
for (line_index, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
let lowercase = trimmed.to_ascii_lowercase();
if ["sk-", "ghp_", "xoxb-", "bearer ", "aiza"]
.iter()
.any(|marker| lowercase.contains(marker))
{
findings.push(format!(
"line {} contains a credential-like literal",
line_index + 1
));
}
}
findings.sort();
findings.dedup();
Ok(findings
.into_iter()
.map(|finding| json!({"file": path, "finding": finding}))
.collect())
}
fn collect_secret_keys(value: &toml::Value, prefix: &str, findings: &mut Vec<String>) {
let toml::Value::Table(table) = value else {
return;
};
for (key, child) in table {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
let normalized = key.to_ascii_lowercase().replace(['-', '_'], "");
let reference_only = matches!(
normalized.as_str(),
"credentialenv" | "credentialids" | "headerenv"
) || prefix.contains("header_env");
let secret_like = [
"apikey",
"accesstoken",
"secret",
"password",
"authorization",
]
.iter()
.any(|needle| normalized.contains(needle));
if secret_like && !reference_only && !matches!(child, toml::Value::Table(_)) {
findings.push(format!("{path} contains a secret-like value"));
}
if !reference_only {
collect_secret_keys(child, &path, findings);
}
}
}
fn command_version(program: &str, args: &[&str]) -> Value {
match ProcessCommand::new(program).args(args).output() {
Ok(output) => json!({
"available": output.status.success(),
"version": String::from_utf8_lossy(&output.stdout).trim(),
}),
Err(error) => json!({"available": false, "error": error.to_string()}),
}
}
fn canonical_workspace(path: &Path) -> Result<PathBuf> {
path.canonicalize()
.with_context(|| format!("workspace does not exist: {}", path.display()))
}
fn resolve_workspace_argument(workspace: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
workspace.join(path)
}
}
fn outcome_exit_code(outcome: RunOutcome) -> ExitCode {
match outcome {
RunOutcome::Verified | RunOutcome::VerifiedWithWaivers => ExitCode::SUCCESS,
RunOutcome::Failed => ExitCode::from(2),
RunOutcome::Blocked => ExitCode::from(3),
RunOutcome::Cancelled => ExitCode::from(130),
}
}
fn print_json(value: &impl Serialize) -> Result<()> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}
struct JsonlSink {
stdout: Mutex<io::Stdout>,
}
impl JsonlSink {
fn new() -> Self {
Self {
stdout: Mutex::new(io::stdout()),
}
}
}
impl EventSink for JsonlSink {
fn emit(&self, event: &EventEnvelope) -> std::result::Result<(), String> {
let mut stdout = self
.stdout
.lock()
.map_err(|_| "stdout lock poisoned".to_owned())?;
serde_json::to_writer(&mut *stdout, event).map_err(|error| error.to_string())?;
stdout.write_all(b"\n").map_err(|error| error.to_string())?;
stdout.flush().map_err(|error| error.to_string())
}
}
struct HumanSink;
impl EventSink for HumanSink {
fn emit(&self, event: &EventEnvelope) -> std::result::Result<(), String> {
eprintln!("{:04} {}", event.seq, event.kind);
Ok(())
}
}
#[cfg(test)]
mod tests {
use proofborne_core::{Criterion, CriterionState, EvidenceFreshness, EvidenceKind};
use super::*;
#[test]
fn contract_file_cannot_smuggle_passed_state() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("contract.json");
let mut criterion = Criterion::required("test", "passes");
criterion.evidence_requirement.allowed_kinds =
[EvidenceKind::Process].into_iter().collect();
criterion.evidence_requirement.allowed_producers =
["test.verifier".to_owned()].into_iter().collect();
criterion.evidence_requirement.freshness = EvidenceFreshness::FinalWorkspaceState;
let mut contract = TaskContract::new("goal", vec![criterion]);
contract.confirmed = true;
contract.criteria[0].state = CriterionState::Passed;
fs::write(&path, serde_json::to_vec(&contract).unwrap()).unwrap();
let loaded = load_contract(None, Some(&path), false, &[]).unwrap();
assert_eq!(loaded.criteria[0].state, CriterionState::Pending);
}
#[test]
fn doctor_detects_literal_secrets_but_allows_environment_references() {
let directory = tempfile::tempdir().unwrap();
let safe = directory.path().join("safe.toml");
fs::write(
&safe,
"[providers.local]\nprovider = 'openai'\nmodel = 'test'\ncredential_env = 'OPENAI_API_KEY'\n",
)
.unwrap();
assert!(scan_config_for_secrets(&safe).unwrap().is_empty());
let unsafe_path = directory.path().join("unsafe.toml");
fs::write(&unsafe_path, "api_key = 'sk-example-must-not-persist'\n").unwrap();
let findings = scan_config_for_secrets(&unsafe_path).unwrap();
assert!(!findings.is_empty());
let serialized = serde_json::to_string(&findings).unwrap();
assert!(!serialized.contains("sk-example-must-not-persist"));
}
#[test]
fn auto_contract_requires_and_compiles_runtime_verifiers() {
let runtime_only = compile_auto_contract("change code", &[]).unwrap();
assert_eq!(
runtime_only.claim_scope,
proofborne_core::ClaimScope::Runtime
);
let command = VerificationCommand {
criterion_id: "tests_pass".to_owned(),
program: "cargo".to_owned(),
args: vec!["test".to_owned()],
cwd: None,
timeout_seconds: 120,
};
let compiled = compile_auto_contract("change code", &[command]).unwrap();
assert_eq!(compiled.claim_scope, proofborne_core::ClaimScope::Task);
assert_eq!(compiled.criteria.len(), 1);
assert!(
compiled.criteria[0]
.evidence_requirement
.allowed_producers
.contains("verify.exec")
);
assert_eq!(
compiled.criteria[0].evidence_requirement.freshness,
EvidenceFreshness::FinalWorkspaceState
);
}
#[test]
fn exit_codes_are_stable() {
assert_eq!(outcome_exit_code(RunOutcome::Verified), ExitCode::from(0));
assert_eq!(outcome_exit_code(RunOutcome::Failed), ExitCode::from(2));
assert_eq!(outcome_exit_code(RunOutcome::Blocked), ExitCode::from(3));
}
#[test]
fn declared_capabilities_do_not_hide_streaming_adapters() {
for provider in ["openai", "anthropic", "gemini", "openai_compatible"] {
assert_eq!(
declared_capabilities(provider)["streaming"],
true,
"{provider} implements Provider::stream"
);
}
assert_eq!(declared_capabilities("mock")["streaming"], false);
assert_eq!(declared_capabilities("unknown")["streaming"], false);
}
#[test]
fn proof_reproduction_is_non_executing_unless_explicitly_opted_in() {
let cli = Cli::try_parse_from(["proofborne", "proof", "reproduce", "proof.zip"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Proof {
command: ProofCommand::Reproduce { execute: false, .. }
})
));
let cli = Cli::try_parse_from([
"proofborne",
"proof",
"reproduce",
"proof.zip",
"--execute",
"--json",
])
.unwrap();
assert!(matches!(
cli.command,
Some(Command::Proof {
command: ProofCommand::Reproduce {
execute: true,
json: true,
..
}
})
));
}
#[test]
fn proof_export_keeps_native_zip_and_optionally_adds_intoto() {
let session = Uuid::now_v7();
let cli = Cli::try_parse_from([
"proofborne",
"proof",
"export",
&session.to_string(),
"--output",
"proof.zip",
"--in-toto-output",
"statement.json",
])
.unwrap();
assert!(matches!(
cli.command,
Some(Command::Proof {
command: ProofCommand::Export {
output,
in_toto_output: Some(attestation),
..
}
}) if output == Path::new("proof.zip")
&& attestation == Path::new("statement.json")
));
}
}