use std::collections::BTreeSet;
use std::io::Read;
use std::path::Path;
use tirith_core::effects::{BoundaryCapability, CommandEffectKind};
use tirith_core::policy::Policy;
use tirith_core::task::{
assign_provenance, decide_document, document_decision_projection, parse_envelope_document,
rejection_token, validate_envelope, EnvelopeRejection, IngressAdapter, TaskDecision,
MAX_TASK_DOCUMENT_BYTES,
};
const EXIT_OK: i32 = 0;
const EXIT_RESTRICTED: i32 = 1;
const EXIT_INPUT: i32 = 2;
fn read_envelope(path: Option<&Path>) -> Result<String, String> {
let cap = MAX_TASK_DOCUMENT_BYTES;
let mut buffer = String::new();
match path {
Some(path) => {
let file = std::fs::File::open(path)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
file.take(cap as u64 + 1)
.read_to_string(&mut buffer)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
}
None => {
std::io::stdin()
.take(cap as u64 + 1)
.read_to_string(&mut buffer)
.map_err(|error| format!("cannot read stdin: {error}"))?;
}
}
if buffer.len() > cap {
return Err(format!("envelope exceeds the {cap}-byte input cap"));
}
Ok(buffer)
}
fn adapter_from_flag(name: Option<&str>) -> Result<IngressAdapter, String> {
Ok(match name.unwrap_or("operator-ingest") {
"operator-ingest" => IngressAdapter::OperatorIngest,
"github-issue" => IngressAdapter::GithubIssue,
"github-pull-request" => IngressAdapter::GithubPullRequest,
"file-read" => IngressAdapter::FileRead,
"http-fetch" => IngressAdapter::HttpFetch,
"unattributed" => IngressAdapter::Unattributed,
other => {
return Err(format!(
"unknown --adapter '{other}'; expected one of operator-ingest, github-issue, \
github-pull-request, file-read, http-fetch, unattributed"
))
}
})
}
fn effect_token(effect: CommandEffectKind) -> &'static str {
match effect {
CommandEffectKind::PackageInstall => "package_install",
CommandEffectKind::PersistenceChange => "persistence_change",
CommandEffectKind::PolicyChange => "policy_change",
CommandEffectKind::SecretRead => "secret_read",
CommandEffectKind::NetworkEgress => "network_egress",
CommandEffectKind::FilesystemWrite => "filesystem_write",
CommandEffectKind::ResourceEscalation => "resource_escalation",
CommandEffectKind::Web3Write => "web3_write",
CommandEffectKind::Web3SignerUse => "web3_signer_use",
}
}
fn print_human(
document: &tirith_core::task_envelope::TaskEnvelopeDocument,
decision: &TaskDecision,
rejections: &[EnvelopeRejection],
) {
println!("tirith task check (diagnostic — nothing was executed)");
println!(" envelope: schema v{}", document.version);
if document
.envelope
.actions
.iter()
.any(|action| matches!(action, tirith_core::task::ProposedAction::Shell { .. }))
{
println!(
" shell claims: {:?} (diagnostic only)",
document.shell_claims
);
}
println!(" gate mode: {:?}", decision.mode);
println!(
" assessment: {}",
if decision.complete {
"complete"
} else {
"INCOMPLETE — treat unlisted effects as unknown, not absent"
}
);
println!(" enforceable at: {:?}", decision.enforceability);
for provenance in &decision.provenance {
let laundered = provenance.claimed_source != provenance.effective_source;
println!(
" source: claimed {:?} -> assigned {:?}{} (receipt: {:?})",
provenance.claimed_source,
provenance.effective_source,
if laundered {
" [claim not honored]"
} else {
""
},
provenance.receipt_status
);
}
let render = |label: &str, effects: &BTreeSet<CommandEffectKind>| {
if effects.is_empty() {
println!(" {label}: (none)");
} else {
println!(
" {label}: {}",
effects
.iter()
.map(|effect| effect_token(*effect))
.collect::<Vec<_>>()
.join(", ")
);
}
};
render("inferred", &decision.inferred_effects);
render("allowed ", &decision.allowed_effects);
render("denied ", &decision.denied_effects);
for rejection in rejections {
println!(" rejected: {}", rejection_token(rejection));
}
}
pub fn run(path: Option<&Path>, adapter: Option<&str>, json: bool) -> i32 {
let adapter = match adapter_from_flag(adapter) {
Ok(adapter) => adapter,
Err(message) => {
eprintln!("tirith task check: {message}");
return EXIT_INPUT;
}
};
let raw = match read_envelope(path) {
Ok(raw) => raw,
Err(message) => {
eprintln!("tirith task check: {message}");
return EXIT_INPUT;
}
};
let document = match parse_envelope_document(&raw) {
Ok(document) => document,
Err(rejection) => {
let token = rejection_token(&rejection);
if json {
println!(
"{}",
serde_json::json!({
"schema_version": 1,
"envelope_rejections": [token],
"diagnostic": true,
})
);
} else {
eprintln!("tirith task check: envelope rejected: {token}");
}
return EXIT_INPUT;
}
};
let envelope = &document.envelope;
let rejections = validate_envelope(envelope);
let policy = Policy::discover_local_only(None);
let provenance = envelope
.sources
.iter()
.map(|source| assign_provenance(source, adapter, None, None))
.collect::<Vec<_>>();
let decision = decide_document(
&document,
provenance,
&policy.task_gate,
BoundaryCapability::ObserveOnly,
None,
);
if json {
println!(
"{}",
document_decision_projection(&document, &decision, &rejections)
);
} else {
print_human(&document, &decision, &rejections);
}
if !decision.denied_effects.is_empty() || !decision.complete || !rejections.is_empty() {
EXIT_RESTRICTED
} else {
EXIT_OK
}
}