mod acp;
mod oauth;
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::{
OAuthHttpClient, OAuthProviderConfig,
OAuthTokenRequestFormat as AdapterOAuthTokenRequestFormat, ProviderBuildError,
ProviderCredentialKind, build_provider, build_unavailable_provider,
declared_provider_capabilities, register_mcp_servers,
};
use proofborne_core::{
AgentGraphOutcome, AgentGraphPlan, AgentGraphTemplate, Criterion, EventEnvelope,
EvidenceFreshness, EvidenceKind, MemoryItem, RunOutcome, TaskContract, TokenStatus,
verify_event_chain,
};
use proofborne_runtime::{
AppConfig, AuthCredential, AuthCredentialKind, AuthStore, CasStore, EventSink,
OAuthProfileConfig, OAuthTokenRequestFormat as RuntimeOAuthTokenRequestFormat, PolicyConfig,
PolicyPreset, ProviderAttempt, ProviderChain, ProviderProfile, ProviderRegistry, Redactor,
RoutingConfig, RunControl, RunOptions, RuntimePaths, SessionEngine, SessionStore, ToolRegistry,
VerificationCommand, diff_proof_bundles, export_intoto_statement, export_proof_bundle,
reproduce_proof_bundle, snapshot_workspace, 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,
},
Graph {
#[command(subcommand)]
command: GraphCommand,
},
Memory {
#[command(subcommand)]
command: MemoryCommand,
},
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, Subcommand)]
enum GraphCommand {
Plan(GraphPlanArgs),
Run(GraphRunArgs),
Inspect(GraphInspectArgs),
}
#[derive(Debug, Args)]
struct GraphPlanArgs {
#[arg(long)]
template: PathBuf,
#[arg(long)]
parent_session: Uuid,
#[arg(long)]
out: PathBuf,
#[arg(long)]
json: bool,
}
#[derive(Debug, Args)]
struct GraphInspectArgs {
#[arg(long)]
plan: PathBuf,
#[arg(long)]
json: bool,
}
#[derive(Debug, Args)]
struct GraphRunArgs {
#[arg(long)]
plan: PathBuf,
#[arg(long, value_enum)]
policy: Option<PolicyArg>,
#[arg(long, requires = "policy")]
trust_workspace: bool,
#[arg(long)]
json: bool,
}
#[derive(Debug, Subcommand)]
enum MemoryCommand {
Search(MemorySearchArgs),
Get(MemoryGetArgs),
}
#[derive(Debug, Args)]
struct MemorySearchArgs {
#[arg(long)]
query: String,
#[arg(long, default_value_t = 10)]
limit: usize,
#[arg(long)]
include_history: bool,
#[arg(long)]
json: bool,
}
#[derive(Debug, Args)]
struct MemoryGetArgs {
#[arg(long)]
id: Uuid,
#[arg(long)]
json: 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,
},
Login {
#[arg(long)]
profile: Option<String>,
#[arg(long, default_value = "oauth")]
id: String,
#[arg(long)]
no_browser: bool,
#[arg(long, default_value_t = 300)]
timeout_seconds: u64,
},
List {
#[arg(long)]
provider: Option<String>,
#[arg(long)]
json: bool,
},
Status {
#[arg(long)]
profile: 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 Box::pin(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).await,
Some(Command::Models { json }) => models_command(&workspace, json),
Some(Command::Session { command }) => Box::pin(session_command(&workspace, command)).await,
Some(Command::Proof { command }) => proof_command(&workspace, command).await,
Some(Command::Memory { command }) => memory_command(&workspace, command),
Some(Command::Graph { command }) => graph_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;
options.routing = prepared.routing.clone();
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 graph_command(workspace: &Path, command: GraphCommand) -> Result<ExitCode> {
match command {
GraphCommand::Plan(args) => {
let template_path = resolve_workspace_argument(workspace, &args.template);
let output_path = resolve_workspace_argument(workspace, &args.out);
require_graph_plan_output_outside_workspace(workspace, &output_path)?;
let template: AgentGraphTemplate = parse_path(&template_path).with_context(|| {
format!("failed to load graph template {}", template_path.display())
})?;
let (contract, parent_state_binding) =
load_verified_graph_parent(workspace, args.parent_session)?;
let graph = template.bind(args.parent_session, &contract, parent_state_binding)?;
write_json_file(&output_path, &graph)?;
if args.json {
print_json(&graph)?;
} else {
println!("wrote immutable graph plan {}", output_path.display());
}
Ok(ExitCode::SUCCESS)
}
GraphCommand::Inspect(args) => {
let plan_path = resolve_workspace_argument(workspace, &args.plan);
let graph: AgentGraphPlan = parse_path(&plan_path)
.with_context(|| format!("failed to load graph {}", plan_path.display()))?;
let _contract = validate_graph_parent_binding(workspace, &graph)?;
if args.json {
print_json(&graph)?;
} else {
println!(
"graph: {}\nparent: {}\nnodes: {}",
graph.graph_id,
graph.parent_session_id,
graph.nodes.len()
);
for node in &graph.nodes {
let dependencies = if node.dependencies.is_empty() {
"-".to_owned()
} else {
node.dependencies
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join(",")
};
println!(
"{}\t{:?}\tdeps={}\t{}/{}",
node.node_id,
node.role,
dependencies,
node.authority.provider,
node.authority.model
);
}
}
Ok(ExitCode::SUCCESS)
}
GraphCommand::Run(args) => {
let plan_path = resolve_workspace_argument(workspace, &args.plan);
let graph: AgentGraphPlan = parse_path(&plan_path)
.with_context(|| format!("failed to load graph {}", plan_path.display()))?;
let contract = validate_graph_parent_binding(workspace, &graph)?;
let (engine, providers, mut policy, redactor) =
prepare_graph_engine(workspace, &graph, args.policy.map(Into::into)).await?;
if args.trust_workspace {
if policy.preset != PolicyPreset::Workspace {
bail!("--trust-workspace requires --policy workspace");
}
policy.trusted_workspace = true;
}
let mut options = RunOptions::new(
workspace.to_path_buf(),
"proofborne-graph-scheduler",
contract,
);
options.policy = policy;
options.redactor = redactor;
let result = if args.json {
engine
.run_agent_graph(graph, &providers, options, &SilentSink)
.await?
} else {
engine
.run_agent_graph(graph, &providers, options, &HumanSink)
.await?
};
if args.json {
print_json(&result)?;
} else {
println!(
"outcome: {:?}\ngraph: {}\nparent generation: {}",
result.outcome,
result.graph_id,
result.scheduler_receipt.final_workspace_generation
);
}
Ok(match result.outcome {
AgentGraphOutcome::Verified => ExitCode::SUCCESS,
AgentGraphOutcome::Cancelled => ExitCode::from(130),
AgentGraphOutcome::Blocked | AgentGraphOutcome::Failed => ExitCode::from(3),
})
}
}
}
fn validate_graph_parent_binding(workspace: &Path, graph: &AgentGraphPlan) -> Result<TaskContract> {
let (contract, parent_state_binding) =
load_verified_graph_parent(workspace, graph.parent_session_id)?;
ensure_graph_parent_binding(&graph.parent_state_binding, &parent_state_binding)?;
graph.validate(&contract)?;
Ok(contract)
}
fn ensure_graph_parent_binding(plan_binding: &str, verified_binding: &str) -> Result<()> {
if plan_binding != verified_binding {
bail!(
"graph parent state binding {plan_binding} differs from verified parent binding {verified_binding}"
);
}
Ok(())
}
fn memory_command(workspace: &Path, command: MemoryCommand) -> Result<ExitCode> {
let paths = RuntimePaths::for_workspace(workspace)?;
paths.ensure_data_directories()?;
let store = SessionStore::open(paths.workspace_data.join("sessions.sqlite3"))?;
let snapshot = snapshot_workspace(workspace)?;
match command {
MemoryCommand::Search(args) => {
let items = store.search_memory(
workspace,
&snapshot.generation,
&args.query,
args.limit,
args.include_history,
)?;
if args.json {
print_json(&items)?;
} else if items.is_empty() {
println!("no matching workspace memory");
} else {
for item in &items {
print_memory_item_human(item);
}
}
}
MemoryCommand::Get(args) => {
let item = store.get_memory(workspace, &snapshot.generation, args.id)?;
if args.json {
print_json(&item)?;
} else {
print_memory_item_human(&item);
}
}
}
Ok(ExitCode::SUCCESS)
}
fn print_memory_item_human(item: &MemoryItem) {
let preview = item
.context
.content
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(160)
.collect::<String>();
println!(
"{}\t{:?}\t{}\t{}",
item.context.id, item.status, item.updated_at, preview
);
}
async fn prepare_graph_engine(
workspace: &Path,
graph: &AgentGraphPlan,
policy_override: Option<PolicyPreset>,
) -> Result<(SessionEngine, ProviderRegistry, PolicyConfig, Redactor)> {
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 auth = AuthStore::new(&paths.user_data);
let mut providers = ProviderRegistry::default();
let mut secrets = Vec::new();
for node in &graph.nodes {
let profile = config
.providers
.get(&node.authority.profile)
.ok_or_else(|| {
anyhow!(
"agent node {} references unknown profile {}",
node.node_id,
node.authority.profile
)
})?;
refresh_profile_oauth_credentials(&auth, &node.authority.profile, profile).await?;
let credentials = auth.resolve_all(profile)?;
let credential = match node.authority.credential_id.as_deref() {
Some(id) => Some(
credentials
.into_iter()
.find(|credential| credential_id_matches(&credential.id, id))
.ok_or_else(|| {
anyhow!(
"agent node {} credential {} is unavailable",
node.node_id,
id
)
})?,
),
None => None,
};
let secret = credential
.as_ref()
.map(|credential| credential.secret.clone());
let account_id = credential
.as_ref()
.and_then(|credential| credential.account_id.clone());
let credential_kind = credential.as_ref().map_or(
ProviderCredentialKind::ApiKey,
|credential| match credential.kind {
AuthCredentialKind::ApiKey => ProviderCredentialKind::ApiKey,
AuthCredentialKind::OAuthAccessToken => ProviderCredentialKind::OAuthAccessToken,
},
);
let provider = build_provider(profile, secret.clone(), account_id, credential_kind)?;
if provider.name() != node.authority.provider {
bail!(
"agent node {} authority adapter {} differs from configured adapter {}",
node.node_id,
node.authority.provider,
provider.name()
);
}
if let Some(secret) = secret {
secrets.push(secret);
}
providers.register_attempt(
node.authority.profile.clone(),
node.authority.credential_id.clone(),
provider,
);
}
let mut tools = ToolRegistry::with_builtins();
register_mcp_servers(&config.mcp, &config.policy.mcp_allow, &mut tools).await?;
let engine = SessionEngine::new(
SessionStore::open(paths.workspace_data.join("sessions.sqlite3"))?,
CasStore::new(paths.workspace_data.join("cas"))?,
tools,
);
Ok((
engine,
providers,
config.policy,
Redactor::with_secrets(secrets),
))
}
fn credential_id_matches(candidate: &str, requested: &str) -> bool {
candidate == requested
|| candidate
.strip_prefix("keyring:")
.is_some_and(|stripped| stripped == requested)
|| candidate
.strip_prefix("env:")
.is_some_and(|stripped| stripped == requested)
}
fn load_verified_graph_parent(
workspace: &Path,
parent_session_id: Uuid,
) -> Result<(TaskContract, String)> {
let (_, store, _) = open_stores(workspace)?;
let record = store.load_session(parent_session_id)?;
let recorded_contract = record
.contract
.as_ref()
.ok_or_else(|| anyhow!("graph parent has no finalized contract"))?;
let proof = record
.proof
.as_ref()
.ok_or_else(|| anyhow!("graph parent has no finalized proof graph"))?;
let canonical_workspace = fs::canonicalize(workspace)
.with_context(|| format!("failed to canonicalize workspace {}", workspace.display()))?;
let recorded_workspace = fs::canonicalize(&record.workspace).with_context(|| {
format!(
"failed to canonicalize recorded workspace {}",
record.workspace.display()
)
})?;
let mut evaluated_contract = normalize_contract_runtime_state(recorded_contract.clone());
let outcome = proof.evaluate(&mut evaluated_contract)?;
verify_event_chain(&record.events)?;
let state_binding = proof
.final_state_binding
.clone()
.ok_or_else(|| anyhow!("graph parent proof lacks a final state binding"))?;
let current_snapshot = snapshot_workspace(&canonical_workspace)?;
if record.status != "verified"
|| outcome != RunOutcome::Verified
|| evaluated_contract != *recorded_contract
|| canonical_workspace != recorded_workspace
|| current_snapshot.generation != state_binding
|| record.events.last().map(|event| event.kind.as_str()) != Some("proof.finalized")
{
bail!(
"graph planning requires an exact verified parent proof at the current workspace state"
);
}
Ok((evaluated_contract, state_binding))
}
fn require_graph_plan_output_outside_workspace(workspace: &Path, output: &Path) -> Result<()> {
let canonical_workspace = fs::canonicalize(workspace)
.with_context(|| format!("failed to canonicalize workspace {}", workspace.display()))?;
let output_parent = output
.parent()
.ok_or_else(|| anyhow!("graph plan output has no parent directory"))?;
let canonical_output_parent = fs::canonicalize(output_parent).with_context(|| {
format!(
"failed to canonicalize graph plan output directory {}",
output_parent.display()
)
})?;
if canonical_output_parent.starts_with(&canonical_workspace) {
bail!(
"graph plan output must be outside the parent workspace so it cannot invalidate the pinned parent state"
);
}
Ok(())
}
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.routing = prepared.routing.clone();
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 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()?;
Ok(normalize_contract_runtime_state(contract))
}
fn normalize_contract_runtime_state(mut contract: TaskContract) -> TaskContract {
for criterion in &mut contract.criteria {
if criterion.waiver.is_none() {
criterion.state = proofborne_core::CriterionState::Pending;
criterion.evidence_ids.clear();
}
}
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,
routing: RoutingConfig,
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");
refresh_profile_oauth_credentials(&auth, &name, profile).await?;
let (profile_attempts, profile_secrets) =
build_profile_attempts(&name, profile, auth.resolve_all(profile)?, Some(&auth))?;
attempts.extend(profile_attempts);
secrets.extend(profile_secrets);
}
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,
routing: config.routing,
redactor,
})
}
fn oauth_provider_config(oauth: &OAuthProfileConfig) -> OAuthProviderConfig {
OAuthProviderConfig {
authorize_endpoint: oauth.authorize_endpoint.clone(),
token_endpoint: oauth.token_endpoint.clone(),
client_id: oauth.client_id.clone(),
redirect_uri: oauth.redirect_uri.clone(),
scopes: oauth.scopes.clone(),
device_code_endpoint: oauth.device_code_endpoint.clone(),
authorize_params: oauth.authorize_params.clone(),
token_request_format: match oauth.token_request_format {
RuntimeOAuthTokenRequestFormat::Form => AdapterOAuthTokenRequestFormat::Form,
RuntimeOAuthTokenRequestFormat::Json => AdapterOAuthTokenRequestFormat::Json,
},
token_headers: oauth.token_headers.clone(),
include_state_in_token_request: oauth.include_state_in_token_request,
account_id_claim: oauth.account_id_claim.clone(),
account_id_response_pointer: oauth.account_id_response_pointer.clone(),
}
}
async fn refresh_profile_oauth_credentials(
auth: &AuthStore,
profile_name: &str,
profile: &ProviderProfile,
) -> Result<()> {
let Some(oauth) = profile.oauth.as_ref() else {
return Ok(());
};
let ids = if profile.credential_ids.is_empty() {
auth.list(Some(&profile.provider))?
.into_iter()
.map(|metadata| metadata.id)
.collect::<Vec<_>>()
} else {
profile.credential_ids.clone()
};
let provider = oauth_provider_config(oauth);
let client = OAuthHttpClient::new().map_err(|error| anyhow!(error))?;
for id in ids {
let Some(bindings) = auth.bindings(profile_name, profile, &id)? else {
continue;
};
if bindings.token_status != TokenStatus::NeedsRefresh {
continue;
}
let credential = auth.oauth_credential(&profile.provider, &id)?;
let refresh_token = credential.refresh_token.as_deref().ok_or_else(|| {
anyhow!(
"OAuth credential {}/{} needs refresh but has no refresh token; log in again",
profile.provider,
id
)
})?;
let refreshed = client
.refresh(&provider, refresh_token)
.await
.map_err(|error| {
anyhow!(
"OAuth credential {}/{} could not be refreshed: {error}",
profile.provider,
id
)
})?;
let scopes = if refreshed.scopes.is_empty() {
credential.scopes
} else {
refreshed.scopes
};
auth.add_oauth(
&profile.provider,
&id,
&refreshed.access_token,
refreshed.refresh_token,
credential.account_id,
scopes,
refreshed.expires_at,
)?;
}
Ok(())
}
fn build_profile_attempts(
profile_name: &str,
profile: &ProviderProfile,
credentials: Vec<AuthCredential>,
auth: Option<&AuthStore>,
) -> Result<(Vec<ProviderAttempt>, Vec<String>)> {
if credentials.is_empty() {
let (provider, routing) =
match build_provider(profile, None, None, ProviderCredentialKind::ApiKey) {
Ok(provider) => (provider, profile.routing.clone()),
Err(ProviderBuildError::MissingCredential) => {
let mut routing = profile.routing.clone();
routing.credential_required = true;
(build_unavailable_provider(profile)?, routing)
}
Err(error) => return Err(error.into()),
};
return Ok((
vec![
ProviderAttempt::new(profile_name, profile.model.clone(), None, provider)
.with_routing(routing),
],
Vec::new(),
));
}
let mut attempts = Vec::with_capacity(credentials.len());
let mut secrets = Vec::with_capacity(credentials.len());
for credential in credentials {
let AuthCredential {
id,
secret,
account_id,
kind,
} = credential;
secrets.push(secret.clone());
let attempt = ProviderAttempt::new(
profile_name,
profile.model.clone(),
Some(id.clone()),
build_provider(
profile,
Some(secret),
account_id,
match kind {
AuthCredentialKind::ApiKey => ProviderCredentialKind::ApiKey,
AuthCredentialKind::OAuthAccessToken => {
ProviderCredentialKind::OAuthAccessToken
}
},
)?,
)
.with_routing(profile.routing.clone());
let with_auth = match auth {
Some(store) if id.starts_with("keyring:") => {
store.bindings(profile_name, profile, &id)?
}
_ => None,
};
let attempt = if let Some(bindings) = with_auth {
attempt.with_auth(Some(bindings.token_status), bindings.scopes)
} else {
attempt
};
attempts.push(attempt);
}
Ok((attempts, secrets))
}
async 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::Login {
profile,
id,
no_browser,
timeout_seconds,
} => {
if timeout_seconds == 0 {
bail!("OAuth callback timeout must be greater than zero");
}
if id.is_empty()
|| id.len() > 128
|| !id
.chars()
.all(|character| character.is_ascii_alphanumeric() || "-_.".contains(character))
{
bail!(
"OAuth credential id must contain only ASCII letters, digits, '-', '_', or '.'"
);
}
let config = AppConfig::load(workspace)?;
let (profile_name, provider_profile) = config.selected_profile(profile.as_deref())?;
if !provider_profile.credential_ids.is_empty()
&& !provider_profile
.credential_ids
.iter()
.any(|configured| configured == &id)
{
bail!(
"OAuth credential id {id} is not configured for profile {profile_name}; add it to credential_ids or omit that field"
);
}
let oauth = provider_profile.oauth.as_ref().ok_or_else(|| {
anyhow!(
"profile {profile_name} has no OAuth metadata; configure [providers.{profile_name}.oauth]"
)
})?;
let oauth_provider = oauth_provider_config(oauth);
let (pkce, code, state, effective_provider) = oauth::authorize_code(
&oauth_provider,
no_browser,
std::time::Duration::from_secs(timeout_seconds),
)
.await?;
let http = OAuthHttpClient::new().map_err(|error| anyhow!(error))?;
let tokens = http
.exchange_code(&effective_provider, &code, &pkce.verifier, Some(&state))
.await
.map_err(|error| anyhow!(error))?;
let scopes = if tokens.scopes.is_empty() {
oauth.scopes.clone()
} else {
tokens.scopes.clone()
};
let expires_at = tokens.expires_at;
store.add_oauth(
&provider_profile.provider,
&id,
&tokens.access_token,
tokens.refresh_token.clone(),
tokens.account_id.clone(),
scopes.clone(),
expires_at,
)?;
let bindings = store
.bindings(profile_name, provider_profile, &format!("keyring:{id}"))?
.ok_or_else(|| anyhow!("OAuth credential metadata was not indexed"))?;
println!(
"{}",
serde_json::to_string(&json!({
"authProtocol": "proofborne.auth.v1",
"profile": profile_name,
"provider": provider_profile.provider,
"credentialId": id,
"accountId": bindings.account_id,
"tokenStatus": bindings.token_status,
"refreshAvailable": bindings.refresh_available,
"scopes": bindings.scopes,
"expiresAt": expires_at,
}))?
);
}
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::Status { profile, json } => {
let config = AppConfig::load(workspace)?;
let mut rows = Vec::new();
for (name, provider_profile) in &config.providers {
if profile
.as_deref()
.is_some_and(|requested| requested != name)
{
continue;
}
let ids = if provider_profile.credential_ids.is_empty() {
store
.list(Some(&provider_profile.provider))?
.into_iter()
.map(|metadata| metadata.id)
.collect::<Vec<_>>()
} else {
provider_profile.credential_ids.clone()
};
for id in ids {
if let Some(bindings) = store.bindings(name, provider_profile, &id)? {
rows.push(json!({
"profile": bindings.profile,
"provider": bindings.provider,
"credentialId": id,
"accountId": bindings.account_id,
"credentialIdentityHash": bindings.credential_identity_hash,
"tokenStatus": bindings.token_status,
"refreshAvailable": bindings.refresh_available,
"scopes": bindings.scopes,
}));
}
}
}
if json {
print_json(&rows)?;
} else if rows.is_empty() {
println!("no indexed keychain credentials match the configured profiles");
} else {
for row in rows {
println!(
"{}\t{}\t{}\t{}\t{}",
row["profile"].as_str().unwrap_or_default(),
row["credentialId"].as_str().unwrap_or_default(),
row["tokenStatus"].as_str().unwrap_or_default(),
row["accountId"].as_str().unwrap_or_default(),
if row["refreshAvailable"].as_bool() == Some(true) {
"refresh"
} else {
""
},
);
}
}
}
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 = config
.providers
.iter()
.map(|(name, profile)| {
Ok(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)?,
"routing": {
"enabled": profile.routing.enabled,
"credentialRequired": profile.routing.credential_required,
"contextTokens": profile.routing.context_tokens,
"pricing": profile.routing.pricing,
"expectedLatencyMs": profile.routing.expected_latency_ms,
},
}))
})
.collect::<Result<Vec<_>>>()?;
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) -> Result<Value> {
let capabilities = declared_provider_capabilities(provider)?;
Ok(json!({
"streaming": capabilities.streaming,
"tools": capabilities.tools,
"structuredOutput": capabilities.structured_output,
"multimodalInput": capabilities.multimodal_input,
"contextTokens": capabilities.context_tokens,
}))
}
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.routing = prepared.routing.clone();
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(())
}
fn write_json_file(path: &Path, value: &impl Serialize) -> Result<()> {
let mut output = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.with_context(|| format!("failed to create immutable graph plan {}", path.display()))?;
serde_json::to_writer_pretty(&mut output, value)
.with_context(|| format!("failed to serialize graph plan {}", path.display()))?;
output.write_all(b"\n")?;
output.flush()?;
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 SilentSink;
impl EventSink for SilentSink {
fn emit(&self, _event: &EventEnvelope) -> std::result::Result<(), String> {
Ok(())
}
}
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 missing_mandatory_credential_remains_an_excluded_routing_candidate() {
let profile = ProviderProfile {
provider: "openai".to_owned(),
model: "gpt-test".to_owned(),
base_url: None,
oauth: None,
credential_env: None,
credential_ids: Vec::new(),
fallbacks: Vec::new(),
timeout_seconds: 30,
store_remote: false,
routing: Default::default(),
};
let (attempts, secrets) =
build_profile_attempts("primary", &profile, Vec::new(), None).unwrap();
assert!(secrets.is_empty());
let mut attempts = attempts.into_iter();
let chain = ProviderChain::new(attempts.next().unwrap(), attempts);
let inventory = chain.routing_inventory(32_768).unwrap();
assert_eq!(inventory.catalog.descriptors[0].provider, "openai");
assert!(!inventory.candidates[0].credential_available);
}
#[test]
fn declared_capabilities_do_not_hide_streaming_adapters() {
for provider in ["openai", "anthropic", "gemini", "openai_compatible"] {
assert_eq!(
declared_capabilities(provider).unwrap()["streaming"],
true,
"{provider} implements Provider::stream"
);
}
assert_eq!(declared_capabilities("mock").unwrap()["streaming"], false);
assert!(declared_capabilities("unknown").is_err());
}
#[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 graph_plan_requires_exact_parent_and_output_path() {
let parent_session = Uuid::now_v7();
let cli = Cli::try_parse_from([
"proofborne",
"graph",
"plan",
"--template",
"graph-template.json",
"--parent-session",
&parent_session.to_string(),
"--out",
"graph-plan.json",
"--json",
])
.unwrap();
assert!(matches!(
cli.command,
Some(Command::Graph {
command: GraphCommand::Plan(GraphPlanArgs {
template,
parent_session: parsed_parent,
out,
json: true,
})
}) if template == Path::new("graph-template.json")
&& parsed_parent == parent_session
&& out == Path::new("graph-plan.json")
));
}
#[test]
fn graph_plan_output_is_create_only() {
let directory = tempfile::tempdir().unwrap();
let output = directory.path().join("graph-plan.json");
write_json_file(&output, &json!({"graphId": "first"})).unwrap();
let error = write_json_file(&output, &json!({"graphId": "replacement"})).unwrap_err();
assert!(
error
.to_string()
.contains("failed to create immutable graph plan")
);
assert_eq!(
serde_json::from_slice::<Value>(&fs::read(output).unwrap()).unwrap(),
json!({"graphId": "first"})
);
}
#[test]
fn graph_plan_output_must_not_invalidate_the_parent_workspace() {
let directory = tempfile::tempdir().unwrap();
let workspace = directory.path().join("workspace");
let outside = directory.path().join("evidence");
fs::create_dir_all(&workspace).unwrap();
fs::create_dir_all(&outside).unwrap();
let error =
require_graph_plan_output_outside_workspace(&workspace, &workspace.join("plan.json"))
.unwrap_err();
assert!(error.to_string().contains("outside the parent workspace"));
require_graph_plan_output_outside_workspace(&workspace, &outside.join("plan.json"))
.unwrap();
}
#[test]
fn credential_id_matches_raw_keyring_and_environment_ids() {
assert!(credential_id_matches("usability", "usability"));
assert!(credential_id_matches("keyring:usability", "usability"));
assert!(credential_id_matches(
"env:OPENAI_API_KEY",
"OPENAI_API_KEY"
));
assert!(!credential_id_matches("keyring:backup", "usability"));
assert!(!credential_id_matches("env:OPENAI_API_KEY", "usability"));
assert!(!credential_id_matches(
"nested:keyring:usability",
"usability"
));
}
#[test]
fn graph_parent_binding_mismatch_fails_closed() {
assert!(ensure_graph_parent_binding("same", "same").is_ok());
let error = ensure_graph_parent_binding("plan", "verified").unwrap_err();
assert!(error.to_string().contains("differs from verified parent"));
}
#[test]
fn graph_inspect_and_memory_search_parse_read_only_options() {
let graph = Cli::try_parse_from([
"proofborne",
"graph",
"inspect",
"--plan",
"graph-plan.json",
"--json",
])
.unwrap();
assert!(matches!(
graph.command,
Some(Command::Graph {
command: GraphCommand::Inspect(GraphInspectArgs { plan, json: true })
}) if plan == Path::new("graph-plan.json")
));
let memory = Cli::try_parse_from([
"proofborne",
"memory",
"search",
"--query",
"verification evidence",
"--limit",
"7",
"--include-history",
"--json",
])
.unwrap();
assert!(matches!(
memory.command,
Some(Command::Memory {
command: MemoryCommand::Search(MemorySearchArgs {
query,
limit: 7,
include_history: true,
json: true,
})
}) if query == "verification evidence"
));
}
#[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")
));
}
}